Added actions

This commit is contained in:
Prozilla 2023-12-13 18:21:37 +01:00
parent d73db00326
commit 5263cfd2b6
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
26 changed files with 460 additions and 209 deletions

View file

@ -63,7 +63,7 @@
"default-case": "off", "default-case": "off",
"arrow-parens": "error", "arrow-parens": "error",
"space-infix-ops": "warn", "space-infix-ops": "warn",
"react/no-multi-comp": "error", "react/no-multi-comp": ["error", { "ignoreStateless": true }],
"jsdoc/no-undefined-types": "warn", "jsdoc/no-undefined-types": "warn",
"jsdoc/require-param": "warn", "jsdoc/require-param": "warn",
"jsdoc/check-tag-names": "warn", "jsdoc/check-tag-names": "warn",

View file

@ -0,0 +1,121 @@
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from "react";
import { useShortcuts } from "../../hooks/utils/keyboard.js";
import style from "./Actions.module.css";
import { useScreenDimensions } from "../../hooks/utils/screen.js";
import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js";
export const STYLES = {
CONTEXT_MENU: style["Context-menu"],
SHORTCUTS_LISTENER: style["Shortcuts-listener"],
};
/**
* @callback actionsType
* @param {object} props
* @param {string=} props.className
* @param {Function} props.onAnyTrigger
* @param {import("react").ElementType} props.children
* @param {*} props.triggerParams
* @param {boolean} props.avoidTaskBar
*/
/**
* @type {actionsType}
* @example
* <ClickAction
* label="Reload"
* shortcut={["Control", "r"]}
* icon={faArrowsRotate}
* onTrigger={() => {
* reloadViewport();
* }}
* />
*/
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 options = {};
const shortcuts = {};
const iterateOverChildren = (children) => {
let actionId = 0;
const newChildren = Children.map(children, (child) => {
if (!isValidElement(child))
return child;
actionId++;
const { label, shortcut, onTrigger } = child.props;
if (label != null && onTrigger != null) {
options[actionId] = onTrigger;
if (shortcut != null)
shortcuts[actionId] = shortcut;
}
if (isListener) {
iterateOverChildren(child.props.children);
return;
}
return cloneElement(child, {
...child.props,
actionId,
children: iterateOverChildren(child.props.children),
onTrigger: (event) => {
onAnyTrigger?.(event, triggerParams);
onTrigger?.(event, triggerParams);
}
});
});
return newChildren;
};
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 -= TASK_BAR_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);
const classNames = [style.Container];
if (className != null)
classNames.push(className);
if (alignLeft)
classNames.push(style["Align-left"]);
if (alignTop)
classNames.push(style["Align-top"]);
if (!initiated)
classNames.push(style.Uninitiated);
return (<div ref={ref} className={classNames.join(" ")}>
{iterateOverChildren(children)}
</div>);
}

View file

@ -0,0 +1,91 @@
.Container {
/* These variables describe the position of the container relative to top left corner of its parent */
--left: 0;
--top: 0;
--right: calc(1 - var(--left));
--bottom: calc(1 - var(--top));
position: absolute;
top: 0;
left: 0;
opacity: 1;
transition: opacity 100ms ease-out;
}
.Container.Uninitiated {
opacity: 0;
}
.Container.Align-left {
--left: 1;
left: unset;
right: 0;
}
.Container.Align-top {
--top: 1;
top: unset;
bottom: 0;
}
.Context-menu.Container {
--border-radius: 0.5rem;
padding: 0.375rem;
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;
border-bottom-right-radius: calc((1 - var(--left) * var(--top)) * var(--border-radius)) !important;
background-color: var(--background-color-b) !important;
}
.Context-menu .Button {
display: flex;
gap: 0.75rem;
justify-content: space-between;
width: 100%;
padding: 0.25rem 0.5rem;
background: none;
border: none;
border-radius: 0.5rem;
outline: none;
font-size: 0.875rem;
text-align: start;
white-space: nowrap;
cursor: pointer;
}
.Context-menu .Button:hover,
.Context-menu .Button:focus-visible {
background-color: hsla(var(--background-color-a-hsl), 75%);
}
.Context-menu .Label {
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
}
.Context-menu .Label p,
.Context-menu .Shortcut {
margin: 0;
}
.Context-menu .Icon {
display: flex;
width: 0.875rem;
height: 0.875rem;
}
.Context-menu .Icon > svg {
width: 100%;
height: 100%;
object-fit: contain;
}
.Context-menu .Shortcut {
color: var(--foreground-color-b);
}

