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",
"arrow-parens": "error",
"space-infix-ops": "warn",
"react/no-multi-comp": "error",
"react/no-multi-comp": ["error", { "ignoreStateless": true }],
"jsdoc/no-undefined-types": "warn",
"jsdoc/require-param": "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 styles from "./FileExplorer.module.css";
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 { useContextMenu } from "../../../hooks/modals/contextMenu.js";
import { QuickAccessButton } from "./QuickAccessButton.jsx";
@ -10,6 +10,8 @@ import { useDialogBox } from "../../../hooks/modals/dialogBox.js";
import Vector2 from "../../../features/math/vector2.js";
import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js";
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
@ -21,19 +23,25 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const windowsManager = useWindowsManager();
const [showHidden] = useState(true);
const { onContextMenu: onContextMenuFile } = useContextMenu({
modalsManager,
options: {
"Open": (file) => { file.open(windowsManager); },
"Delete": (file) => { file.delete(); },
}
const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, file) => {
file.open(windowsManager);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
file.delete();
}}/>
</Actions>
});
const { onContextMenu: onContextMenuFolder } = useContextMenu({
modalsManager,
options: {
"Open": (folder) => { changeDirectory(folder.name); },
"Delete": (folder) => { folder.delete(); }
}
const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder) => {
changeDirectory(folder.name);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
folder.delete();
}}/>
</Actions>
});
// const { onContextMenu: onNew } = useContextMenu({
// modalsManager,
@ -50,7 +58,7 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path);
console.log(directory);
console.debug(directory);
if (directory) {
setCurrentDirectory(directory);

View file

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

View file

@ -131,28 +131,18 @@
}
.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;
color: var(--foreground-color-a);
background-color: var(--background-color-a);
border: none;
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 {
--hover-color: var(--red-b);
color: var(--background-color-a);
background-color: var(--red-a);
--text-color: var(--background-color-a) !important;
--normal-color: var(--red-a) !important;
--hover-color: var(--red-b) !important;
}
.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 { 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";
export const Desktop = memo(() => {
const settingsManager = useSettingsManager();
@ -21,35 +24,44 @@ export const Desktop = memo(() => {
const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot();
const { onContextMenu } = useContextMenu({
modalsManager,
options: {
"Refresh": () => { reloadViewport(); },
"Change appearance": () => { windowsManager.open("settings", { initialTabIndex: 0 }); },
[`Open in ${APP_NAMES.FILE_EXPLORER}`]: () => {
const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => {
reloadViewport();
}}/>
<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" });
},
}
}}/>
</Actions>
});
const { onContextMenu: onContextMenuFile } = useContextMenu({
modalsManager,
options: {
"Open": (file) => { file.open(windowsManager); },
[`Reveal in ${APP_NAMES.FILE_EXPLORER}`]: (file) => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: file.parent.path });
},
"Delete": (file) => { file.delete(); },
}
const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, file) => {
file.open(windowsManager);
}}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, file) => {
file.parent.open(windowsManager);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
file.delete();
}}/>
</Actions>
});
const { onContextMenu: onContextMenuFolder } = useContextMenu({
modalsManager,
options: {
"Open": (folder) => { folder.open(windowsManager); },
[`Reveal in ${APP_NAMES.FILE_EXPLORER}`]: (folder) => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: folder.parent.path });
},
"Delete": (folder) => { folder.delete(); },
}
const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder) => {
folder.open(windowsManager);
}}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, folder) => {
folder.parent.open(windowsManager);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
folder.delete();
}}/>
</Actions>
});
useEffect(() => {
@ -67,6 +79,7 @@ export const Desktop = memo(() => {
};
return (<>
<ShortcutsListener/>
<div
className={styles.Container}
onContextMenu={onContextMenu}

View file

@ -24,15 +24,18 @@ export const ModalView = memo(({ 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}/>
</div>);
if (modal.dismissible) {
return (<OutsideClickListener onOutsideClick={() => { modal.close(); }}>
{container}
<Container/>
</OutsideClickListener>);
} 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").className} root.className
*/
export const ModalsView = memo(({ modalsManager, modals, style, className }) => {
export const ModalsView = memo(({ modalsManager, modals, style, className, ...props }) => {
const ref = useRef(null);
useEffect(() => {
@ -20,7 +20,7 @@ export const ModalsView = memo(({ modalsManager, modals, style, className }) =>
}, [modalsManager, ref]);
return (
<div ref={ref} style={style} className={`${styles.Container} ${className}`}>
<div ref={ref} style={style} className={`${styles.Container} ${className ?? ""}`} {...props}>
{modals?.map((modal) =>
<ModalView key={modal.id} modal={modal}/>
)}

View file

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

View file

@ -1,7 +1,7 @@
import { memo, useRef, useState } from "react";
import styles from "./TaskBar.module.css";
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 { ReactSVG } from "react-svg";
import { HomeMenu } from "./menus/HomeMenu.jsx";
@ -13,24 +13,36 @@ import { SearchMenu } from "./menus/SearchMenu.jsx";
import { Calendar } from "./indicators/Calendar.jsx";
import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js";
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(() => {
const [showHome, setShowHome] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const ref = useRef(null);
const { boxShadow, onUpdate } = useScrollWithShadow({
ref,
shadow: {
offset: 20,
blurRadius: 10,
spreadRadius: -10,
color: {
a: 25
}
}
});
const { boxShadow, onUpdate } = useScrollWithShadow({ ref, shadow: {
offset: 20,
blurRadius: 10,
spreadRadius: -10,
color: { a: 25 }
} });
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) => {
setShowHome(value);
@ -67,8 +79,17 @@ export const Taskbar = memo(() => {
updateShowSearch(true);
};
return (
<div className={styles["Task-bar"]}>
return (<>
<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["Home-container"]}>
<OutsideClickListener onOutsideClick={() => { updateShowHome(false); }}>
@ -103,8 +124,14 @@ export const Taskbar = memo(() => {
</OutsideClickListener>
</div>
</div>
<div className={styles["App-icons"]} style={{ boxShadow }}>
<div className={styles["App-icons-inner"]} onScroll={onUpdate} onResize={onUpdate} ref={ref}>
<div className={styles["App-icons"]} data-allow-context-menu={true} style={{ boxShadow }}>
<div
className={styles["App-icons-inner"]}
data-allow-context-menu={true}
onScroll={onUpdate}
onResize={onUpdate}
ref={ref}
>
{AppsManager.APPLICATIONS.map((app) =>
<AppButton app={app} key={app.id}/>
)}
@ -118,5 +145,5 @@ export const Taskbar = memo(() => {
<button title="View Desktop" id="desktop-button"/>
</div>
</div>
);
</>);
}, []);

View file

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

View file

@ -1,8 +1,25 @@
.Button {
color: var(--foreground-color-a);
background: none;
--text-color: var(--foreground-color-a);
--normal-color: var(--background-color-a);
--hover-color: var(--background-color-b);
color: var(--text-color);
background-color: var(--normal-color);
border: none;
outline: none;
font-size: 0.875em;
transition: background-color 100ms ease-in-out;
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 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 { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
import Draggable from "react-draggable";
@ -13,6 +13,9 @@ import { useModals } from "../../hooks/modals/modals.js";
import { ModalsView } from "../modals/ModalsView.jsx";
import { useContextMenu } from "../../hooks/modals/contextMenu.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
@ -41,43 +44,30 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
const nodeRef = useRef(null);
const [modalsManager, modals] = useModals();
const [initialised, setInitialised] = useState(false);
const [startSize, setStartSize] = useState(size);
const [startPosition, setStartPosition] = useState(position);
const [maximized, setMaximized] = useState(false);
const [minimized, setMinimized] = useState(false);
const [screenWidth, setScreenWidth] = useState(100);
const [screenHeight, setScreenHeight] = useState(100);
const [screenWidth, screenHeight] = useScreenDimensions();
const [title, setTitle] = useState(app.name);
const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id));
const { onContextMenu } = useContextMenu({
modalsManager,
options: {
"Maximize": () => { setMaximized(!maximized); },
"Close": () => { close(); }
},
shortcuts: {
"Maximize": ["F11"],
"Close": ["Control", "q"]
}
const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Maximize" icon={faExpand} shortcut={["F11"]} onTrigger={() => {
setMaximized(!maximized);
}}/>
<ClickAction label="Close" icon={faTimes} shortcut={["Control", "q"]} onTrigger={() => {
close();
}}/>
</Actions>
});
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
setScreenHeight(event[0].contentBoxSize[0].blockSize);
setInitialised(true);
});
resizeObserver.observe(document.getElementById("root"));
}, []);
useEffect(() => {
if (!initialised)
if (screenWidth == null || screenHeight == null)
return;
if (size.x > screenWidth || size.y > screenHeight) {
@ -94,7 +84,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
setStartPosition(position);
}
}
}, [initialised, position, size, screenHeight, screenWidth]);
}, [position, size, screenHeight, screenWidth]);
const close = (event) => {
event?.preventDefault();
@ -119,6 +109,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
classNames.push(styles.Minimized);
return (<>
<ShortcutsListener/>
<ModalsView modalsManager={modalsManager} modals={modals} style={{ zIndex: 1 }}/>
<Draggable
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 TASKBAR_HEIGHT = 48;
export const SCREEN_MARGIN = 32;

View file

@ -4,6 +4,7 @@
*/
export function closeViewport(requireConfirmation = false) {
if (requireConfirmation && window.confirm("Are you sure you want to shut down ProzillaOS?")) {
console.info("Closing viewport");
window.open("about:blank", "_self");
}
}
@ -13,5 +14,6 @@ export function closeViewport(requireConfirmation = false) {
* @param {boolean} bypassCache
*/
export function reloadViewport(bypassCache = false) {
console.info("Reloading viewport");
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 { randomRange } from "../math/random.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 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;
while (this.windowIds.includes(id.toString())) {

View file

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