Added actions
This commit is contained in:
parent
d73db00326
commit
5263cfd2b6
26 changed files with 460 additions and 209 deletions
|
|
@ -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",
|
||||
|
|
|
|||
121
src/components/actions/Actions.jsx
Normal file
121
src/components/actions/Actions.jsx
Normal 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>);
|
||||
}
|
||||
91
src/components/actions/Actions.module.css
Normal file
91
src/components/actions/Actions.module.css
Normal 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);
|
||||
}
|
||||
0
src/components/actions/actions/Action.module.css
Normal file
0
src/components/actions/actions/Action.module.css
Normal file
13
src/components/actions/actions/ClickAction.jsx
Normal file
13
src/components/actions/actions/ClickAction.jsx
Normal 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>);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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/>;
|
||||
}
|
||||
});
|
||||
|
|
@ -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}/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.Container {
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
z-index: 11;
|
||||
}
|
||||
|
|
@ -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>);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
</>);
|
||||
}, []);
|
||||
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
1
src/constants/taskBar.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const TASK_BAR_HEIGHT = 3 * 16;
|
||||
|
|
@ -1,2 +1 @@
|
|||
export const SCREEN_MARGIN = 32;
|
||||
export const TASKBAR_HEIGHT = 48;
|
||||
export const SCREEN_MARGIN = 32;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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
20
src/hooks/utils/screen.js
Normal 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];
|
||||
}
|
||||
Loading…
Reference in a new issue