View file

@ -0,0 +1,13 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatShortcut } from "../../../features/utils/string.js";
import styles from "../Actions.module.css";
export function ClickAction({ actionId, label, shortcut, onTrigger, icon }) {
return (<button key={actionId} className={styles.Button} tabIndex={0} onClick={onTrigger}>
<span className={styles.Label}>
{icon && <div className={styles.Icon}><FontAwesomeIcon icon={icon}/></div>}
<p>{label}</p>
</span>
{shortcut && <p className={styles.Shortcut}>{formatShortcut(shortcut)}</p>}
</button>);
}

View file

@ -2,7 +2,7 @@ import { useState } from "react";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js"; import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
import styles from "./FileExplorer.module.css"; import styles from "./FileExplorer.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch } from "@fortawesome/free-solid-svg-icons"; import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
import { useContextMenu } from "../../../hooks/modals/contextMenu.js"; import { useContextMenu } from "../../../hooks/modals/contextMenu.js";
import { QuickAccessButton } from "./QuickAccessButton.jsx"; import { QuickAccessButton } from "./QuickAccessButton.jsx";
@ -10,6 +10,8 @@ import { useDialogBox } from "../../../hooks/modals/dialogBox.js";
import Vector2 from "../../../features/math/vector2.js"; import Vector2 from "../../../features/math/vector2.js";
import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js"; import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js";
import { DirectoryList } from "./directory-list/DirectoryList.jsx"; import { DirectoryList } from "./directory-list/DirectoryList.jsx";
import { Actions } from "../../actions/Actions.jsx";
import { ClickAction } from "../../actions/actions/ClickAction.jsx";
/** /**
* @param {import("../../windows/WindowView.jsx").windowProps} props * @param {import("../../windows/WindowView.jsx").windowProps} props
@ -21,19 +23,25 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const [showHidden] = useState(true); const [showHidden] = useState(true);
const { onContextMenu: onContextMenuFile } = useContextMenu({ const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
modalsManager, <Actions {...props}>
options: { <ClickAction label="Open" onTrigger={(event, file) => {
"Open": (file) => { file.open(windowsManager); }, file.open(windowsManager);
"Delete": (file) => { file.delete(); }, }}/>
} <ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
file.delete();
}}/>
</Actions>
}); });
const { onContextMenu: onContextMenuFolder } = useContextMenu({ const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
modalsManager, <Actions {...props}>
options: { <ClickAction label="Open" onTrigger={(event, folder) => {
"Open": (folder) => { changeDirectory(folder.name); }, changeDirectory(folder.name);
"Delete": (folder) => { folder.delete(); } }}/>
} <ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
folder.delete();
}}/>
</Actions>
}); });
// const { onContextMenu: onNew } = useContextMenu({ // const { onContextMenu: onNew } = useContextMenu({
// modalsManager, // modalsManager,
@ -50,7 +58,7 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path); const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path);
console.log(directory); console.debug(directory);
if (directory) { if (directory) {
setCurrentDirectory(directory); setCurrentDirectory(directory);

View file

@ -29,10 +29,7 @@ export function AboutSettings() {
</Button> </Button>
<Button <Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`} className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={(event) => { href="https://github.com/Prozilla/prozilla-os"
event.preventDefault();
window.open("https://github.com/Prozilla/prozilla-os");
}}
> >
View source View source
</Button> </Button>

View file

@ -131,28 +131,18 @@
} }
.Button { .Button {
--hover-color: var(--background-color-b); --normal-color: var(--background-color-a) !important;
--hover-color: var(--background-color-b) !important;
margin-bottom: 0.75rem; margin-bottom: 0.75rem !important;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
color: var(--foreground-color-a);
background-color: var(--background-color-a);
border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
outline: none;
transition: background-color 100ms ease-in-out;
cursor: pointer;
}
.Button:hover, .Button:focus-visible {
background-color: var(--hover-color);
} }
.Button-red { .Button-red {
--hover-color: var(--red-b); --text-color: var(--background-color-a) !important;
--normal-color: var(--red-a) !important;
color: var(--background-color-a); --hover-color: var(--red-b) !important;
background-color: var(--red-a);
} }
.Progress-bar-container { .Progress-bar-container {

View file

@ -13,6 +13,9 @@ import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext.js"
import { DirectoryList } from "../applications/file-explorer/directory-list/DirectoryList.jsx"; import { DirectoryList } from "../applications/file-explorer/directory-list/DirectoryList.jsx";
import { APPS, APP_NAMES } from "../../constants/applications.js"; import { APPS, APP_NAMES } from "../../constants/applications.js";
import Vector2 from "../../features/math/vector2.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";
export const Desktop = memo(() => { export const Desktop = memo(() => {
const settingsManager = useSettingsManager(); const settingsManager = useSettingsManager();
@ -21,35 +24,44 @@ export const Desktop = memo(() => {
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const { onContextMenu } = useContextMenu({ const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) =>
modalsManager, <Actions {...props}>
options: { <ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => {
"Refresh": () => { reloadViewport(); }, reloadViewport();
"Change appearance": () => { windowsManager.open("settings", { initialTabIndex: 0 }); }, }}/>
[`Open in ${APP_NAMES.FILE_EXPLORER}`]: () => { <ClickAction label="Change appearance" icon={faPaintBrush} onTrigger={() => {
windowsManager.open("settings", { initialTabIndex: 0 });
}}/>
<ClickAction label={`Open in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={() => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Desktop" }); windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Desktop" });
}, }}/>
} </Actions>
}); });
const { onContextMenu: onContextMenuFile } = useContextMenu({ const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
modalsManager, <Actions {...props}>
options: { <ClickAction label="Open" onTrigger={(event, file) => {
"Open": (file) => { file.open(windowsManager); }, file.open(windowsManager);
[`Reveal in ${APP_NAMES.FILE_EXPLORER}`]: (file) => { }}/>
windowsManager.open(APPS.FILE_EXPLORER, { startPath: file.parent.path }); <ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, file) => {
}, file.parent.open(windowsManager);
"Delete": (file) => { file.delete(); }, }}/>
} <ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
file.delete();
}}/>
</Actions>
}); });
const { onContextMenu: onContextMenuFolder } = useContextMenu({ const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
modalsManager, <Actions {...props}>
options: { <ClickAction label="Open" onTrigger={(event, folder) => {
"Open": (folder) => { folder.open(windowsManager); }, folder.open(windowsManager);
[`Reveal in ${APP_NAMES.FILE_EXPLORER}`]: (folder) => { }}/>
windowsManager.open(APPS.FILE_EXPLORER, { startPath: folder.parent.path }); <ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, folder) => {
}, folder.parent.open(windowsManager);
"Delete": (folder) => { folder.delete(); }, }}/>
} <ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
folder.delete();
}}/>
</Actions>
}); });
useEffect(() => { useEffect(() => {
@ -67,6 +79,7 @@ export const Desktop = memo(() => {
}; };
return (<> return (<>
<ShortcutsListener/>
<div <div
className={styles.Container} className={styles.Container}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}

View file

@ -24,15 +24,18 @@ export const ModalView = memo(({ modal }) => {
}; };
}, [modal]); }, [modal]);
const container = (<div className={styles.Container} style={{ "--position-x": modal.position.x, "--position-y": modal.position.y }}> const Container = () => (<div
className={styles.Container}
style={{ "--position-x": modal.position.x, "--position-y": modal.position.y }}
>
<modal.element modal={modal} {...modal.props}/> <modal.element modal={modal} {...modal.props}/>
</div>); </div>);
if (modal.dismissible) { if (modal.dismissible) {
return (<OutsideClickListener onOutsideClick={() => { modal.close(); }}> return (<OutsideClickListener onOutsideClick={() => { modal.close(); }}>
{container} <Container/>
</OutsideClickListener>); </OutsideClickListener>);
} else { } else {
return container; return <Container/>;
} }
}); });

View file

@ -11,7 +11,7 @@ import styles from "./ModalsView.module.css";
* @param {import("react").CSSProperties} root.style * @param {import("react").CSSProperties} root.style
* @param {import("react").className} root.className * @param {import("react").className} root.className
*/ */
export const ModalsView = memo(({ modalsManager, modals, style, className }) => { export const ModalsView = memo(({ modalsManager, modals, style, className, ...props }) => {
const ref = useRef(null); const ref = useRef(null);
useEffect(() => { useEffect(() => {
@ -20,7 +20,7 @@ export const ModalsView = memo(({ modalsManager, modals, style, className }) =>
}, [modalsManager, ref]); }, [modalsManager, ref]);
return ( return (
<div ref={ref} style={style} className={`${styles.Container} ${className}`}> <div ref={ref} style={style} className={`${styles.Container} ${className ?? ""}`} {...props}>
{modals?.map((modal) => {modals?.map((modal) =>
<ModalView key={modal.id} modal={modal}/> <ModalView key={modal.id} modal={modal}/>
)} )}

View file

@ -1,4 +1,4 @@
.Container { .Container {
position: relative; position: relative;
z-index: 9; z-index: 11;
} }

View file

@ -1,26 +0,0 @@
import Modal from "../../../features/modals/modal.js";
import { formatShortcut } from "../../../features/utils/string.js";
import styles from "./ContextMenu.module.css";
/**
* @param {object} props
* @param {Modal} props.modal
* @param {Object<string, Function>} props.options
* @param {Object<string, string[]>} props.shortcuts
*/
export function ContextMenu({ modal, options, shortcuts }) {
return (<div className={styles.Container}>
{Object.entries(options).map(([label, callback]) =>
<button className={styles.Button} key={label} tabIndex={0} onClick={() => {
modal.close();
callback(modal.props?.params);
}}>
<p className={styles.Label}>{label}</p>
{shortcuts && Object.keys(shortcuts).includes(label)
? <p className={styles.Shortcut}>{formatShortcut(shortcuts[label])}</p>
: null
}
</button>
)}
</div>);
}

View file

@ -1,33 +0,0 @@
.Container {
padding: 0.5rem;
border-top-left-radius: 0 !important;
background-color: var(--background-color-b) !important;
}
.Button {
display: flex;
gap: 0.75rem;
justify-content: space-between;
width: 100%;
padding: 0.25rem 0.5rem;
background: none;
border: none;
border-radius: 0.5rem;
outline: none;
font-size: 0.875rem;
text-align: start;
white-space: nowrap;
cursor: pointer;
}
.Button:hover, .Button:focus-visible {
background-color: hsla(var(--background-color-a-hsl), 75%);
}
.Label, .Shortcut {
margin: 0;
}
.Shortcut {
color: var(--foreground-color-b);
}

View file

@ -7,30 +7,18 @@ import utilStyles from "../../../styles/utils.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js"; import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js";
import { useScreenDimensions } from "../../../hooks/utils/screen.js";
export function DialogBox({ modal, params }) { export function DialogBox({ modal, params }) {
const { app, title, children } = params; const { app, title, children } = params;
const nodeRef = useRef(null); const nodeRef = useRef(null);
const [initialised, setInitialised] = useState(false);
const [startPosition, setStartPosition] = useState(modal.position); const [startPosition, setStartPosition] = useState(modal.position);
const [screenWidth, screenHeight] = useScreenDimensions();
const [screenWidth, setScreenWidth] = useState(100);
const [screenHeight, setScreenHeight] = useState(100);
useEffect(() => { useEffect(() => {
const resizeObserver = new ResizeObserver((event) => { if (screenWidth == null || screenHeight == null)
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
setScreenHeight(event[0].contentBoxSize[0].blockSize);
setInitialised(true);
});
resizeObserver.observe(document.getElementById("root"));
}, []);
useEffect(() => {
if (!initialised)
return; return;
if (modal.size.x > screenWidth || modal.size.y > screenHeight) { if (modal.size.x > screenWidth || modal.size.y > screenHeight) {
@ -45,7 +33,7 @@ export function DialogBox({ modal, params }) {
setStartPosition(modal.position); setStartPosition(modal.position);
} }
} }
}, [initialised, modal, screenHeight, screenWidth]); }, [modal, screenHeight, screenWidth]);
const onClick = (event) => { const onClick = (event) => {
event.preventDefault(); event.preventDefault();

View file

@ -1,7 +1,7 @@
import { memo, useRef, useState } from "react"; import { memo, useRef, useState } from "react";
import styles from "./TaskBar.module.css"; import styles from "./TaskBar.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
import AppsManager from "../../features/applications/applications.js"; import AppsManager from "../../features/applications/applications.js";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import { HomeMenu } from "./menus/HomeMenu.jsx"; import { HomeMenu } from "./menus/HomeMenu.jsx";
@ -13,24 +13,36 @@ import { SearchMenu } from "./menus/SearchMenu.jsx";
import { Calendar } from "./indicators/Calendar.jsx"; import { Calendar } from "./indicators/Calendar.jsx";
import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js"; import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js";
import { AppButton } from "./AppButton.jsx"; import { AppButton } from "./AppButton.jsx";
import { useContextMenu } from "../../hooks/modals/contextMenu.js";
import { Actions } from "../actions/Actions.jsx";
import { useModals } from "../../hooks/modals/modals.js";
import { ClickAction } from "../actions/actions/ClickAction.jsx";
import { APPS, APP_NAMES } from "../../constants/applications.js";
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
import { ModalsView } from "../modals/ModalsView.jsx";
import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js";
export const Taskbar = memo(() => { export const Taskbar = memo(() => {
const [showHome, setShowHome] = useState(false); const [showHome, setShowHome] = useState(false);
const [showSearch, setShowSearch] = useState(false); const [showSearch, setShowSearch] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const ref = useRef(null); const ref = useRef(null);
const { boxShadow, onUpdate } = useScrollWithShadow({ const { boxShadow, onUpdate } = useScrollWithShadow({ ref, shadow: {
ref, offset: 20,
shadow: { blurRadius: 10,
offset: 20, spreadRadius: -10,
blurRadius: 10, color: { a: 25 }
spreadRadius: -10, } });
color: {
a: 25
}
}
});
const inputRef = useRef(null); const inputRef = useRef(null);
const [modalsManager, modals] = useModals();
const windowsManager = useWindowsManager();
const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions avoidTaskBar={false} {...props}>
<ClickAction label={`Open ${APP_NAMES.SETTINGS}`} icon={faCog} onTrigger={() => {
windowsManager.open(APPS.SETTINGS);
}}/>
</Actions>
});
const updateShowHome = (value) => { const updateShowHome = (value) => {
setShowHome(value); setShowHome(value);
@ -67,8 +79,17 @@ export const Taskbar = memo(() => {
updateShowSearch(true); updateShowSearch(true);
}; };
return ( return (<>
<div className={styles["Task-bar"]}> <ModalsView modalsManager={modalsManager} modals={modals}/>
<div
style={{ "--task-bar-height": `${TASK_BAR_HEIGHT}px` }}
className={styles["Task-bar"]}
data-allow-context-menu={true}
onContextMenu={(event) => {
if (event.target.getAttribute("data-allow-context-menu"))
onContextMenu(event);
}}
>
<div className={styles["Menu-icons"]}> <div className={styles["Menu-icons"]}>
<div className={styles["Home-container"]}> <div className={styles["Home-container"]}>
<OutsideClickListener onOutsideClick={() => { updateShowHome(false); }}> <OutsideClickListener onOutsideClick={() => { updateShowHome(false); }}>
@ -103,8 +124,14 @@ export const Taskbar = memo(() => {
</OutsideClickListener> </OutsideClickListener>
</div> </div>
</div> </div>
<div className={styles["App-icons"]} style={{ boxShadow }}> <div className={styles["App-icons"]} data-allow-context-menu={true} style={{ boxShadow }}>
<div className={styles["App-icons-inner"]} onScroll={onUpdate} onResize={onUpdate} ref={ref}> <div
className={styles["App-icons-inner"]}
data-allow-context-menu={true}
onScroll={onUpdate}
onResize={onUpdate}
ref={ref}
>
{AppsManager.APPLICATIONS.map((app) => {AppsManager.APPLICATIONS.map((app) =>
<AppButton app={app} key={app.id}/> <AppButton app={app} key={app.id}/>
)} )}
@ -118,5 +145,5 @@ export const Taskbar = memo(() => {
<button title="View Desktop" id="desktop-button"/> <button title="View Desktop" id="desktop-button"/>
</div> </div>
</div> </div>
); </>);
}, []); }, []);

View file

@ -1,18 +1,39 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./Button.module.css"; import styles from "./Button.module.css";
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
/** /**
* @param {object} props * @param {object} props
* @param {string} props.className
* @param {string} props.href
*/ */
export function Button(props) { export function Button(props) {
const { className = "", children } = props; let { className = "" } = props;
const { href, children } = props;
return ( className = `${styles.Button} ${className}`;
<button
className={`${className} ${styles.Button}`} if (href != null) {
className = `${styles["Button-link"]} ${className}`;
return (<a
href={href}
target="_blank"
rel="noreferrer"
tabIndex={0} tabIndex={0}
{...props} {...props}
className={className}
> >
{children} {children}
</button> <FontAwesomeIcon icon={faExternalLink}/>
); </a>);
} else {
return (<button
tabIndex={0}
{...props}
className={className}
>
{children}
</button>);
}
} }

View file

@ -1,8 +1,25 @@
.Button { .Button {
color: var(--foreground-color-a); --text-color: var(--foreground-color-a);
background: none; --normal-color: var(--background-color-a);
--hover-color: var(--background-color-b);
color: var(--text-color);
background-color: var(--normal-color);
border: none; border: none;
outline: none; outline: none;
font-size: 0.875em;
transition: background-color 100ms ease-in-out; transition: background-color 100ms ease-in-out;
cursor: pointer; cursor: pointer;
} }
.Button:hover, .Button:focus-visible {
background-color: var(--hover-color);
}
.Button-link {
text-decoration: none;
}
.Button-link > svg {
margin-left: 0.5rem;
}

View file

@ -1,6 +1,6 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./WindowView.module.css"; import styles from "./WindowView.module.css";
import { faMinus, faWindowMaximize as fasWindowMaximize, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faExpand, faMinus, faWindowMaximize as fasWindowMaximize, faTimes, faXmark } from "@fortawesome/free-solid-svg-icons";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
import Draggable from "react-draggable"; import Draggable from "react-draggable";
@ -13,6 +13,9 @@ import { useModals } from "../../hooks/modals/modals.js";
import { ModalsView } from "../modals/ModalsView.jsx"; import { ModalsView } from "../modals/ModalsView.jsx";
import { useContextMenu } from "../../hooks/modals/contextMenu.js"; import { useContextMenu } from "../../hooks/modals/contextMenu.js";
import AppsManager from "../../features/applications/applications.js"; import AppsManager from "../../features/applications/applications.js";
import { ClickAction } from "../actions/actions/ClickAction.jsx";
import { Actions } from "../actions/Actions.jsx";
import { useScreenDimensions } from "../../hooks/utils/screen.js";
/** /**
* @typedef {object} windowProps * @typedef {object} windowProps
@ -41,43 +44,30 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
const nodeRef = useRef(null); const nodeRef = useRef(null);
const [modalsManager, modals] = useModals(); const [modalsManager, modals] = useModals();
const [initialised, setInitialised] = useState(false);
const [startSize, setStartSize] = useState(size); const [startSize, setStartSize] = useState(size);
const [startPosition, setStartPosition] = useState(position); const [startPosition, setStartPosition] = useState(position);
const [maximized, setMaximized] = useState(false); const [maximized, setMaximized] = useState(false);
const [minimized, setMinimized] = useState(false); const [minimized, setMinimized] = useState(false);
const [screenWidth, setScreenWidth] = useState(100); const [screenWidth, screenHeight] = useScreenDimensions();
const [screenHeight, setScreenHeight] = useState(100);
const [title, setTitle] = useState(app.name); const [title, setTitle] = useState(app.name);
const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id)); const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id));
const { onContextMenu } = useContextMenu({ const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) =>
modalsManager, <Actions {...props}>
options: { <ClickAction label="Maximize" icon={faExpand} shortcut={["F11"]} onTrigger={() => {
"Maximize": () => { setMaximized(!maximized); }, setMaximized(!maximized);
"Close": () => { close(); } }}/>
}, <ClickAction label="Close" icon={faTimes} shortcut={["Control", "q"]} onTrigger={() => {
shortcuts: { close();
"Maximize": ["F11"], }}/>
"Close": ["Control", "q"] </Actions>
}
}); });
useEffect(() => { useEffect(() => {
const resizeObserver = new ResizeObserver((event) => { if (screenWidth == null || screenHeight == null)
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
setScreenHeight(event[0].contentBoxSize[0].blockSize);
setInitialised(true);
});
resizeObserver.observe(document.getElementById("root"));
}, []);
useEffect(() => {
if (!initialised)
return; return;
if (size.x > screenWidth || size.y > screenHeight) { if (size.x > screenWidth || size.y > screenHeight) {
@ -94,7 +84,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
setStartPosition(position); setStartPosition(position);
} }
} }
}, [initialised, position, size, screenHeight, screenWidth]); }, [position, size, screenHeight, screenWidth]);
const close = (event) => { const close = (event) => {
event?.preventDefault(); event?.preventDefault();
@ -119,6 +109,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
classNames.push(styles.Minimized); classNames.push(styles.Minimized);
return (<> return (<>
<ShortcutsListener/>
<ModalsView modalsManager={modalsManager} modals={modals} style={{ zIndex: 1 }}/> <ModalsView modalsManager={modalsManager} modals={modals} style={{ zIndex: 1 }}/>
<Draggable <Draggable
key={id} key={id}

1
src/constants/taskBar.js Normal file
View file

@ -0,0 +1 @@
export const TASK_BAR_HEIGHT = 3 * 16;

View file

@ -1,2 +1 @@
export const SCREEN_MARGIN = 32; export const SCREEN_MARGIN = 32;
export const TASKBAR_HEIGHT = 48;

View file

@ -4,6 +4,7 @@
*/ */
export function closeViewport(requireConfirmation = false) { export function closeViewport(requireConfirmation = false) {
if (requireConfirmation && window.confirm("Are you sure you want to shut down ProzillaOS?")) { if (requireConfirmation && window.confirm("Are you sure you want to shut down ProzillaOS?")) {
console.info("Closing viewport");
window.open("about:blank", "_self"); window.open("about:blank", "_self");
} }
} }
@ -13,5 +14,6 @@ export function closeViewport(requireConfirmation = false) {
* @param {boolean} bypassCache * @param {boolean} bypassCache
*/ */
export function reloadViewport(bypassCache = false) { export function reloadViewport(bypassCache = false) {
console.info("Reloading viewport");
window.location.reload(bypassCache); window.location.reload(bypassCache);
} }

View file

@ -1,4 +1,5 @@
import { SCREEN_MARGIN, TASKBAR_HEIGHT } from "../../constants/windows.js"; import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js";
import { SCREEN_MARGIN } from "../../constants/windows.js";
import AppsManager from "../applications/applications.js"; import AppsManager from "../applications/applications.js";
import { randomRange } from "../math/random.js"; import { randomRange } from "../math/random.js";
import Vector2 from "../math/vector2.js"; import Vector2 from "../math/vector2.js";
@ -25,7 +26,7 @@ export default class WindowsManager {
const size = options?.size ?? app.windowOptions?.size ?? new Vector2(700, 400); const size = options?.size ?? app.windowOptions?.size ?? new Vector2(700, 400);
const position = new Vector2(randomRange(SCREEN_MARGIN, window.innerWidth - size.x - SCREEN_MARGIN), const position = new Vector2(randomRange(SCREEN_MARGIN, window.innerWidth - size.x - SCREEN_MARGIN),
randomRange(SCREEN_MARGIN, window.innerHeight - size.y - SCREEN_MARGIN - TASKBAR_HEIGHT)); randomRange(SCREEN_MARGIN, window.innerHeight - size.y - SCREEN_MARGIN - TASK_BAR_HEIGHT));
let id = 0; let id = 0;
while (this.windowIds.includes(id.toString())) { while (this.windowIds.includes(id.toString())) {

View file

@ -1,9 +1,8 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { ContextMenu } from "../../components/modals/context-menu/ContextMenu.jsx";
import Vector2 from "../../features/math/vector2.js"; import Vector2 from "../../features/math/vector2.js";
import Modal from "../../features/modals/modal.js"; import Modal from "../../features/modals/modal.js";
import ModalsManager from "../../features/modals/modals.js"; import ModalsManager from "../../features/modals/modals.js";
import { useShortcuts } from "../utils/keyboard.js"; import { STYLES } from "../../components/actions/Actions.jsx";
/** /**
* @typedef {Function} onContextMenuType * @typedef {Function} onContextMenuType
@ -15,11 +14,13 @@ import { useShortcuts } from "../utils/keyboard.js";
/** /**
* @param {object} props * @param {object} props
* @param {ModalsManager} props.modalsManager * @param {ModalsManager} props.modalsManager
* @param {Object<string, Object<string, Function>>} props.options * @param {import("../../components/actions/Actions.jsx").actionsType} props.Actions
* @param {Object<string, Object<string, string[]>>} props.shortcuts * @returns {{
* @returns {{ onContextMenu: onContextMenuType }} * onContextMenu: onContextMenuType,
* ShortcutsListener: import("../../components/actions/Actions.jsx").actionsType
* }}
*/ */
export function useContextMenu({ modalsManager, options, shortcuts }) { export function useContextMenu({ modalsManager, Actions }) {
// Open a new modal when context menu is triggered // Open a new modal when context menu is triggered
const onContextMenu = useCallback((event, params = {}) => { const onContextMenu = useCallback((event, params = {}) => {
event.preventDefault(); event.preventDefault();
@ -34,15 +35,21 @@ export function useContextMenu({ modalsManager, options, shortcuts }) {
positionY -= containerRect.y / 2; positionY -= containerRect.y / 2;
} }
const newModal = new Modal(ContextMenu) const newModal = new Modal(Actions)
.setPosition(new Vector2(positionX, positionY)) .setPosition(new Vector2(positionX, positionY))
.setProps({ options, shortcuts, params }); .setProps({
triggerParams: params,
className: STYLES.CONTEXT_MENU,
onAnyTrigger: () => {
newModal.close();
}
});
modalsManager.open(newModal); modalsManager.open(newModal);
return newModal; return newModal;
}, [modalsManager, options, shortcuts]); }, [Actions, modalsManager]);
useShortcuts({ options, shortcuts, useCategories: false }); const ShortcutsListener = () => <><Actions className={STYLES.SHORTCUTS_LISTENER}/></>;
return { onContextMenu }; return { onContextMenu, ShortcutsListener };
} }

View file

@ -1,7 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
/** /**
* @param {Object} params * @param {object} params
* @param {Function} params.onMouseDown * @param {Function} params.onMouseDown
* @param {Function} params.onMouseUp * @param {Function} params.onMouseUp
* @param {Function} params.onClick * @param {Function} params.onClick

20
src/hooks/utils/screen.js Normal file
View file

@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
/**
* @returns {[number, number]}
*/
export function useScreenDimensions() {
const [screenWidth, setScreenWidth] = useState(null);
const [screenHeight, setScreenHeight] = useState(null);
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
setScreenHeight(event[0].contentBoxSize[0].blockSize);
});
resizeObserver.observe(document.getElementById("root"));
}, []);
return [screenWidth, screenHeight];
}