diff --git a/docs/features/apps/terminal/README.md b/docs/features/apps/terminal/README.md index 8d9c675..1ac2a58 100644 --- a/docs/features/apps/terminal/README.md +++ b/docs/features/apps/terminal/README.md @@ -28,10 +28,7 @@ export const touch = new Command("touch") + "A FILE argument that does not exist is created empty." }) .setExecute(function(args, { currentDirectory }) { - if (args[0] === "girls\\" && args[1] === "boo**") - 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)) return { blank: true }; diff --git a/src/components/apps/file-explorer/FileExplorer.jsx b/src/components/apps/file-explorer/FileExplorer.jsx index 8af3d58..0c75af7 100644 --- a/src/components/apps/file-explorer/FileExplorer.jsx +++ b/src/components/apps/file-explorer/FileExplorer.jsx @@ -21,11 +21,19 @@ import { FileProperties } from "../../modals/file-properties/FileProperties.jsx" import { useHistory } from "../../../hooks/_utils/history.js"; import { Divider } from "../../actions/actions/Divider.jsx"; 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 {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 [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~")); const [path, setPath] = useState(currentDirectory?.path ?? ""); @@ -36,7 +44,12 @@ export function FileExplorer({ startPath, app, modalsManager }) { const { openWindowedModal } = useWindowedModal({ modalsManager }); const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) => - { + { + if (isSelector) { + onSelectionChange?.({ files: [file.id], directory: currentDirectory }); + onSelectionFinish?.(); + return; + } file.open(windowsManager); }}/> { @@ -56,7 +69,7 @@ export function FileExplorer({ startPath, app, modalsManager }) { const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) => { - folder.open(windowsManager); + changeDirectory(folder.linkedPath ?? folder.name); }}/> { windowsManager.open(APPS.TERMINAL, { startPath: folder.path }); @@ -138,7 +151,7 @@ export function FileExplorer({ startPath, app, modalsManager }) { const itemCount = currentDirectory.getItemCount(showHidden); return ( -
+
- -

- {itemCount === 1 - ? itemCount + " item" - : itemCount + " items" - } -

-
+ {!isSelector + ? +

+ {itemCount === 1 + ? itemCount + " item" + : itemCount + " items" + } +

+
+ :
+
+
+ }
); } \ No newline at end of file diff --git a/src/components/apps/file-explorer/FileExplorer.module.css b/src/components/apps/file-explorer/FileExplorer.module.css index 35de11d..5141a45 100644 --- a/src/components/apps/file-explorer/FileExplorer.module.css +++ b/src/components/apps/file-explorer/FileExplorer.module.css @@ -153,4 +153,8 @@ .Footer p { margin: 0; font-size: 0.875em; +} + +.Selector { + --footer-height: 4rem; } \ No newline at end of file diff --git a/src/components/apps/file-explorer/directory-list/DirectoryList.jsx b/src/components/apps/file-explorer/directory-list/DirectoryList.jsx index 9cc6d6d..7e1bf8e 100644 --- a/src/components/apps/file-explorer/directory-list/DirectoryList.jsx +++ b/src/components/apps/file-explorer/directory-list/DirectoryList.jsx @@ -17,6 +17,13 @@ import { ImagePreview } from "./ImagePreview.jsx"; * @param {VirtualFolder} folder */ +/** + * @callback onSelectionChange + * @param {object} selection + * @param {string[]} selection.files + * @param {string[]} selection.folders + */ + /** * @param {object} props * @param {VirtualFolder} props.directory @@ -26,17 +33,23 @@ import { ImagePreview } from "./ImagePreview.jsx"; * @param {string} props.className * @param {fileEvent} props.onContextMenuFile * @param {folderEvent} props.onContextMenuFolder - * @param {fileEvent} props.onClickFile - * @param {folderEvent} props.onClickFolder + * @param {fileEvent} props.onOpenFile + * @param {folderEvent} props.onOpenFolder + * @param {boolean} props.allowMultiSelect + * @param {onSelectionChange} props.onSelectionChange */ 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 [selectedFiles, setSelectedFiles] = useState([]); const ref = useRef(null); const [rectSelectStart, setRectSelectStart] = useState(null); const [rectSelectEnd, setRectSelectEnd] = useState(null); + + useEffect(() => { + onSelectionChange?.({ files: selectedFiles, folders: selectedFolders, directory }); + }, [directory, onSelectionChange, selectedFiles, selectedFolders]); useEffect(() => { clearSelection(); @@ -79,11 +92,15 @@ export function DirectoryList({ directory, showHidden = false, folderClassName, setSelectedFiles([]); }; const selectFolder = (folder, exclusive = false) => { + if (!allowMultiSelect) + exclusive = true; setSelectedFolders(exclusive ? [folder.id] : [...selectedFolders, folder.id]); if (exclusive) setSelectedFiles([]); }; const selectFile = (file, exclusive = false) => { + if (!allowMultiSelect) + exclusive = true; setSelectedFiles(exclusive ? [file.id] : [...selectedFiles, file.id]); if (exclusive) setSelectedFolders([]); @@ -155,7 +172,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName, selectFolder(folder, !event.ctrlKey); }} onDoubleClick={(event) => { - onClickFolder?.(event, folder); + onOpenFolder?.(event, folder); }} >
@@ -177,7 +194,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName, selectFile(file, !event.ctrlKey); }} onDoubleClick={(event) => { - onClickFile?.(event, file); + onOpenFile?.(event, file); }} >
diff --git a/src/components/apps/settings/AppearanceSettings.jsx b/src/components/apps/settings/AppearanceSettings.jsx deleted file mode 100644 index a2d7a87..0000000 --- a/src/components/apps/settings/AppearanceSettings.jsx +++ /dev/null @@ -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 (<> -
-

Wallpaper

-
- {virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) => - - )} -
-
- ); -} \ No newline at end of file diff --git a/src/components/apps/settings/Settings.jsx b/src/components/apps/settings/Settings.jsx index 688e724..21d2d78 100644 --- a/src/components/apps/settings/Settings.jsx +++ b/src/components/apps/settings/Settings.jsx @@ -1,16 +1,17 @@ import styles from "./Settings.module.css"; 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 { Tab, TabList, TabPanel, Tabs } from "react-tabs"; -import { AppearanceSettings } from "./AppearanceSettings.jsx"; -import { AboutSettings } from "./AboutSettings.jsx"; -import { StorageTab } from "./StorageSettings.jsx"; +import { AppearanceSettings } from "./tabs/AppearanceSettings.jsx"; +import { AboutSettings } from "./tabs/AboutSettings.jsx"; +import { StorageTab } from "./tabs/StorageSettings.jsx"; +import { AppsSettings } from "./tabs/AppsSettings.jsx"; /** * @param {import("../../windows/WindowView.jsx").windowProps} props */ -export function Settings({ initialTabIndex }) { +export function Settings({ initialTabIndex, modalsManager }) { return ( + + +

Apps

+

Appearance

@@ -33,7 +38,10 @@ export function Settings({ initialTabIndex }) {
- + + + + diff --git a/src/components/apps/settings/Settings.module.css b/src/components/apps/settings/Settings.module.css index 9c56374..5365d31 100644 --- a/src/components/apps/settings/Settings.module.css +++ b/src/components/apps/settings/Settings.module.css @@ -75,6 +75,15 @@ width: 100%; } +.Option-list { + gap: 0.5rem; +} + +.Option-horizontal { + flex-direction: row; + justify-content: space-between; +} + .Option > .Label { color: var(--foreground-color-a); } @@ -145,6 +154,52 @@ --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 { width: 100%; max-width: 35rem; @@ -158,4 +213,27 @@ display: flex; justify-content: space-between; 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; } \ No newline at end of file diff --git a/src/components/apps/settings/AboutSettings.jsx b/src/components/apps/settings/tabs/AboutSettings.jsx similarity index 67% rename from src/components/apps/settings/AboutSettings.jsx rename to src/components/apps/settings/tabs/AboutSettings.jsx index 670fd9d..de46a7f 100644 --- a/src/components/apps/settings/AboutSettings.jsx +++ b/src/components/apps/settings/tabs/AboutSettings.jsx @@ -1,10 +1,10 @@ -import { Button } from "../../_utils/button/Button.jsx"; -import styles from "./Settings.module.css"; -import utilStyles from "../../../styles/utils.module.css"; -import Vector2 from "../../../features/math/vector2.js"; -import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; -import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js"; -import { NAME } from "../../../config/branding.config.js"; +import { Button } from "../../../_utils/button/Button.jsx"; +import styles from "../Settings.module.css"; +import utilStyles from "../../../../styles/utils.module.css"; +import Vector2 from "../../../../features/math/vector2.js"; +import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext.js"; +import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js"; +import { NAME } from "../../../../config/branding.config.js"; export function AboutSettings() { const windowsManager = useWindowsManager(); diff --git a/src/components/apps/settings/tabs/AppOption.jsx b/src/components/apps/settings/tabs/AppOption.jsx new file mode 100644 index 0000000..2cef643 --- /dev/null +++ b/src/components/apps/settings/tabs/AppOption.jsx @@ -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) => + + windowsManager.open(app.id)}/> + { + 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(",")); + }}/> + + }); + + return
+ + + {app.name} + + + {/*
+ + + +
*/} +
; +} \ No newline at end of file diff --git a/src/components/apps/settings/tabs/AppearanceSettings.jsx b/src/components/apps/settings/tabs/AppearanceSettings.jsx new file mode 100644 index 0000000..569adc8 --- /dev/null +++ b/src/components/apps/settings/tabs/AppearanceSettings.jsx @@ -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 (<> +
+

Wallpaper

+ +
+ {virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) => + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/apps/settings/tabs/AppsSettings.jsx b/src/components/apps/settings/tabs/AppsSettings.jsx new file mode 100644 index 0000000..f31f5e7 --- /dev/null +++ b/src/components/apps/settings/tabs/AppsSettings.jsx @@ -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
+

Apps

+ {AppsManager.APPS.map((app) => + + )} +
; +} \ No newline at end of file diff --git a/src/components/apps/settings/StorageSettings.jsx b/src/components/apps/settings/tabs/StorageSettings.jsx similarity index 69% rename from src/components/apps/settings/StorageSettings.jsx rename to src/components/apps/settings/tabs/StorageSettings.jsx index 2324bde..907b738 100644 --- a/src/components/apps/settings/StorageSettings.jsx +++ b/src/components/apps/settings/tabs/StorageSettings.jsx @@ -1,10 +1,10 @@ -import styles from "./Settings.module.css"; -import utilStyles from "../../../styles/utils.module.css"; -import { round } from "../../../features/math/round.js"; -import { ProgressBar } from "../../_utils/progress-bar/ProgressBar.jsx"; -import { Button } from "../../_utils/button/Button.jsx"; -import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js"; -import { StorageManager } from "../../../features/storage/storageManager.js"; +import styles from "../Settings.module.css"; +import utilStyles from "../../../../styles/utils.module.css"; +import { round } from "../../../../features/math/round.js"; +import { ProgressBar } from "../../../_utils/progress-bar/ProgressBar.jsx"; +import { Button } from "../../../_utils/button/Button.jsx"; +import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js"; +import { StorageManager } from "../../../../features/storage/storageManager.js"; export function StorageTab() { const virtualRoot = useVirtualRoot(); diff --git a/src/components/apps/text-editor/TextEditor.jsx b/src/components/apps/text-editor/TextEditor.jsx index b7203f1..b9e72a4 100644 --- a/src/components/apps/text-editor/TextEditor.jsx +++ b/src/components/apps/text-editor/TextEditor.jsx @@ -9,6 +9,10 @@ import { MarkdownLink } from "./overrides/MarkdownLink.jsx"; import { MarkdownImage } from "./overrides/MarkdownImage.jsx"; import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; 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 = { a: MarkdownLink, @@ -26,6 +30,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal const [content, setContent] = useState(file?.content ?? ""); const [unsavedChanges, setUnsavedChanges] = useState(file == null); const [zoom, setZoom] = useState(DEFAULT_ZOOM); + const { openWindowedModal } = useWindowedModal({ modalsManager }); useEffect(() => { (async () => { @@ -115,12 +120,27 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal }; } + console.log(currentFile); + return (
{ + openWindowedModal({ + size: DEFAULT_FILE_SELECTOR_SIZE, + Modal: (props) => { + setCurrentFile(file); + setUnsavedChanges(false); + }} + {...props} + /> + }); + }, "Save": saveText, // "Save As": saveTextAs, "Quit": () => { @@ -145,6 +165,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal shortcuts={{ "File": { "New": ["Control", "e"], + "Open": ["Control", "o"], "Save": ["Control", "s"], "Quit": ["Control", "q"], }, @@ -156,17 +177,17 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal }} /> {currentMode === "view" - ? CODE_FORMATS.includes(file?.extension) + ? CODE_FORMATS.includes(currentFile?.extension) ? {content} :
- {file?.extension === "md" + {currentFile?.extension === "md" ? {content} - :

{content}

+ :

{content}

}
: