Added file selector modal
This commit is contained in:
parent
133ce49855
commit
a57e680024
30 changed files with 533 additions and 129 deletions
|
|
@ -28,10 +28,7 @@ export const touch = new Command("touch")
|
||||||
+ "A FILE argument that does not exist is created empty."
|
+ "A FILE argument that does not exist is created empty."
|
||||||
})
|
})
|
||||||
.setExecute(function(args, { currentDirectory }) {
|
.setExecute(function(args, { currentDirectory }) {
|
||||||
if (args[0] === "girls\\" && args[1] === "boo**")
|
const { name, extension } = VirtualFile.convertId(args[0]);
|
||||||
return `${this.name}: Cannot touch 'girls boo**': Permission denied`;
|
|
||||||
|
|
||||||
const [name, extension] = args[0].split(".");
|
|
||||||
|
|
||||||
if (currentDirectory.findFile(name, extension))
|
if (currentDirectory.findFile(name, extension))
|
||||||
return { blank: true };
|
return { blank: true };
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,19 @@ import { FileProperties } from "../../modals/file-properties/FileProperties.jsx"
|
||||||
import { useHistory } from "../../../hooks/_utils/history.js";
|
import { useHistory } from "../../../hooks/_utils/history.js";
|
||||||
import { Divider } from "../../actions/actions/Divider.jsx";
|
import { Divider } from "../../actions/actions/Divider.jsx";
|
||||||
import { CODE_FORMATS } from "../../../config/apps/textEditor.config.js";
|
import { CODE_FORMATS } from "../../../config/apps/textEditor.config.js";
|
||||||
|
import { SELECTOR_MODE } from "../../../config/apps/fileExplorer.config.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
||||||
|
* @param {string} props.startPath
|
||||||
|
* @param {*} props.Footer
|
||||||
|
* @param {number} props.selectorMode
|
||||||
|
* @param {import("./directory-list/DirectoryList.jsx").onSelectionChange} props.onSelectionChange
|
||||||
|
* @param {Function} props.onSelectionFinish
|
||||||
*/
|
*/
|
||||||
export function FileExplorer({ startPath, app, modalsManager }) {
|
export function FileExplorer({ startPath, selectorMode, Footer, modalsManager, onSelectionChange, onSelectionFinish }) {
|
||||||
|
const isSelector = (Footer != null && selectorMode != null && selectorMode !== SELECTOR_MODE.NONE);
|
||||||
|
|
||||||
const virtualRoot = useVirtualRoot();
|
const virtualRoot = useVirtualRoot();
|
||||||
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
|
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
|
||||||
const [path, setPath] = useState(currentDirectory?.path ?? "");
|
const [path, setPath] = useState(currentDirectory?.path ?? "");
|
||||||
|
|
@ -36,7 +44,12 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
||||||
const { openWindowedModal } = useWindowedModal({ modalsManager });
|
const { openWindowedModal } = useWindowedModal({ modalsManager });
|
||||||
const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
|
const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
|
||||||
<Actions {...props}>
|
<Actions {...props}>
|
||||||
<ClickAction label="Open" onTrigger={(event, file) => {
|
<ClickAction label={!isSelector ? "Open" : "Select"} onTrigger={(event, file) => {
|
||||||
|
if (isSelector) {
|
||||||
|
onSelectionChange?.({ files: [file.id], directory: currentDirectory });
|
||||||
|
onSelectionFinish?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
file.open(windowsManager);
|
file.open(windowsManager);
|
||||||
}}/>
|
}}/>
|
||||||
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
|
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
|
||||||
|
|
@ -56,7 +69,7 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
||||||
const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
|
const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
|
||||||
<Actions {...props}>
|
<Actions {...props}>
|
||||||
<ClickAction label="Open" onTrigger={(event, folder) => {
|
<ClickAction label="Open" onTrigger={(event, folder) => {
|
||||||
folder.open(windowsManager);
|
changeDirectory(folder.linkedPath ?? folder.name);
|
||||||
}}/>
|
}}/>
|
||||||
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder) => {
|
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder) => {
|
||||||
windowsManager.open(APPS.TERMINAL, { startPath: folder.path });
|
windowsManager.open(APPS.TERMINAL, { startPath: folder.path });
|
||||||
|
|
@ -138,7 +151,7 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
||||||
const itemCount = currentDirectory.getItemCount(showHidden);
|
const itemCount = currentDirectory.getItemCount(showHidden);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.Container}>
|
<div className={!isSelector ? styles.Container : `${styles.Container} ${styles.Selector}`}>
|
||||||
<div className={styles.Header}>
|
<div className={styles.Header}>
|
||||||
<button
|
<button
|
||||||
title="Back"
|
title="Back"
|
||||||
|
|
@ -221,28 +234,37 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
||||||
id="main"
|
id="main"
|
||||||
className={styles.Main}
|
className={styles.Main}
|
||||||
showHidden={showHidden}
|
showHidden={showHidden}
|
||||||
onClickFile={(event, file) => {
|
onOpenFile={(event, file) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (isSelector)
|
||||||
|
return onSelectionFinish?.();
|
||||||
const options = {};
|
const options = {};
|
||||||
if (file.extension === "md" || CODE_FORMATS.includes(file.extension))
|
if (file.extension === "md" || CODE_FORMATS.includes(file.extension))
|
||||||
options.mode = "view";
|
options.mode = "view";
|
||||||
windowsManager.openFile(file, options);
|
windowsManager.openFile(file, options);
|
||||||
}}
|
}}
|
||||||
onClickFolder={(event, folder) => {
|
onOpenFolder={(event, folder) => {
|
||||||
changeDirectory(folder.linkedPath ?? folder.name);
|
changeDirectory(folder.linkedPath ?? folder.name);
|
||||||
}}
|
}}
|
||||||
onContextMenuFile={onContextMenuFile}
|
onContextMenuFile={onContextMenuFile}
|
||||||
onContextMenuFolder={onContextMenuFolder}
|
onContextMenuFolder={onContextMenuFolder}
|
||||||
|
allowMultiSelect={selectorMode !== SELECTOR_MODE.SINGLE}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.Footer}>
|
{!isSelector
|
||||||
<p className={utilStyles["Text-light"]}>
|
? <span className={styles.Footer}>
|
||||||
{itemCount === 1
|
<p className={utilStyles["Text-light"]}>
|
||||||
? itemCount + " item"
|
{itemCount === 1
|
||||||
: itemCount + " items"
|
? itemCount + " item"
|
||||||
}
|
: itemCount + " items"
|
||||||
</p>
|
}
|
||||||
</span>
|
</p>
|
||||||
|
</span>
|
||||||
|
: <div className={`${styles.Footer} ${styles["Selector-footer"]}`}>
|
||||||
|
<Footer/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -153,4 +153,8 @@
|
||||||
.Footer p {
|
.Footer p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Selector {
|
||||||
|
--footer-height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +17,13 @@ import { ImagePreview } from "./ImagePreview.jsx";
|
||||||
* @param {VirtualFolder} folder
|
* @param {VirtualFolder} folder
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback onSelectionChange
|
||||||
|
* @param {object} selection
|
||||||
|
* @param {string[]} selection.files
|
||||||
|
* @param {string[]} selection.folders
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} props
|
* @param {object} props
|
||||||
* @param {VirtualFolder} props.directory
|
* @param {VirtualFolder} props.directory
|
||||||
|
|
@ -26,17 +33,23 @@ import { ImagePreview } from "./ImagePreview.jsx";
|
||||||
* @param {string} props.className
|
* @param {string} props.className
|
||||||
* @param {fileEvent} props.onContextMenuFile
|
* @param {fileEvent} props.onContextMenuFile
|
||||||
* @param {folderEvent} props.onContextMenuFolder
|
* @param {folderEvent} props.onContextMenuFolder
|
||||||
* @param {fileEvent} props.onClickFile
|
* @param {fileEvent} props.onOpenFile
|
||||||
* @param {folderEvent} props.onClickFolder
|
* @param {folderEvent} props.onOpenFolder
|
||||||
|
* @param {boolean} props.allowMultiSelect
|
||||||
|
* @param {onSelectionChange} props.onSelectionChange
|
||||||
*/
|
*/
|
||||||
export function DirectoryList({ directory, showHidden = false, folderClassName, fileClassName, className,
|
export function DirectoryList({ directory, showHidden = false, folderClassName, fileClassName, className,
|
||||||
onContextMenuFile, onContextMenuFolder, onClickFile, onClickFolder, ...props }) {
|
onContextMenuFile, onContextMenuFolder, onOpenFile, onOpenFolder, allowMultiSelect = true, onSelectionChange, ...props }) {
|
||||||
const [selectedFolders, setSelectedFolders] = useState([]);
|
const [selectedFolders, setSelectedFolders] = useState([]);
|
||||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [rectSelectStart, setRectSelectStart] = useState(null);
|
const [rectSelectStart, setRectSelectStart] = useState(null);
|
||||||
const [rectSelectEnd, setRectSelectEnd] = useState(null);
|
const [rectSelectEnd, setRectSelectEnd] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectionChange?.({ files: selectedFiles, folders: selectedFolders, directory });
|
||||||
|
}, [directory, onSelectionChange, selectedFiles, selectedFolders]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
@ -79,11 +92,15 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
|
||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
};
|
};
|
||||||
const selectFolder = (folder, exclusive = false) => {
|
const selectFolder = (folder, exclusive = false) => {
|
||||||
|
if (!allowMultiSelect)
|
||||||
|
exclusive = true;
|
||||||
setSelectedFolders(exclusive ? [folder.id] : [...selectedFolders, folder.id]);
|
setSelectedFolders(exclusive ? [folder.id] : [...selectedFolders, folder.id]);
|
||||||
if (exclusive)
|
if (exclusive)
|
||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
};
|
};
|
||||||
const selectFile = (file, exclusive = false) => {
|
const selectFile = (file, exclusive = false) => {
|
||||||
|
if (!allowMultiSelect)
|
||||||
|
exclusive = true;
|
||||||
setSelectedFiles(exclusive ? [file.id] : [...selectedFiles, file.id]);
|
setSelectedFiles(exclusive ? [file.id] : [...selectedFiles, file.id]);
|
||||||
if (exclusive)
|
if (exclusive)
|
||||||
setSelectedFolders([]);
|
setSelectedFolders([]);
|
||||||
|
|
@ -155,7 +172,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
|
||||||
selectFolder(folder, !event.ctrlKey);
|
selectFolder(folder, !event.ctrlKey);
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
onClickFolder?.(event, folder);
|
onOpenFolder?.(event, folder);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles["Folder-icon"]}>
|
<div className={styles["Folder-icon"]}>
|
||||||
|
|
@ -177,7 +194,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
|
||||||
selectFile(file, !event.ctrlKey);
|
selectFile(file, !event.ctrlKey);
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
onClickFile?.(event, file);
|
onOpenFile?.(event, file);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles["File-icon"]}>
|
<div className={styles["File-icon"]}>
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { SettingsManager } from "../../../features/settings/settingsManager.js";
|
|
||||||
import styles from "./Settings.module.css";
|
|
||||||
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
|
|
||||||
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext.js";
|
|
||||||
import { WALLPAPERS_PATH } from "../../../config/apps/settings.config.js";
|
|
||||||
|
|
||||||
export function AppearanceSettings() {
|
|
||||||
const virtualRoot = useVirtualRoot();
|
|
||||||
const settingsManager = useSettingsManager();
|
|
||||||
const [wallpaper, setWallpaper] = useState(null);
|
|
||||||
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
settings.get("wallpaper", setWallpaper);
|
|
||||||
}, [settings]);
|
|
||||||
|
|
||||||
const onChange = (event) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
settings.set("wallpaper", value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<div className={styles["Option"]}>
|
|
||||||
<p className={styles["Label"]}>Wallpaper</p>
|
|
||||||
<div className={styles["Input"]}>
|
|
||||||
{virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) =>
|
|
||||||
<label className={styles["Image-select"]} key={id}>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
value={source}
|
|
||||||
aria-label="Wallpaper image"
|
|
||||||
checked={source === wallpaper ? "checked" : ""}
|
|
||||||
onChange={onChange}
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
<img src={source} alt={id} draggable="false"/>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import styles from "./Settings.module.css";
|
import styles from "./Settings.module.css";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCircleInfo, faHardDrive, faPalette } from "@fortawesome/free-solid-svg-icons";
|
import { faCircleInfo, faHardDrive, faPalette, faShapes } from "@fortawesome/free-solid-svg-icons";
|
||||||
import utilStyles from "../../../styles/utils.module.css";
|
import utilStyles from "../../../styles/utils.module.css";
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
||||||
import { AppearanceSettings } from "./AppearanceSettings.jsx";
|
import { AppearanceSettings } from "./tabs/AppearanceSettings.jsx";
|
||||||
import { AboutSettings } from "./AboutSettings.jsx";
|
import { AboutSettings } from "./tabs/AboutSettings.jsx";
|
||||||
import { StorageTab } from "./StorageSettings.jsx";
|
import { StorageTab } from "./tabs/StorageSettings.jsx";
|
||||||
|
import { AppsSettings } from "./tabs/AppsSettings.jsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
||||||
*/
|
*/
|
||||||
export function Settings({ initialTabIndex }) {
|
export function Settings({ initialTabIndex, modalsManager }) {
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultIndex={initialTabIndex ?? 0}
|
defaultIndex={initialTabIndex ?? 0}
|
||||||
|
|
@ -19,6 +20,10 @@ export function Settings({ initialTabIndex }) {
|
||||||
selectedTabPanelClassName={styles["Active-panel"]}
|
selectedTabPanelClassName={styles["Active-panel"]}
|
||||||
>
|
>
|
||||||
<TabList className={styles.Tabs}>
|
<TabList className={styles.Tabs}>
|
||||||
|
<Tab className={styles["Tab-button"]} tabIndex="0">
|
||||||
|
<FontAwesomeIcon icon={faShapes}/>
|
||||||
|
<p className={utilStyles["Text-semibold"]}>Apps</p>
|
||||||
|
</Tab>
|
||||||
<Tab className={styles["Tab-button"]} tabIndex="0">
|
<Tab className={styles["Tab-button"]} tabIndex="0">
|
||||||
<FontAwesomeIcon icon={faPalette}/>
|
<FontAwesomeIcon icon={faPalette}/>
|
||||||
<p className={utilStyles["Text-semibold"]}>Appearance</p>
|
<p className={utilStyles["Text-semibold"]}>Appearance</p>
|
||||||
|
|
@ -33,7 +38,10 @@ export function Settings({ initialTabIndex }) {
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel className={styles["Tab-panel"]}>
|
<TabPanel className={styles["Tab-panel"]}>
|
||||||
<AppearanceSettings/>
|
<AppsSettings modalsManager={modalsManager}/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel className={styles["Tab-panel"]}>
|
||||||
|
<AppearanceSettings modalsManager={modalsManager}/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel className={styles["Tab-panel"]}>
|
<TabPanel className={styles["Tab-panel"]}>
|
||||||
<StorageTab/>
|
<StorageTab/>
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,15 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Option-list {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Option-horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.Option > .Label {
|
.Option > .Label {
|
||||||
color: var(--foreground-color-a);
|
color: var(--foreground-color-a);
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +154,52 @@
|
||||||
--hover-color: var(--red-b) !important;
|
--hover-color: var(--red-b) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Icon-button {
|
||||||
|
--color: var(--foreground-color-a);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
height: 1.25rem;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0%);
|
||||||
|
border-radius: 9999px;
|
||||||
|
transform: scale(100%);
|
||||||
|
transform-origin: center;
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button:hover::after, .Icon-button:focus-visible::after {
|
||||||
|
background-color: rgba(255, 255, 255, 10%);
|
||||||
|
transform: scale(150%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button:disabled {
|
||||||
|
--color: var(--foreground-color-c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button svg {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button svg path {
|
||||||
|
fill: var(--color);
|
||||||
|
transition: fill 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
.Progress-bar-container {
|
.Progress-bar-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 35rem;
|
max-width: 35rem;
|
||||||
|
|
@ -158,4 +213,27 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Option > span.Label {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon div {
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon div > svg {
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Button } from "../../_utils/button/Button.jsx";
|
import { Button } from "../../../_utils/button/Button.jsx";
|
||||||
import styles from "./Settings.module.css";
|
import styles from "../Settings.module.css";
|
||||||
import utilStyles from "../../../styles/utils.module.css";
|
import utilStyles from "../../../../styles/utils.module.css";
|
||||||
import Vector2 from "../../../features/math/vector2.js";
|
import Vector2 from "../../../../features/math/vector2.js";
|
||||||
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
|
import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext.js";
|
||||||
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
|
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js";
|
||||||
import { NAME } from "../../../config/branding.config.js";
|
import { NAME } from "../../../../config/branding.config.js";
|
||||||
|
|
||||||
export function AboutSettings() {
|
export function AboutSettings() {
|
||||||
const windowsManager = useWindowsManager();
|
const windowsManager = useWindowsManager();
|
||||||
75
src/components/apps/settings/tabs/AppOption.jsx
Normal file
75
src/components/apps/settings/tabs/AppOption.jsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import AppsManager from "../../../../features/apps/appsManager.js";
|
||||||
|
import { ImagePreview } from "../../file-explorer/directory-list/ImagePreview.jsx";
|
||||||
|
import styles from "../Settings.module.css";
|
||||||
|
import { faEllipsisVertical, faThumbTack } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext.js";
|
||||||
|
import { useContextMenu } from "../../../../hooks/modals/contextMenu.js";
|
||||||
|
import { Actions } from "../../../actions/Actions.jsx";
|
||||||
|
import { ClickAction } from "../../../actions/actions/ClickAction.jsx";
|
||||||
|
import { removeFromArray } from "../../../../features/_utils/array.utils.js";
|
||||||
|
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext.js";
|
||||||
|
import { SettingsManager } from "../../../../features/settings/settingsManager.js";
|
||||||
|
|
||||||
|
export function AppOption({ app, pins, setPins, modalsManager }) {
|
||||||
|
const isPinned = pins.includes(app.id);
|
||||||
|
|
||||||
|
const settingsManager = useSettingsManager();
|
||||||
|
const windowsManager = useWindowsManager();
|
||||||
|
|
||||||
|
const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) =>
|
||||||
|
<Actions {...props}>
|
||||||
|
<ClickAction label="Launch" icon={AppsManager.getAppIconUrl(app.id)} onTrigger={() => windowsManager.open(app.id)}/>
|
||||||
|
<ClickAction label={isPinned ? "Unpin from taskbar" : "Pin to taskbar"} icon={faThumbTack} onTrigger={() => {
|
||||||
|
const newPins = [...pins];
|
||||||
|
if (isPinned) {
|
||||||
|
removeFromArray(app.id, pins);
|
||||||
|
} else {
|
||||||
|
newPins.push(app.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
|
||||||
|
settings.set("pins", newPins.join(","));
|
||||||
|
}}/>
|
||||||
|
</Actions>
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={`${styles["Option"]} ${styles["Option-horizontal"]}`}>
|
||||||
|
<span className={styles["Label"]}>
|
||||||
|
<ImagePreview className={styles["Icon"]} source={AppsManager.getAppIconUrl(app.id)}/>
|
||||||
|
{app.name}
|
||||||
|
</span>
|
||||||
|
<button className={styles["Icon-button"]} onClick={onContextMenu}>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical}/>
|
||||||
|
</button>
|
||||||
|
{/* <div className={styles["Button-group"]}>
|
||||||
|
<Button
|
||||||
|
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
|
||||||
|
onClick={() => windowsManager.open(id)}
|
||||||
|
>
|
||||||
|
Launch
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`${styles.Button} ${styles["Button-red"]} ${utilStyles["Text-bold"]}`}
|
||||||
|
>
|
||||||
|
Uninstall
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
|
||||||
|
onClick={() => {
|
||||||
|
const newPins = [...pins];
|
||||||
|
if (isPinned) {
|
||||||
|
removeFromArray(id, pins);
|
||||||
|
} else {
|
||||||
|
newPins.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
|
||||||
|
settings.set("pins", newPins.join(","));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPinned ? "Unpin from taskbar" : "Pin to taskbar"}
|
||||||
|
</Button>
|
||||||
|
</div> */}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
74
src/components/apps/settings/tabs/AppearanceSettings.jsx
Normal file
74
src/components/apps/settings/tabs/AppearanceSettings.jsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { SettingsManager } from "../../../../features/settings/settingsManager.js";
|
||||||
|
import styles from "../Settings.module.css";
|
||||||
|
import utilStyles from "../../../../styles/utils.module.css";
|
||||||
|
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js";
|
||||||
|
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext.js";
|
||||||
|
import { WALLPAPERS_PATH } from "../../../../config/apps/settings.config.js";
|
||||||
|
import { useWindowedModal } from "../../../../hooks/modals/windowedModal.js";
|
||||||
|
import ModalsManager from "../../../../features/modals/modalsManager.js";
|
||||||
|
import { Button } from "../../../_utils/button/Button.jsx";
|
||||||
|
import { FileSelector } from "../../../modals/file-selector/FileSelector.jsx";
|
||||||
|
import { SELECTOR_MODE } from "../../../../config/apps/fileExplorer.config.js";
|
||||||
|
import { DEFAULT_FILE_SELECTOR_SIZE } from "../../../../config/modals.config.js";
|
||||||
|
import { IMAGE_FORMATS } from "../../../../config/apps/mediaViewer.config.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {ModalsManager} props.modalsManager
|
||||||
|
*/
|
||||||
|
export function AppearanceSettings({ modalsManager }) {
|
||||||
|
const virtualRoot = useVirtualRoot();
|
||||||
|
const settingsManager = useSettingsManager();
|
||||||
|
const [wallpaper, setWallpaper] = useState(null);
|
||||||
|
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
|
||||||
|
const { openWindowedModal } = useWindowedModal({ modalsManager });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
settings.get("wallpaper", setWallpaper);
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
const onChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
settings.set("wallpaper", value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className={styles["Option"]}>
|
||||||
|
<p className={styles["Label"]}>Wallpaper</p>
|
||||||
|
<Button
|
||||||
|
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
|
||||||
|
onClick={() => {
|
||||||
|
openWindowedModal({
|
||||||
|
size: DEFAULT_FILE_SELECTOR_SIZE,
|
||||||
|
Modal: (props) => <FileSelector
|
||||||
|
type={SELECTOR_MODE.SINGLE}
|
||||||
|
allowedFormats={IMAGE_FORMATS}
|
||||||
|
onFinish={(file) => {
|
||||||
|
settings.set("wallpaper", file.source);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
|
<div className={styles["Input"]}>
|
||||||
|
{virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) =>
|
||||||
|
<label className={styles["Image-select"]} key={id}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={source}
|
||||||
|
aria-label="Wallpaper image"
|
||||||
|
checked={source === wallpaper ? "checked" : ""}
|
||||||
|
onChange={onChange}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
<img src={source} alt={id} draggable="false"/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
30
src/components/apps/settings/tabs/AppsSettings.jsx
Normal file
30
src/components/apps/settings/tabs/AppsSettings.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styles from "../Settings.module.css";
|
||||||
|
import AppsManager from "../../../../features/apps/appsManager.js";
|
||||||
|
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext.js";
|
||||||
|
import { SettingsManager } from "../../../../features/settings/settingsManager.js";
|
||||||
|
import ModalsManager from "../../../../features/modals/modalsManager.js";
|
||||||
|
import { AppOption } from "./AppOption.jsx";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {ModalsManager} props.modalsManager
|
||||||
|
*/
|
||||||
|
export function AppsSettings({ modalsManager }) {
|
||||||
|
const settingsManager = useSettingsManager();
|
||||||
|
const [pins, setPins] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
|
||||||
|
settings.get("pins", (pins) => {
|
||||||
|
setPins(pins.split(","));
|
||||||
|
});
|
||||||
|
}, [settingsManager]);
|
||||||
|
|
||||||
|
return <div className={`${styles["Option"]} ${styles["Option-list"]}`}>
|
||||||
|
<p className={styles["Label"]}>Apps</p>
|
||||||
|
{AppsManager.APPS.map((app) =>
|
||||||
|
<AppOption key={app.id} app={app} modalsManager={modalsManager} pins={pins} setPins={setPins}/>
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import styles from "./Settings.module.css";
|
import styles from "../Settings.module.css";
|
||||||
import utilStyles from "../../../styles/utils.module.css";
|
import utilStyles from "../../../../styles/utils.module.css";
|
||||||
import { round } from "../../../features/math/round.js";
|
import { round } from "../../../../features/math/round.js";
|
||||||
import { ProgressBar } from "../../_utils/progress-bar/ProgressBar.jsx";
|
import { ProgressBar } from "../../../_utils/progress-bar/ProgressBar.jsx";
|
||||||
import { Button } from "../../_utils/button/Button.jsx";
|
import { Button } from "../../../_utils/button/Button.jsx";
|
||||||
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
|
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js";
|
||||||
import { StorageManager } from "../../../features/storage/storageManager.js";
|
import { StorageManager } from "../../../../features/storage/storageManager.js";
|
||||||
|
|
||||||
export function StorageTab() {
|
export function StorageTab() {
|
||||||
const virtualRoot = useVirtualRoot();
|
const virtualRoot = useVirtualRoot();
|
||||||
|
|
@ -9,6 +9,10 @@ import { MarkdownLink } from "./overrides/MarkdownLink.jsx";
|
||||||
import { MarkdownImage } from "./overrides/MarkdownImage.jsx";
|
import { MarkdownImage } from "./overrides/MarkdownImage.jsx";
|
||||||
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
|
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import { useWindowedModal } from "../../../hooks/modals/windowedModal.js";
|
||||||
|
import { DEFAULT_FILE_SELECTOR_SIZE } from "../../../config/modals.config.js";
|
||||||
|
import { FileSelector } from "../../modals/file-selector/FileSelector.jsx";
|
||||||
|
import { SELECTOR_MODE } from "../../../config/apps/fileExplorer.config.js";
|
||||||
|
|
||||||
const OVERRIDES = {
|
const OVERRIDES = {
|
||||||
a: MarkdownLink,
|
a: MarkdownLink,
|
||||||
|
|
@ -26,6 +30,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
|
||||||
const [content, setContent] = useState(file?.content ?? "");
|
const [content, setContent] = useState(file?.content ?? "");
|
||||||
const [unsavedChanges, setUnsavedChanges] = useState(file == null);
|
const [unsavedChanges, setUnsavedChanges] = useState(file == null);
|
||||||
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
|
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
|
||||||
|
const { openWindowedModal } = useWindowedModal({ modalsManager });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -115,12 +120,27 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(currentFile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.Container} style={{ fontSize: zoom }}>
|
<div className={styles.Container} style={{ fontSize: zoom }}>
|
||||||
<HeaderMenu
|
<HeaderMenu
|
||||||
options={{
|
options={{
|
||||||
"File": {
|
"File": {
|
||||||
"New": newText,
|
"New": newText,
|
||||||
|
"Open": () => {
|
||||||
|
openWindowedModal({
|
||||||
|
size: DEFAULT_FILE_SELECTOR_SIZE,
|
||||||
|
Modal: (props) => <FileSelector
|
||||||
|
type={SELECTOR_MODE.SINGLE}
|
||||||
|
onFinish={(file) => {
|
||||||
|
setCurrentFile(file);
|
||||||
|
setUnsavedChanges(false);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
});
|
||||||
|
},
|
||||||
"Save": saveText,
|
"Save": saveText,
|
||||||
// "Save As": saveTextAs,
|
// "Save As": saveTextAs,
|
||||||
"Quit": () => {
|
"Quit": () => {
|
||||||
|
|
@ -145,6 +165,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
|
||||||
shortcuts={{
|
shortcuts={{
|
||||||
"File": {
|
"File": {
|
||||||
"New": ["Control", "e"],
|
"New": ["Control", "e"],
|
||||||
|
"Open": ["Control", "o"],
|
||||||
"Save": ["Control", "s"],
|
"Save": ["Control", "s"],
|
||||||
"Quit": ["Control", "q"],
|
"Quit": ["Control", "q"],
|
||||||
},
|
},
|
||||||
|
|
@ -156,17 +177,17 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{currentMode === "view"
|
{currentMode === "view"
|
||||||
? CODE_FORMATS.includes(file?.extension)
|
? CODE_FORMATS.includes(currentFile?.extension)
|
||||||
? <SyntaxHighlighter
|
? <SyntaxHighlighter
|
||||||
language={EXTENSION_TO_LANGUAGE[file?.extension] ?? file?.extension}
|
language={EXTENSION_TO_LANGUAGE[currentFile?.extension] ?? currentFile?.extension}
|
||||||
className={styles.Code}
|
className={styles.Code}
|
||||||
useInlineStyles={false}
|
useInlineStyles={false}
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
>{content}</SyntaxHighlighter>
|
>{content}</SyntaxHighlighter>
|
||||||
: <div ref={ref} className={styles.View}>
|
: <div ref={ref} className={styles.View}>
|
||||||
{file?.extension === "md"
|
{currentFile?.extension === "md"
|
||||||
? <Markdown options={{ overrides }}>{content}</Markdown>
|
? <Markdown options={{ overrides }}>{content}</Markdown>
|
||||||
: <p>{content}</p>
|
: <pre><p>{content}</p></pre>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
: <textarea
|
: <textarea
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function MarkdownLink({ href, children, modalsManager, windowsManager, cu
|
||||||
|
|
||||||
if (!href.startsWith("http://") && !href.startsWith("https://")) {
|
if (!href.startsWith("http://") && !href.startsWith("https://")) {
|
||||||
const target = currentFile.parent.navigate(href);
|
const target = currentFile.parent.navigate(href);
|
||||||
if (target) {
|
if (target != null) {
|
||||||
if (target.isFile()) {
|
if (target.isFile()) {
|
||||||
setCurrentFile(target);
|
setCurrentFile(target);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export const Desktop = memo(() => {
|
||||||
}}
|
}}
|
||||||
fileClassName={styles["Item"]}
|
fileClassName={styles["Item"]}
|
||||||
folderClassName={styles["Item"]}
|
folderClassName={styles["Item"]}
|
||||||
onClickFile={(event, file) => {
|
onOpenFile={(event, file) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const options = {};
|
const options = {};
|
||||||
|
|
@ -147,7 +147,7 @@ export const Desktop = memo(() => {
|
||||||
|
|
||||||
windowsManager.openFile(file, options);
|
windowsManager.openFile(file, options);
|
||||||
}}
|
}}
|
||||||
onClickFolder={(event, { linkedPath, path }) => {
|
onOpenFolder={(event, { linkedPath, path }) => {
|
||||||
windowsManager.open(APPS.FILE_EXPLORER, { startPath: linkedPath ?? path });
|
windowsManager.open(APPS.FILE_EXPLORER, { startPath: linkedPath ?? path });
|
||||||
}}
|
}}
|
||||||
onContextMenuFile={onContextMenuFile}
|
onContextMenuFile={onContextMenuFile}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Draggable from "react-draggable";
|
import Draggable from "react-draggable";
|
||||||
import { ReactSVG } from "react-svg";
|
import { ReactSVG } from "react-svg";
|
||||||
import utilStyles from "../../../styles/utils.module.css";
|
import utilStyles from "../../../styles/utils.module.css";
|
||||||
|
import Modal from "../../../features/modals/modal.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {Modal} props.modal
|
||||||
|
* @param {*} props.params
|
||||||
|
* @param {*} props.children
|
||||||
|
*/
|
||||||
export function WindowedModal({ modal, params, children, ...props }) {
|
export function WindowedModal({ modal, params, children, ...props }) {
|
||||||
const { iconUrl, title } = params;
|
const { iconUrl, title } = params;
|
||||||
|
|
||||||
|
|
@ -34,7 +41,7 @@ export function WindowedModal({ modal, params, children, ...props }) {
|
||||||
}
|
}
|
||||||
}, [modal, screenHeight, screenWidth]);
|
}, [modal, screenHeight, screenWidth]);
|
||||||
|
|
||||||
return (<Draggable
|
return <Draggable
|
||||||
axis="both"
|
axis="both"
|
||||||
handle={".Window-handle"}
|
handle={".Window-handle"}
|
||||||
defaultPosition={startPosition}
|
defaultPosition={startPosition}
|
||||||
|
|
@ -72,5 +79,5 @@ export function WindowedModal({ modal, params, children, ...props }) {
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>);
|
</Draggable>;
|
||||||
}
|
}
|
||||||
|
|
@ -89,22 +89,4 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.Window-content button {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
width: fit-content;
|
|
||||||
color: var(--foreground-color-a);
|
|
||||||
background-color: var(--background-color-a);
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
outline: none;
|
|
||||||
transition: background-color 100ms ease-in-out;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Window-content button:hover,
|
|
||||||
.Window-content button:focus-visible {
|
|
||||||
background-color: var(--background-color-b);
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DIALOG_CONTENT_TYPES } from "../../../config/modals.config.js";
|
import { DIALOG_CONTENT_TYPES } from "../../../config/modals.config.js";
|
||||||
import { WindowedModal } from "../_templates/WindowedModal.jsx";
|
import { WindowedModal } from "../_utils/WindowedModal.jsx";
|
||||||
|
|
||||||
export function DialogBox({ modal, params, children, ...props }) {
|
export function DialogBox({ modal, params, children, ...props }) {
|
||||||
const onClick = (event) => {
|
const onClick = (event) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ImagePreview } from "../../apps/file-explorer/directory-list/ImagePreview.jsx";
|
import { ImagePreview } from "../../apps/file-explorer/directory-list/ImagePreview.jsx";
|
||||||
import { WindowedModal } from "../_templates/WindowedModal.jsx";
|
import { WindowedModal } from "../_utils/WindowedModal.jsx";
|
||||||
import styles from "./FileProperties.module.css";
|
import styles from "./FileProperties.module.css";
|
||||||
import utilStyles from "../../../styles/utils.module.css";
|
import utilStyles from "../../../styles/utils.module.css";
|
||||||
import Modal from "../../../features/modals/modal.js";
|
import Modal from "../../../features/modals/modal.js";
|
||||||
|
|
|
||||||
89
src/components/modals/file-selector/FileSelector.jsx
Normal file
89
src/components/modals/file-selector/FileSelector.jsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { APP_ICONS } from "../../../config/apps.config.js";
|
||||||
|
import { SELECTOR_MODE } from "../../../config/apps/fileExplorer.config.js";
|
||||||
|
import { useModals } from "../../../hooks/modals/modals.js";
|
||||||
|
import { Button } from "../../_utils/button/Button.jsx";
|
||||||
|
import { FileExplorer } from "../../apps/file-explorer/FileExplorer.jsx";
|
||||||
|
import { ModalsView } from "../ModalsView.jsx";
|
||||||
|
import { WindowedModal } from "../_utils/WindowedModal.jsx";
|
||||||
|
import styles from "./FileSelector.module.css";
|
||||||
|
import { VirtualFile } from "../../../features/virtual-drive/file/virtualFile.js";
|
||||||
|
import Modal from "../../../features/modals/modal.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback onFinish
|
||||||
|
* @param {VirtualFile|VirtualFile[]} result
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {Modal} props.modal
|
||||||
|
* @param {*} props.params
|
||||||
|
* @param {number} props.type
|
||||||
|
* @param {string[]=} props.allowedFormats
|
||||||
|
* @param {onFinish} props.onFinish
|
||||||
|
*/
|
||||||
|
export function FileSelector({ modal, params, type, allowedFormats, onFinish, ...props }) {
|
||||||
|
const multi = (type === SELECTOR_MODE.MULTIPLE);
|
||||||
|
|
||||||
|
const [modalsManager, modals] = useModals();
|
||||||
|
const [selection, setSelection] = useState(multi ? [] : null);
|
||||||
|
const [directory, setDirectory] = useState(null);
|
||||||
|
|
||||||
|
params.title = multi ? "Select files" : "Select a file";
|
||||||
|
params.iconUrl = APP_ICONS.FILE_EXPLORER;
|
||||||
|
|
||||||
|
const finish = (event) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
|
||||||
|
if (directory == null || selection == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const files = selection.map((id) => {
|
||||||
|
const { name, extension } = VirtualFile.convertId(id);
|
||||||
|
return directory.findFile(name, extension);
|
||||||
|
}).filter((file) => {
|
||||||
|
if (file == null)
|
||||||
|
return false;
|
||||||
|
const validFormat = (allowedFormats == null || allowedFormats.includes(file.extension));
|
||||||
|
return validFormat;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
modal.close();
|
||||||
|
onFinish?.(multi ? files : files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <WindowedModal modal={modal} params={params} {...props}>
|
||||||
|
<ModalsView modalsManager={modalsManager} modals={modals}/>
|
||||||
|
<FileExplorer
|
||||||
|
modalsManager={modalsManager}
|
||||||
|
selectorMode={type}
|
||||||
|
Footer={() =>
|
||||||
|
<div className={styles.Footer}>
|
||||||
|
<span className={styles.Selection}>
|
||||||
|
{multi
|
||||||
|
? <p>Selected file(s): {selection.join(", ")}</p>
|
||||||
|
: <p>Selected file: {selection ?? ""}</p>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<div className={styles.Buttons}>
|
||||||
|
<Button className={styles.Button} onClick={finish}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
<Button className={styles.Button} onClick={() => { modal.close(); }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onSelectionChange={({ files, directory }) => {
|
||||||
|
setSelection(files);
|
||||||
|
setDirectory(directory);
|
||||||
|
}}
|
||||||
|
onSelectionFinish={finish}
|
||||||
|
/>
|
||||||
|
</WindowedModal>;
|
||||||
|
}
|
||||||
25
src/components/modals/file-selector/FileSelector.module.css
Normal file
25
src/components/modals/file-selector/FileSelector.module.css
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
.Footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Selection > p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button {
|
||||||
|
--normal-color: var(--background-color-a);
|
||||||
|
--hover-color: var(--background-color-b);
|
||||||
|
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
@ -77,12 +77,10 @@ export const Taskbar = memo(() => {
|
||||||
}), [modalsManager, pins, windows, windowsManager]);
|
}), [modalsManager, pins, windows, windowsManager]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
|
||||||
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
|
settings.get("pins", (pins) => {
|
||||||
settings.get("pins", (pins) => {
|
setPins(pins.split(","));
|
||||||
setPins(pins.split(","));
|
});
|
||||||
});
|
|
||||||
})();
|
|
||||||
}, [settingsManager]);
|
}, [settingsManager]);
|
||||||
|
|
||||||
const updateShowHome = (value) => {
|
const updateShowHome = (value) => {
|
||||||
|
|
|
||||||
5
src/config/apps/fileExplorer.config.js
Normal file
5
src/config/apps/fileExplorer.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const SELECTOR_MODE = {
|
||||||
|
NONE: 0,
|
||||||
|
SINGLE: 1,
|
||||||
|
MULTIPLE: 2
|
||||||
|
};
|
||||||
|
|
@ -4,4 +4,5 @@ export const DIALOG_CONTENT_TYPES = {
|
||||||
CloseButton: 0
|
CloseButton: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200);
|
export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200);
|
||||||
|
export const DEFAULT_FILE_SELECTOR_SIZE = new Vector2(700, 400);
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js";
|
||||||
import Command from "../command.js";
|
import Command from "../command.js";
|
||||||
|
|
||||||
export const cat = new Command("cat")
|
export const cat = new Command("cat")
|
||||||
|
|
@ -8,7 +9,7 @@ export const cat = new Command("cat")
|
||||||
description: "Concetenate FILE(s) to standard output."
|
description: "Concetenate FILE(s) to standard output."
|
||||||
})
|
})
|
||||||
.setExecute(function(args, { currentDirectory, options }) {
|
.setExecute(function(args, { currentDirectory, options }) {
|
||||||
const [name, extension] = args[0].split(".");
|
const { name, extension } = VirtualFile.convertId(args[0]);
|
||||||
const file = currentDirectory.findFile(name, extension);
|
const file = currentDirectory.findFile(name, extension);
|
||||||
|
|
||||||
if (!file)
|
if (!file)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js";
|
||||||
import Command from "../command.js";
|
import Command from "../command.js";
|
||||||
|
|
||||||
export const rm = new Command("rm")
|
export const rm = new Command("rm")
|
||||||
|
|
@ -6,7 +7,7 @@ export const rm = new Command("rm")
|
||||||
purpose: "Remove a file"
|
purpose: "Remove a file"
|
||||||
})
|
})
|
||||||
.setExecute((args, { currentDirectory }) => {
|
.setExecute((args, { currentDirectory }) => {
|
||||||
const [name, extension] = args[0].split(".");
|
const { name, extension } = VirtualFile.convertId(args[0]);
|
||||||
const file = currentDirectory.findFile(name, extension);
|
const file = currentDirectory.findFile(name, extension);
|
||||||
|
|
||||||
if (!file)
|
if (!file)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js";
|
||||||
import Command from "../command.js";
|
import Command from "../command.js";
|
||||||
|
|
||||||
export const touch = new Command("touch")
|
export const touch = new Command("touch")
|
||||||
|
|
@ -12,7 +13,7 @@ export const touch = new Command("touch")
|
||||||
if (args[0] === "girls\\" && args[1] === "boo**")
|
if (args[0] === "girls\\" && args[1] === "boo**")
|
||||||
return `${this.name}: Cannot touch 'girls boo**': Permission denied`;
|
return `${this.name}: Cannot touch 'girls boo**': Permission denied`;
|
||||||
|
|
||||||
const [name, extension] = args[0].split(".");
|
const { name, extension } = VirtualFile.convertId(args[0]);
|
||||||
|
|
||||||
if (currentDirectory.findFile(name, extension))
|
if (currentDirectory.findFile(name, extension))
|
||||||
return { blank: true };
|
return { blank: true };
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,20 @@ export class VirtualFile extends VirtualBase {
|
||||||
return `${this.name}.${this.extension}`;
|
return `${this.name}.${this.extension}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {{
|
||||||
|
* name: string,
|
||||||
|
* extension: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
static convertId(id) {
|
||||||
|
const sections = id.split(".");
|
||||||
|
const extension = sections.pop();
|
||||||
|
const name = sections.join(".");
|
||||||
|
return { name, extension };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens this file in an app associated with its extension
|
* Opens this file in an app associated with its extension
|
||||||
* @param {WindowsManager} windowsManager
|
* @param {WindowsManager} windowsManager
|
||||||
|
|
|
||||||
|
|
@ -334,8 +334,7 @@ export class VirtualFolder extends VirtualBase {
|
||||||
if (folder != null)
|
if (folder != null)
|
||||||
return folder;
|
return folder;
|
||||||
|
|
||||||
// To do: add support for file names with dots
|
const { name, extension } = VirtualFile.convertId(lastSegment);
|
||||||
const [name, extension] = lastSegment.split(".");
|
|
||||||
return currentDirectory.findFile(name, extension);
|
return currentDirectory.findFile(name, extension);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@ export function useWindowedModal({ modalsManager }) {
|
||||||
positionY -= containerRect.y / 2;
|
positionY -= containerRect.y / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(WindowedModal);
|
|
||||||
|
|
||||||
const newModal = new Modal(WindowedModal)
|
const newModal = new Modal(WindowedModal)
|
||||||
.setPosition(new Vector2(positionX, positionY))
|
.setPosition(new Vector2(positionX, positionY))
|
||||||
.setSize(size)
|
.setSize(size)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue