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
?
: 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