Updated desktop context menu
This commit is contained in:
parent
ed80c17dfa
commit
f51d938a09
18 changed files with 249 additions and 54 deletions
|
|
@ -1,3 +1,4 @@
|
|||
<options>
|
||||
<wallpaper>/assets/wallpapers/vibrant-wallpaper-purple-yellow.png</wallpaper>
|
||||
<show-icons>true</show-icons>
|
||||
</options>
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
5
src/components/actions/actions/Divider.jsx
Normal file
5
src/components/actions/actions/Divider.jsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import styles from "../Actions.module.css";
|
||||
|
||||
export function Divider() {
|
||||
return <div className={styles.Divider}/>;
|
||||
}
|
||||
29
src/components/actions/actions/DropdownAction.jsx
Normal file
29
src/components/actions/actions/DropdownAction.jsx
Normal file
|
|
@ -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 (<div
|
||||
key={label}
|
||||
className={classNames.join(" ")}
|
||||
tabIndex={0}
|
||||
onMouseEnter={() => { setShowContent(true); }}
|
||||
onMouseLeave={() => { setShowContent(false); }}
|
||||
>
|
||||
<span className={styles.Label}>
|
||||
{icon && <div className={styles.Icon}><FontAwesomeIcon icon={icon}/></div>}
|
||||
<p>{label}</p>
|
||||
</span>
|
||||
<div className={styles["Dropdown-arrow"]}><FontAwesomeIcon icon={faCaretRight}/></div>
|
||||
<div className={styles["Dropdown-content"]}>
|
||||
{children}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
40
src/components/actions/actions/RadioAction.jsx
Normal file
40
src/components/actions/actions/RadioAction.jsx
Normal file
|
|
@ -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 (<div key={actionId}>
|
||||
{options.map(({ label, shortcut }, index) =>
|
||||
<button key={label} className={styles.Button} tabIndex={0} onClick={(event) => {
|
||||
setActiveIndex(index);
|
||||
onTrigger(event, index);
|
||||
}}>
|
||||
<span className={styles.Label}>
|
||||
<div className={styles.Icon}>
|
||||
{activeIndex === index
|
||||
? <FontAwesomeIcon icon={faCircleDot}/>
|
||||
: <FontAwesomeIcon icon={faCircle}/>
|
||||
}
|
||||
</div>
|
||||
<p>{label}</p>
|
||||
</span>
|
||||
{shortcut && <p className={styles.Shortcut}>{formatShortcut(shortcut)}</p>}
|
||||
</button>
|
||||
)}
|
||||
</div>);
|
||||
}
|
||||
26
src/components/actions/actions/ToggleAction.jsx
Normal file
26
src/components/actions/actions/ToggleAction.jsx
Normal file
|
|
@ -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 (<button key={actionId} className={styles.Button} tabIndex={0} onClick={(event) => {
|
||||
onTrigger(event, !active);
|
||||
setActive(!active);
|
||||
}}>
|
||||
<span className={styles.Label}>
|
||||
<div className={styles.Icon}>
|
||||
{active
|
||||
? <FontAwesomeIcon icon={faSquareCheck}/>
|
||||
: <FontAwesomeIcon icon={faSquare}/>
|
||||
}
|
||||
</div>
|
||||
<p>{label}</p>
|
||||
</span>
|
||||
{shortcut && <p className={styles.Shortcut}>{formatShortcut(shortcut)}</p>}
|
||||
</button>);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@
|
|||
}
|
||||
|
||||
.Main {
|
||||
--scale: inherit !important;
|
||||
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
.Container {
|
||||
--scale: 1rem;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
<Actions {...props}>
|
||||
<DropdownAction label="View" icon={faEye}>
|
||||
<RadioAction initialIndex={iconSize} onTrigger={(event, params, value) => {
|
||||
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
|
||||
settings.set("icon-size", parseInt(value));
|
||||
}} options={[
|
||||
{ label: "Small icons" },
|
||||
{ label: "Medium icons" },
|
||||
{ label: "Large icons" }
|
||||
]}/>
|
||||
<Divider/>
|
||||
<ToggleAction label="Show dekstop icons" initialValue={showIcons} onTrigger={(event, params, value) => {
|
||||
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
|
||||
settings.set("show-icons", (!showIcons).toString());
|
||||
}}/>
|
||||
</DropdownAction>
|
||||
<ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => {
|
||||
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 (<>
|
||||
<ShortcutsListener/>
|
||||
<div
|
||||
|
|
@ -85,18 +114,22 @@ export const Desktop = memo(() => {
|
|||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<ModalsView modalsManager={modalsManager} modals={modals}/>
|
||||
<DirectoryList
|
||||
{showIcons && <DirectoryList
|
||||
directory={directory}
|
||||
className={styles.Content}
|
||||
style={{
|
||||
"--scale": `${iconScale}rem`
|
||||
}}
|
||||
fileClassName={styles["Item"]}
|
||||
folderClassName={styles["Item"]}
|
||||
onClickFile={(event, file) => {
|
||||
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
|
||||
? <img src={wallpaper} className={styles.Wallpaper} alt="Desktop wallpaper" onError={onError}/>
|
||||
: null
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@
|
|||
}
|
||||
|
||||
.Content {
|
||||
--scale: 1rem;
|
||||
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export const WALLPAPERS = [
|
|||
"/assets/wallpapers/wave-abstract-wallpaper-teal.png",
|
||||
];
|
||||
|
||||
export const FALLBACK_WALLPAPER = WALLPAPERS[6];
|
||||
export const FALLBACK_WALLPAPER = WALLPAPERS[6];
|
||||
export const FALLBACK_ICON_SIZE = 1;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
3
src/features/utils/number.js
Normal file
3
src/features/utils/number.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function isValidInteger(number) {
|
||||
return (parseInt(number) || parseInt(number) === 0);
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
Loading…
Reference in a new issue