Updated desktop context menu

This commit is contained in:
Prozilla 2024-06-11 18:39:22 +02:00
parent 00b82c6548
commit 9cad04edcd
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
9 changed files with 124 additions and 31 deletions

View file

@ -14,7 +14,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4">
<link rel="manifest" href="/site.webmanifest?v=4"> <link rel="manifest" href="/site.webmanifest?v=4">
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#4cdfff"> <link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#4cdfff">
<link rel="shortcut icon" href="/favicon.ico?v=4"> <link rel="shortcut icon" type="image/x-icon" href="https://os.prozilla.dev/favicon.ico?">
<meta name="apple-mobile-web-app-title" content="ProzillaOS"> <meta name="apple-mobile-web-app-title" content="ProzillaOS">
<meta name="application-name" content="ProzillaOS"> <meta name="application-name" content="ProzillaOS">
<meta name="msapplication-TileColor" content="#0d1114"> <meta name="msapplication-TileColor" content="#0d1114">
@ -70,8 +70,6 @@
] ]
} }
</script> </script>
<!-- Inserted tags -->
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run ProzillaOS.</noscript> <noscript>You need to enable JavaScript to run ProzillaOS.</noscript>

View file

@ -9,7 +9,7 @@ export interface ActionProps {
label?: string; label?: string;
icon?: string | object; icon?: string | object;
shortcut?: string[]; shortcut?: string[];
onTrigger?: (event: Event, triggerParams: unknown, ...args: unknown[]) => void; onTrigger?: (event?: Event, triggerParams?: unknown, ...args: unknown[]) => void;
children?: ReactNode; children?: ReactNode;
} }
@ -40,8 +40,8 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi
const options = {}; const options = {};
const shortcuts = {}; const shortcuts = {};
let actionId = 0;
const iterateOverChildren = (children: ReactNode): ReactNode => { const iterateOverChildren = (children: ReactNode): ReactNode => {
let actionId = 0;
const newChildren = Children.map(children, (child) => { const newChildren = Children.map(children, (child) => {
if (!isValidElement(child)) if (!isValidElement(child))
return child; return child;
@ -49,25 +49,30 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi
actionId++; actionId++;
const { label, shortcut, onTrigger } = child.props as ActionProps; const { label, shortcut, onTrigger } = child.props as ActionProps;
const onTriggerOverride = (event: Event, ...args: unknown[]) => {
onAnyTrigger?.(event, triggerParams, ...args);
onTrigger?.(event, triggerParams, ...args);
};
// Register shortcut
if (label != null && onTrigger != null) { if (label != null && onTrigger != null) {
options[actionId] = onTrigger; options[actionId] = onTriggerOverride;
if (shortcut != null) if (shortcut != null)
shortcuts[actionId] = shortcut; shortcuts[actionId] = shortcut;
} }
if (isListener) { // Prevent listener from rendering
if (isListener)
return iterateOverChildren((child.props as ActionProps).children); return iterateOverChildren((child.props as ActionProps).children);
}
return cloneElement(child, { return cloneElement(child, {
...child.props, ...child.props,
actionId, actionId,
children: iterateOverChildren((child.props as ActionProps).children), children: iterateOverChildren((child.props as ActionProps).children),
onTrigger: (event, ...args) => { onTrigger: onTriggerOverride
onAnyTrigger?.(event, triggerParams, ...args);
onTrigger?.(event, triggerParams, ...args);
}
} as ActionProps); } as ActionProps);
}); });

View file

@ -18,9 +18,11 @@
} }
.Content { .Content {
--direction: column;
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: column; flex-direction: var(--direction);
flex-wrap: wrap; flex-wrap: wrap;
align-content: flex-start; align-content: flex-start;
justify-content: flex-start; justify-content: flex-start;

View file

@ -5,7 +5,7 @@ import styles from "./Desktop.module.css";
import { useEffect } from "react"; import { useEffect } from "react";
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext";
import { useContextMenu } from "../../hooks/modals/contextMenu"; import { useContextMenu } from "../../hooks/modals/contextMenu";
import { FALLBACK_ICON_SIZE, FALLBACK_WALLPAPER } from "../../config/desktop.config"; import { FALLBACK_ICON_DIRECTION, FALLBACK_ICON_SIZE, FALLBACK_WALLPAPER } from "../../config/desktop.config";
import { reloadViewport } from "../../features/_utils/browser.utils"; import { reloadViewport } from "../../features/_utils/browser.utils";
import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext";
import { DirectoryList } from "../apps/file-explorer/directory-list/DirectoryList"; import { DirectoryList } from "../apps/file-explorer/directory-list/DirectoryList";
@ -13,7 +13,7 @@ import { APPS, APP_ICONS, APP_NAMES } from "../../config/apps.config";
import Vector2 from "../../features/math/vector2"; import Vector2 from "../../features/math/vector2";
import { Actions } from "../actions/Actions"; import { Actions } from "../actions/Actions";
import { ClickAction } from "../actions/actions/ClickAction"; import { ClickAction } from "../actions/actions/ClickAction";
import { faArrowsRotate, faEye, faFolder, faPaintBrush, faTerminal, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faArrowsRotate, faCompress, faExpand, faEye, faFolder, faPaintBrush, faTerminal, faTrash } from "@fortawesome/free-solid-svg-icons";
import { ToggleAction } from "../actions/actions/ToggleAction"; import { ToggleAction } from "../actions/actions/ToggleAction";
import { DropdownAction } from "../actions/actions/DropdownAction"; import { DropdownAction } from "../actions/actions/DropdownAction";
import { RadioAction } from "../actions/actions/RadioAction"; import { RadioAction } from "../actions/actions/RadioAction";
@ -34,6 +34,7 @@ export const Desktop = memo(() => {
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const [showIcons, setShowIcons] = useState(false); const [showIcons, setShowIcons] = useState(false);
const [iconSize, setIconSize] = useState(FALLBACK_ICON_SIZE); const [iconSize, setIconSize] = useState(FALLBACK_ICON_SIZE);
const [iconDirection, setIconDirection] = useState(FALLBACK_ICON_DIRECTION);
const { openWindowedModal } = useWindowedModal(); const { openWindowedModal } = useWindowedModal();
const directory = virtualRoot.navigate("~/Desktop"); const directory = virtualRoot.navigate("~/Desktop");
@ -47,7 +48,15 @@ export const Desktop = memo(() => {
}} options={[ }} options={[
{ label: "Small icons" }, { label: "Small icons" },
{ label: "Medium icons" }, { label: "Medium icons" },
{ label: "Large icons" } { label: "Large icons" },
]}/>
<Divider/>
<RadioAction initialIndex={iconDirection} onTrigger={(event, params, value: string) => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("icon-direction", value);
}} options={[
{ label: "Align vertically" },
{ label: "Align horizontally" },
]}/> ]}/>
<Divider/> <Divider/>
<ToggleAction label="Show dekstop icons" initialValue={showIcons} onTrigger={() => { <ToggleAction label="Show dekstop icons" initialValue={showIcons} onTrigger={() => {
@ -58,6 +67,25 @@ export const Desktop = memo(() => {
<ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => { <ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => {
reloadViewport(); reloadViewport();
}}/> }}/>
<ClickAction
label={!document.fullscreenElement ? "Enter fullscreen" : "Exit fullscreen"}
shortcut={["F11"]}
icon={!document.fullscreenElement ? faExpand : faCompress}
onTrigger={() => {
if (windowsManager.isAnyFocused())
return;
if (!document.fullscreenElement) {
void document.body.requestFullscreen().catch((error) => {
console.error(error);
});
} else {
void document.exitFullscreen().catch((error) => {
console.error(error);
});
}
}}
/>
<ClickAction label="Change appearance" icon={faPaintBrush} onTrigger={() => { <ClickAction label="Change appearance" icon={faPaintBrush} onTrigger={() => {
windowsManager.open("settings", { tab: TABS.APPEARANCE }); windowsManager.open("settings", { tab: TABS.APPEARANCE });
}}/> }}/>
@ -122,6 +150,10 @@ export const Desktop = memo(() => {
if (isValidInteger(value)) if (isValidInteger(value))
setIconSize(parseInt(value)); setIconSize(parseInt(value));
}); });
void settings.get("icon-direction", (value) => {
if (isValidInteger(value))
setIconDirection(parseInt(value));
});
}, [settingsManager]); }, [settingsManager]);
const onError = () => { const onError = () => {
@ -129,7 +161,7 @@ export const Desktop = memo(() => {
void settings.set("wallpaper", FALLBACK_WALLPAPER); void settings.set("wallpaper", FALLBACK_WALLPAPER);
}; };
const iconScale = 1 + ((isValidInteger(iconSize) ? iconSize : FALLBACK_ICON_SIZE) - 1) / 4; const iconScale = 1 + ((isValidInteger(iconSize) ? iconSize : FALLBACK_ICON_SIZE) - 1) / 5;
return (<> return (<>
<ShortcutsListener/> <ShortcutsListener/>
@ -141,7 +173,8 @@ export const Desktop = memo(() => {
directory={directory as VirtualFolder} directory={directory as VirtualFolder}
className={styles.Content} className={styles.Content}
style={{ style={{
"--scale": `${iconScale}rem` "--scale": `${iconScale}rem`,
"--direction": iconDirection == 1 ? "row" : "column"
}} }}
fileClassName={styles["Item"]} fileClassName={styles["Item"]}
folderClassName={styles["Item"]} folderClassName={styles["Item"]}

View file

@ -11,4 +11,5 @@ export const WALLPAPERS = [
]; ];
export const FALLBACK_WALLPAPER = WALLPAPERS[6]; export const FALLBACK_WALLPAPER = WALLPAPERS[6];
export const FALLBACK_ICON_SIZE = 1; export const FALLBACK_ICON_SIZE = 1;
export const FALLBACK_ICON_DIRECTION = 0; // 0: vertical, 1: horizontal

View file

@ -79,11 +79,18 @@ export function loadDefaultData(virtualRoot: VirtualRoot) {
}); });
}); });
// Create files and folders based on repository tree loadTree(virtualRoot);
}
// Create files and folders based on repository tree
function loadTree(virtualRoot: VirtualRoot) {
const excludedFiles = [
"/public/config/tree.json"
];
void fetch("/config/tree.json").then((response) => void fetch("/config/tree.json").then((response) =>
response.json() response.json()
).then(({ files, folders }: { files: string[], folders: string[] }) => { ).then(({ files, folders }: { files: string[], folders: string[] }) => {
folders.forEach((folderPath) => { folders.forEach((folderPath) => {
const lastSlashIndex = folderPath.lastIndexOf("/"); const lastSlashIndex = folderPath.lastIndexOf("/");
@ -100,11 +107,12 @@ export function loadDefaultData(virtualRoot: VirtualRoot) {
}); });
files.forEach((filePath) => { files.forEach((filePath) => {
if (excludedFiles.includes(filePath))
return;
const lastSlashIndex = filePath.lastIndexOf("/"); const lastSlashIndex = filePath.lastIndexOf("/");
const callback = (virtualFile: VirtualFile) => { const callback = (virtualFile: VirtualFile) => {
console.log(virtualFile.absolutePath);
const virtualPath = virtualFile.absolutePath; const virtualPath = virtualFile.absolutePath;
if (virtualPath.startsWith("/public/")) { if (virtualPath.startsWith("/public/")) {
virtualFile.setSource(virtualPath.replace(/^\/public\//, "/")); virtualFile.setSource(virtualPath.replace(/^\/public\//, "/"));
@ -126,6 +134,6 @@ export function loadDefaultData(virtualRoot: VirtualRoot) {
parentFolder.createFile(name, extension, callback); parentFolder.createFile(name, extension, callback);
}); });
}).catch(() => { }).catch(() => {
console.warn("Failed to fetch repository tree. Make sure the tree data is valid and up-to-date using 'npm run fetch'."); console.warn("Failed to load repository tree. Make sure the tree data is valid and up-to-date using 'npm run fetch'.");
}); });
} }

View file

@ -136,6 +136,17 @@ export default class WindowsManager {
return this.windows[windowId].isFocused; return this.windows[windowId].isFocused;
} }
isAnyFocused() {
let anyFocused = false;
Object.values(this.windows).forEach((window) => {
if (window.isFocused)
return anyFocused = true;
});
return anyFocused;
}
/** /**
* @param minimized - Leave as undefined to toggle the window's minimization state * @param minimized - Leave as undefined to toggle the window's minimization state
*/ */

View file

@ -28,6 +28,9 @@ interface UseShortcutsParams {
useCategories?: boolean, useCategories?: boolean,
} }
/**
* TO DO: rewrite to use a global shortcuts manager instead, to allow certain shortcuts to be prioritized and prevent conflicts
*/
export function useShortcuts({ options, shortcuts, useCategories = true }: UseShortcutsParams) { export function useShortcuts({ options, shortcuts, useCategories = true }: UseShortcutsParams) {
const [activeKeys, setActiveKeys] = useState<string[]>([]); const [activeKeys, setActiveKeys] = useState<string[]>([]);
@ -37,10 +40,6 @@ export function useShortcuts({ options, shortcuts, useCategories = true }: UseSh
const keys = [...activeKeys]; const keys = [...activeKeys];
// Prevent alt-tabbing from messing with alt key registration
if (keys.includes("Alt") && !event.altKey)
removeFromArray("Alt", keys);
const checkGroup = (group: Record<string, string[]>, category?: string) => { const checkGroup = (group: Record<string, string[]>, category?: string) => {
for (const [name, shortcut] of Object.entries(group)) { for (const [name, shortcut] of Object.entries(group)) {
let active = true; let active = true;
@ -60,9 +59,9 @@ export function useShortcuts({ options, shortcuts, useCategories = true }: UseSh
continue; continue;
if (category != null) { if (category != null) {
(options?.[category]?.[name] as Function)?.(); (options?.[category]?.[name] as Function)?.(event);
} else { } else {
(options?.[name] as Function)?.(); (options?.[name] as Function)?.(event);
} }
} }
}; };
@ -78,6 +77,18 @@ export function useShortcuts({ options, shortcuts, useCategories = true }: UseSh
setActiveKeys(keys); setActiveKeys(keys);
}, [activeKeys, options, shortcuts, useCategories]); }, [activeKeys, options, shortcuts, useCategories]);
useEffect(() => {
const onBlur = () => {
setActiveKeys([]);
};
document.addEventListener("blur", onBlur);
return () => {
document.removeEventListener("blur", onBlur);
};
}, []);
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
const isRepeated = activeKeys.includes(event.key); const isRepeated = activeKeys.includes(event.key);
checkShortcuts(event, isRepeated); checkShortcuts(event, isRepeated);

View file

@ -59,6 +59,23 @@ function generateCname() {
return DOMAIN; return DOMAIN;
} }
function generateTemplate(html: string) {
const baseUrlRegex = /(?<=")https?:\/\/[a-z0-9-]+(\.)([a-zA-Z0-9-]+(\.))*[a-z]{2,3}\/(?=.*")/gi;
html = html.replaceAll(baseUrlRegex, BASE_URL);
const commentsRegex = /<!--.*?-->/g;
html = html.replaceAll(commentsRegex, "");
const emptyLinesRegex = /^\s*$\n/gm;
html = html.replaceAll(emptyLinesRegex, "");
const path = `${BUILD_DIR}/index.html`;
fs.writeFileSync(path, html, { flag: "w+" });
console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`);
return html;
}
/** /**
* To avoid GitHub pages rendering certain pages that are only defined by React router as a 404 page, * To avoid GitHub pages rendering certain pages that are only defined by React router as a 404 page,
* we copy the content of our index file to the 404 page, letting React router properly handle routing on every page * we copy the content of our index file to the 404 page, letting React router properly handle routing on every page
@ -69,6 +86,9 @@ function generate404Page(template: string) {
console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`); console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`);
} }
/**
* Add an HTML file for every app page so they can be properly crawled and indexed
*/
function generateAppPages(template: string) { function generateAppPages(template: string) {
for (const [key, value] of Object.entries(APPS)) { for (const [key, value] of Object.entries(APPS)) {
const appId = value; const appId = value;
@ -120,9 +140,13 @@ try {
}); });
console.log(`\n${ANSI.fg.yellow}Generating pages...${ANSI.reset}`); console.log(`\n${ANSI.fg.yellow}Generating pages...${ANSI.reset}`);
const template = fs.readFileSync(PATHS.indexHtml, "utf-8");
const html = fs.readFileSync(PATHS.indexHtml, "utf-8");
const template = generateTemplate(html);
generate404Page(template); generate404Page(template);
generateAppPages(template); generateAppPages(template);
console.log(`\n${ANSI.fg.green}✓ Staging complete${ANSI.reset}`); console.log(`\n${ANSI.fg.green}✓ Staging complete${ANSI.reset}`);
} catch (error) { } catch (error) {
console.error(error); console.error(error);