From f51d938a090b60a4da95a956253cfa0996f02a76 Mon Sep 17 00:00:00 2001 From: Prozilla Date: Tue, 19 Dec 2023 13:45:15 +0100 Subject: [PATCH] Updated desktop context menu --- public/config/desktop.xml | 1 + src/components/actions/Actions.jsx | 39 +++------------ src/components/actions/Actions.module.css | 40 ++++++++++++++-- .../actions/actions/Action.module.css | 0 src/components/actions/actions/Divider.jsx | 5 ++ .../actions/actions/DropdownAction.jsx | 29 ++++++++++++ .../actions/actions/RadioAction.jsx | 40 ++++++++++++++++ .../actions/actions/ToggleAction.jsx | 26 ++++++++++ .../file-explorer/FileExplorer.jsx | 5 +- .../file-explorer/FileExplorer.module.css | 2 + .../directory-list/DirectoryList.jsx | 5 -- .../directory-list/DirectoryList.module.css | 2 + src/components/desktop/Desktop.jsx | 47 ++++++++++++++++--- src/components/desktop/Desktop.module.css | 2 - src/constants/desktop.js | 3 +- src/features/settings/settings.js | 9 +++- src/features/utils/number.js | 3 ++ src/hooks/utils/screen.js | 45 +++++++++++++++++- 18 files changed, 249 insertions(+), 54 deletions(-) delete mode 100644 src/components/actions/actions/Action.module.css create mode 100644 src/components/actions/actions/Divider.jsx create mode 100644 src/components/actions/actions/DropdownAction.jsx create mode 100644 src/components/actions/actions/RadioAction.jsx create mode 100644 src/components/actions/actions/ToggleAction.jsx create mode 100644 src/features/utils/number.js diff --git a/public/config/desktop.xml b/public/config/desktop.xml index 4db23f9..5a30d6c 100644 --- a/public/config/desktop.xml +++ b/public/config/desktop.xml @@ -1,3 +1,4 @@ /assets/wallpapers/vibrant-wallpaper-purple-yellow.png + true \ No newline at end of file diff --git a/src/components/actions/Actions.jsx b/src/components/actions/Actions.jsx index e351724..587b4c0 100644 --- a/src/components/actions/Actions.jsx +++ b/src/components/actions/Actions.jsx @@ -1,8 +1,7 @@ -import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from "react"; +import { Children, cloneElement, isValidElement } from "react"; import { useShortcuts } from "../../hooks/utils/keyboard.js"; import styles from "./Actions.module.css"; -import { useScreenDimensions } from "../../hooks/utils/screen.js"; -import { TASKBAR_HEIGHT } from "../../constants/taskBar.js"; +import { useScreenBounds } from "../../hooks/utils/screen.js"; export const STYLES = { CONTEXT_MENU: styles["Context-menu"], @@ -34,11 +33,7 @@ export const STYLES = { export function Actions({ children, className, onAnyTrigger, triggerParams, avoidTaskbar = true }) { const isListener = (className === STYLES.SHORTCUTS_LISTENER); - const ref = useRef(null); - const [initiated, setInitiated] = useState(false); - const [alignLeft, setAlignLeft] = useState(false); - const [alignTop, setAlignTop] = useState(false); - const [screenWidth, screenHeight] = useScreenDimensions(); + const { ref, initiated, alignLeft, alignTop } = useScreenBounds({ avoidTaskbar }); const options = {}; const shortcuts = {}; @@ -68,9 +63,9 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi ...child.props, actionId, children: iterateOverChildren(child.props.children), - onTrigger: (event) => { - onAnyTrigger?.(event, triggerParams); - onTrigger?.(event, triggerParams); + onTrigger: (event, ...args) => { + onAnyTrigger?.(event, triggerParams, ...args); + onTrigger?.(event, triggerParams, ...args); } }); }); @@ -80,28 +75,6 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi useShortcuts({ options, shortcuts, useCategories: false }); - useEffect(() => { - if (ref.current == null || screenWidth == null || screenHeight == null) - return; - - const rect = ref.current.getBoundingClientRect(); - const maxX = screenWidth; - let maxY = screenHeight; - - if (avoidTaskbar) - maxY -= TASKBAR_HEIGHT; - - const isOverflowingRight = (rect.x + rect.width > maxX); - const isOverflowingBottom = (rect.y + rect.height > maxY); - - if (isOverflowingRight) - setAlignLeft(true); - if (isOverflowingBottom) - setAlignTop(true); - - setInitiated(true); - }, [alignLeft, avoidTaskbar, screenHeight, screenWidth]); - if (isListener) return iterateOverChildren(children); diff --git a/src/components/actions/Actions.module.css b/src/components/actions/Actions.module.css index e62a27f..234d5a2 100644 --- a/src/components/actions/Actions.module.css +++ b/src/components/actions/Actions.module.css @@ -32,8 +32,9 @@ .Context-menu.Container { --border-radius: 0.5rem; + --padding: 0.375rem; - padding: 0.375rem; + padding: var(--padding); border-top-left-radius: calc((1 - var(--right) * var(--bottom)) * var(--border-radius)) !important; border-top-right-radius: calc((1 - var(--left) * var(--bottom)) * var(--border-radius)) !important; border-bottom-left-radius: calc((1 - var(--right) * var(--top)) * var(--border-radius)) !important; @@ -41,7 +42,8 @@ background-color: var(--background-color-b) !important; } -.Context-menu .Button { +.Context-menu .Button, +.Context-menu .Dropdown { display: flex; gap: 0.75rem; justify-content: space-between; @@ -58,7 +60,9 @@ } .Context-menu .Button:hover, -.Context-menu .Button:focus-visible { +.Context-menu .Button:focus-visible, +.Context-menu .Dropdown:hover, +.Context-menu .Dropdown:focus-visible { background-color: hsla(var(--background-color-a-hsl), 75%); } @@ -88,4 +92,34 @@ .Context-menu .Shortcut { color: var(--foreground-color-b); +} + +.Context-menu .Dropdown { + position: relative; +} + +.Context-menu .Dropdown .Dropdown-content { + opacity: 1; + position: absolute; + top: calc(var(--padding) * -1); + left: 100%; + padding: var(--padding); + border-radius: 0.5rem; + border-top-left-radius: 0; + background-color: var(--background-color-b); + transition: opacity 100ms ease-out; + cursor: auto; +} + +.Context-menu .Dropdown:not(.Active) .Dropdown-content { + opacity: 0; + pointer-events: none; +} + +.Context-menu .Divider { + width: calc(100% - 0.5rem); + height: 0.1rem; + border-radius: 1rem; + background-color: var(--foreground-color-c); + margin: 0.5rem auto; } \ No newline at end of file diff --git a/src/components/actions/actions/Action.module.css b/src/components/actions/actions/Action.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/actions/actions/Divider.jsx b/src/components/actions/actions/Divider.jsx new file mode 100644 index 0000000..5935a0d --- /dev/null +++ b/src/components/actions/actions/Divider.jsx @@ -0,0 +1,5 @@ +import styles from "../Actions.module.css"; + +export function Divider() { + return
; +} \ No newline at end of file diff --git a/src/components/actions/actions/DropdownAction.jsx b/src/components/actions/actions/DropdownAction.jsx new file mode 100644 index 0000000..081b75c --- /dev/null +++ b/src/components/actions/actions/DropdownAction.jsx @@ -0,0 +1,29 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import styles from "../Actions.module.css"; +import { faCaretRight } from "@fortawesome/free-solid-svg-icons"; +import { useState } from "react"; + +export function DropdownAction({ label, icon, children }) { + const [showContent, setShowContent] = useState(false); + + const classNames = [styles.Dropdown]; + if (showContent) + classNames.push(styles.Active); + + return (
{ setShowContent(true); }} + onMouseLeave={() => { setShowContent(false); }} + > + + {icon &&
} +

{label}

+
+
+
+ {children} +
+
); +} \ No newline at end of file diff --git a/src/components/actions/actions/RadioAction.jsx b/src/components/actions/actions/RadioAction.jsx new file mode 100644 index 0000000..533d1fd --- /dev/null +++ b/src/components/actions/actions/RadioAction.jsx @@ -0,0 +1,40 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatShortcut } from "../../../features/utils/string.js"; +import styles from "../Actions.module.css"; +import { faCircleDot } from "@fortawesome/free-solid-svg-icons"; +import { useState } from "react"; +import { faCircle } from "@fortawesome/free-regular-svg-icons"; + +/** + * @param {object} props + * @param {string} props.actionId + * @param {{ + * label: string, + * shortcut: string[] + * }[]} props.options + * @param {number} props.initialIndex + * @param {Function} props.onTrigger + */ +export function RadioAction({ actionId, options, initialIndex, onTrigger }) { + const [activeIndex, setActiveIndex] = useState(initialIndex ?? 0); + + return (
+ {options.map(({ label, shortcut }, index) => + + )} +
); +} \ No newline at end of file diff --git a/src/components/actions/actions/ToggleAction.jsx b/src/components/actions/actions/ToggleAction.jsx new file mode 100644 index 0000000..06456b2 --- /dev/null +++ b/src/components/actions/actions/ToggleAction.jsx @@ -0,0 +1,26 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatShortcut } from "../../../features/utils/string.js"; +import styles from "../Actions.module.css"; +import { useState } from "react"; +import { faSquare } from "@fortawesome/free-regular-svg-icons"; +import { faSquareCheck } from "@fortawesome/free-solid-svg-icons"; + +export function ToggleAction({ actionId, label, shortcut, initialValue, onTrigger }) { + const [active, setActive] = useState(initialValue ?? false); + + return (); +} \ No newline at end of file diff --git a/src/components/applications/file-explorer/FileExplorer.jsx b/src/components/applications/file-explorer/FileExplorer.jsx index 72c7ea3..26da32f 100644 --- a/src/components/applications/file-explorer/FileExplorer.jsx +++ b/src/components/applications/file-explorer/FileExplorer.jsx @@ -217,7 +217,10 @@ export function FileExplorer({ startPath, app, modalsManager }) { showHidden={showHidden} onClickFile={(event, file) => { event.preventDefault(); - windowsManager.openFile(file, { mode: "view" }); + const options = {}; + if (file.extension === "md") + options.mode = "view"; + windowsManager.openFile(file, options); }} onClickFolder={(event, folder) => { changeDirectory(folder.linkedPath ?? folder.name); diff --git a/src/components/applications/file-explorer/FileExplorer.module.css b/src/components/applications/file-explorer/FileExplorer.module.css index 661fe83..f63b470 100644 --- a/src/components/applications/file-explorer/FileExplorer.module.css +++ b/src/components/applications/file-explorer/FileExplorer.module.css @@ -122,6 +122,8 @@ } .Main { + --scale: inherit !important; + position: relative; flex: 1; display: flex; diff --git a/src/components/applications/file-explorer/directory-list/DirectoryList.jsx b/src/components/applications/file-explorer/directory-list/DirectoryList.jsx index f7c3441..3ad524d 100644 --- a/src/components/applications/file-explorer/directory-list/DirectoryList.jsx +++ b/src/components/applications/file-explorer/directory-list/DirectoryList.jsx @@ -63,15 +63,11 @@ export function DirectoryList({ directory, showHidden = false, folderClassName, }; document.addEventListener("mousemove", onMoveRectSelect); - document.addEventListener("pointermove", onMoveRectSelect); document.addEventListener("mouseup", onStopRectSelect); - document.addEventListener("pointerup", onStopRectSelect); return () => { document.removeEventListener("mousemove", onMoveRectSelect); - document.removeEventListener("pointermove", onMoveRectSelect); document.removeEventListener("mouseup", onStopRectSelect); - document.removeEventListener("pointerup", onStopRectSelect); }; }); @@ -140,7 +136,6 @@ export function DirectoryList({ directory, showHidden = false, folderClassName, className={classNames.join(" ")} onClick={clearSelection} onMouseDown={onStartRectSelect} - onPointerDown={onStartRectSelect} {...props} > {rectSelectStart != null && rectSelectEnd != null diff --git a/src/components/applications/file-explorer/directory-list/DirectoryList.module.css b/src/components/applications/file-explorer/directory-list/DirectoryList.module.css index e25f190..49d0616 100644 --- a/src/components/applications/file-explorer/directory-list/DirectoryList.module.css +++ b/src/components/applications/file-explorer/directory-list/DirectoryList.module.css @@ -1,4 +1,6 @@ .Container { + --scale: 1rem; + position: relative; width: 100%; height: 100%; diff --git a/src/components/desktop/Desktop.jsx b/src/components/desktop/Desktop.jsx index c8ce8d3..79a6a34 100644 --- a/src/components/desktop/Desktop.jsx +++ b/src/components/desktop/Desktop.jsx @@ -7,7 +7,7 @@ import { useModals } from "../../hooks/modals/modals.js"; import { ModalsView } from "../modals/ModalsView.jsx"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; import { useContextMenu } from "../../hooks/modals/contextMenu.js"; -import { FALLBACK_WALLPAPER } from "../../constants/desktop.js"; +import { FALLBACK_ICON_SIZE, FALLBACK_WALLPAPER } from "../../constants/desktop.js"; import { reloadViewport } from "../../features/utils/browser.js"; import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext.js"; import { DirectoryList } from "../applications/file-explorer/directory-list/DirectoryList.jsx"; @@ -15,7 +15,12 @@ import { APPS, APP_NAMES } from "../../constants/applications.js"; import Vector2 from "../../features/math/vector2.js"; import { Actions } from "../actions/Actions.jsx"; import { ClickAction } from "../actions/actions/ClickAction.jsx"; -import { faArrowsRotate, faFolder, faPaintBrush, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faArrowsRotate, faEye, faFolder, faPaintBrush, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { ToggleAction } from "../actions/actions/ToggleAction.jsx"; +import { DropdownAction } from "../actions/actions/DropdownAction.jsx"; +import { RadioAction } from "../actions/actions/RadioAction.jsx"; +import { Divider } from "../actions/actions/Divider.jsx"; +import { isValidInteger } from "../../features/utils/number.js"; export const Desktop = memo(() => { const settingsManager = useSettingsManager(); @@ -23,9 +28,26 @@ export const Desktop = memo(() => { const [modalsManager, modals] = useModals(); const windowsManager = useWindowsManager(); const virtualRoot = useVirtualRoot(); + const [showIcons, setShowIcons] = useState(false); + const [iconSize, setIconSize] = useState(FALLBACK_ICON_SIZE); const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) => + + { + const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); + settings.set("icon-size", parseInt(value)); + }} options={[ + { label: "Small icons" }, + { label: "Medium icons" }, + { label: "Large icons" } + ]}/> + + { + const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); + settings.set("show-icons", (!showIcons).toString()); + }}/> + { reloadViewport(); }}/> @@ -68,6 +90,11 @@ export const Desktop = memo(() => { (async () => { const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); settings.get("wallpaper", setWallpaper); + settings.get("show-icons", (value) => { setShowIcons(value === "true"); }); + settings.get("icon-size", (value) => { + if (isValidInteger(value)) + setIconSize(parseInt(value)); + }); })(); }, [settingsManager]); @@ -78,6 +105,8 @@ export const Desktop = memo(() => { settings.set("wallpaper", FALLBACK_WALLPAPER); }; + const iconScale = 1 + ((isValidInteger(iconSize) ? iconSize : FALLBACK_ICON_SIZE) - 1) / 4; + return (<>
{ onContextMenu={onContextMenu} > - { event.preventDefault(); - const options = { mode: "view" }; - if (file.name === "info.md") { + const options = {}; + if (file.name === "info.md") options.size = new Vector2(575, 675); - } + if (file.extension === "md") + options.mode = "view"; windowsManager.openFile(file, options); }} @@ -105,7 +138,7 @@ export const Desktop = memo(() => { }} onContextMenuFile={onContextMenuFile} onContextMenuFolder={onContextMenuFolder} - /> + />} {wallpaper ? Desktop wallpaper : null diff --git a/src/components/desktop/Desktop.module.css b/src/components/desktop/Desktop.module.css index 3896d1f..1831f7e 100644 --- a/src/components/desktop/Desktop.module.css +++ b/src/components/desktop/Desktop.module.css @@ -18,8 +18,6 @@ } .Content { - --scale: 1rem; - position: absolute; display: flex; flex-direction: column; diff --git a/src/constants/desktop.js b/src/constants/desktop.js index 328a850..bc68cad 100644 --- a/src/constants/desktop.js +++ b/src/constants/desktop.js @@ -9,4 +9,5 @@ export const WALLPAPERS = [ "/assets/wallpapers/wave-abstract-wallpaper-teal.png", ]; -export const FALLBACK_WALLPAPER = WALLPAPERS[6]; \ No newline at end of file +export const FALLBACK_WALLPAPER = WALLPAPERS[6]; +export const FALLBACK_ICON_SIZE = 1; \ No newline at end of file diff --git a/src/features/settings/settings.js b/src/features/settings/settings.js index c6c87ca..02f982b 100644 --- a/src/features/settings/settings.js +++ b/src/features/settings/settings.js @@ -1,6 +1,8 @@ import { VirtualFile } from "../virtual-drive/virtualFile.js"; import { VirtualRoot } from "../virtual-drive/virtualRoot.js"; +const PARENT_NODE = "options"; + export class Settings { xmlDoc = null; @@ -112,8 +114,13 @@ export class Settings { if (await this.isMissingXmlDoc()) return; - if (this.xmlDoc.getElementsByTagName(key)?.[0]) + if (this.xmlDoc.getElementsByTagName(key).length > 0) { this.xmlDoc.getElementsByTagName(key)[0].textContent = value; + } else if (this.xmlDoc.getElementsByTagName(PARENT_NODE).length > 0) { + const newOption = this.xmlDoc.createElement(key); + newOption.textContent = value; + this.xmlDoc.getElementsByTagName(PARENT_NODE)[0].appendChild(newOption); + } await this.write(); } diff --git a/src/features/utils/number.js b/src/features/utils/number.js new file mode 100644 index 0000000..b2015fb --- /dev/null +++ b/src/features/utils/number.js @@ -0,0 +1,3 @@ +export function isValidInteger(number) { + return (parseInt(number) || parseInt(number) === 0); +} \ No newline at end of file diff --git a/src/hooks/utils/screen.js b/src/hooks/utils/screen.js index 1524028..7b0dbbe 100644 --- a/src/hooks/utils/screen.js +++ b/src/hooks/utils/screen.js @@ -1,4 +1,5 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { TASKBAR_HEIGHT } from "../../constants/taskBar.js"; /** * @returns {[number, number]} @@ -17,4 +18,46 @@ export function useScreenDimensions() { }, []); return [screenWidth, screenHeight]; +} + +/** + * @param {object} props + * @param {boolean} props.avoidTaskbar + * @returns {{ + * ref, + * initiated: boolean, + * alignLeft: boolean, + * alignTop: boolean + * }} + */ +export function useScreenBounds({ avoidTaskbar = true }) { + const ref = useRef(null); + const [initiated, setInitiated] = useState(false); + const [alignLeft, setAlignLeft] = useState(false); + const [alignTop, setAlignTop] = useState(false); + const [screenWidth, screenHeight] = useScreenDimensions(); + + useEffect(() => { + if (ref.current == null || screenWidth == null || screenHeight == null) + return; + + const rect = ref.current.getBoundingClientRect(); + const maxX = screenWidth; + let maxY = screenHeight; + + if (avoidTaskbar) + maxY -= TASKBAR_HEIGHT; + + const isOverflowingRight = (rect.x + rect.width > maxX); + const isOverflowingBottom = (rect.y + rect.height > maxY); + + if (isOverflowingRight) + setAlignLeft(true); + if (isOverflowingBottom) + setAlignTop(true); + + setInitiated(true); + }, [alignLeft, avoidTaskbar, screenHeight, screenWidth]); + + return { ref, initiated, alignLeft, alignTop }; } \ No newline at end of file