diff --git a/package.json b/package.json index 4b5f729..759ac65 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,14 @@ "author": "Prozilla", "homepage": "https://os.prozilla.dev/", "repository": "https://github.com/Prozilla/Prozilla-OS", + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "predeploy": "npm run build", + "deploy": "sh deploy.sh" + }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-regular-svg-icons": "^6.4.0", @@ -23,13 +31,10 @@ "react-tabs": "^6.0.2", "web-vitals": "^2.1.4" }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", - "predeploy": "npm run build", - "deploy": "sh deploy.sh" + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "eslint-plugin-jsdoc": "^46.4.6", + "gh-pages": "^5.0.0" }, "eslintConfig": { "extends": [ @@ -98,10 +103,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "eslint-plugin-jsdoc": "^46.4.6", - "gh-pages": "^5.0.0" } } diff --git a/src/components/applications/file-explorer/FileExplorer.jsx b/src/components/applications/file-explorer/FileExplorer.jsx index 03a9009..9c5c17b 100644 --- a/src/components/applications/file-explorer/FileExplorer.jsx +++ b/src/components/applications/file-explorer/FileExplorer.jsx @@ -5,10 +5,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFile, faFileLines, faFolder, faHouse, faImage, faPlus, faSearch } from "@fortawesome/free-solid-svg-icons"; import { VirtualFile } from "../../../features/virtual-drive/virtualFile.js"; import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; -import utilStyles from "../../../styles/utils.module.css"; -// import { useContextMenu } from "../../../hooks/modals/ContextMenu.js"; +import { useContextMenu } from "../../../hooks/modals/contextMenu.js"; import { useModals } from "../../../hooks/modals/modals.js"; import { ModalsView } from "../../modals/ModalsView.jsx"; +import { QuickAccessButton } from "./QuickAccessButton.jsx"; +import { useDialogBox } from "../../../hooks/modals/dialogBox.js"; +import Vector2 from "../../../features/math/vector2.js"; +import { DIALOG_CONTENT_TYPES } from "../../modals/dialog-box/DialogBox.jsx"; /** * @param {object} props @@ -35,19 +38,36 @@ function FilePreview({ file }) { return preview; } -export function FileExplorer({ startPath }) { +export function FileExplorer({ startPath, app }) { const virtualRoot = useVirtualRoot(); const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~")); const [path, setPath] = useState(currentDirectory?.path ?? ""); const windowsManager = useWindowsManager(); const [showHidden] = useState(true); const [modalsManager, modals] = useModals(); - // const { onContextMenuFile } = useContextMenu({ - // modalsManager, - // options: { - // "Open": () => {} - // } - // }); + + const { onContextMenu: onContextMenuFile } = useContextMenu({ + modalsManager, + options: { + "Open": ({ file }) => { windowsManager.openFile(file); }, + "Delete": ({ file }) => { file.delete(); }, + } + }); + const { onContextMenu: onContextMenuFolder } = useContextMenu({ + modalsManager, + options: { + "Open": ({ name }) => { changeDirectory(name); }, + "Delete": ({ name }) => { currentDirectory.findSubFolder(name)?.delete(); } + } + }); + const { onContextMenu: onNew } = useContextMenu({ + modalsManager, + options: { + "File": () => { currentDirectory.createFile("New File"); }, + "Folder": () => { currentDirectory.createFolder("New Folder"); } + } + }); + const { onDialogBox } = useDialogBox({ modalsManager }); const changeDirectory = (path, absolute = false) => { if (currentDirectory == null) @@ -94,7 +114,25 @@ export function FileExplorer({ startPath }) { { changeDirectory(".."); }}> - + { + onDialogBox(event, { + app, + title: "Error", + size: new Vector2(300, 150), + children: <> + This folder is protected. + Ok + > + }); + + // if (currentDirectory.canBeEdited) { + // onNew(event); + // } else { + + // } + }} + > - { changeDirectory("~"); }}> - - Home - - { changeDirectory("~/Desktop"); }}> - - Desktop - - { changeDirectory("~/Documents"); }}> - - Documents - - { changeDirectory("~/Images"); }}> - - Images - + { changeDirectory("~"); }} icon={faHouse}/> + { changeDirectory("~/Desktop"); }} icon={faDesktop}/> + { changeDirectory("~/Documents"); }} icon={faFileLines}/> + { changeDirectory("~/Images"); }} icon={faImage}/> - + {currentDirectory?.getSubFolders(showHidden)?.map(({ name }, index) => { + onContextMenuFolder(event, { name }); + }} onClick={() => { changeDirectory(name); }} @@ -157,6 +174,9 @@ export function FileExplorer({ startPath }) { )} {currentDirectory?.getFiles(showHidden)?.map((file, index) => { + onContextMenuFile(event, { file }); + }} onClick={(event) => { event.preventDefault(); windowsManager.openFile(file); diff --git a/src/components/applications/file-explorer/QuickAccessButton.jsx b/src/components/applications/file-explorer/QuickAccessButton.jsx new file mode 100644 index 0000000..1dbd23a --- /dev/null +++ b/src/components/applications/file-explorer/QuickAccessButton.jsx @@ -0,0 +1,16 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import styles from "./FileExplorer.module.css"; +import utilStyles from "../../../styles/utils.module.css"; + +export function QuickAccessButton({ onClick, icon, name }) { + return ( + + + {name} + + ); +} \ No newline at end of file diff --git a/src/components/desktop/Desktop.jsx b/src/components/desktop/Desktop.jsx index 6adde1a..8c5632d 100644 --- a/src/components/desktop/Desktop.jsx +++ b/src/components/desktop/Desktop.jsx @@ -8,6 +8,7 @@ import { ModalsView } from "../modals/ModalsView.jsx"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; import { useContextMenu } from "../../hooks/modals/contextMenu.js"; import { FALLBACK_WALLPAPER } from "../../constants/desktop.js"; +import { reloadViewport } from "../../features/utils/browser.js"; export const Desktop = memo(() => { const settingsManager = useSettingsManager(); @@ -17,8 +18,9 @@ export const Desktop = memo(() => { const { onContextMenu } = useContextMenu({ modalsManager, options: { + "Refresh": () => { reloadViewport(); }, "Change appearance": () => { windowsManager.open("settings", { initialTabIndex: 0 }); }, - "Open in Files": () => { windowsManager.open("file-explorer", { startPath: "~/Desktop" }); } + "Open in Files": () => { windowsManager.open("file-explorer", { startPath: "~/Desktop" }); }, } }); diff --git a/src/components/modals/ModalView.jsx b/src/components/modals/ModalView.jsx index a427fea..e7d041c 100644 --- a/src/components/modals/ModalView.jsx +++ b/src/components/modals/ModalView.jsx @@ -2,17 +2,37 @@ import { memo } from "react"; import { Modal as ModalType } from "../../features/modals/modal.js"; import OutsideClickListener from "../../hooks/utils/outsideClick.js"; import styles from "./ModalView.module.css"; +import { useEffect } from "react"; /** * @param {object} root * @param {ModalType} root.modal */ export const ModalView = memo(({ modal }) => { - return ( - { modal.close(); }}> - - - - - ); + useEffect(() => { + const onDismiss = (event) => { + if (event.key === "Escape" && modal.dismissible) + modal.close(); + }; + + if (modal.dismissible) { + document.addEventListener("keydown", onDismiss); + } + + return () => { + document.removeEventListener("keydown", onDismiss); + }; + }, [modal]); + + const container = ( + + ); + + if (modal.dismissible) { + return ( { modal.close(); }}> + {container} + ); + } else { + return container; + } }); \ No newline at end of file diff --git a/src/components/modals/ModalView.module.css b/src/components/modals/ModalView.module.css index 8863b1a..bdcae67 100644 --- a/src/components/modals/ModalView.module.css +++ b/src/components/modals/ModalView.module.css @@ -8,7 +8,7 @@ } .Container > * { - background-color: var(--background-color-a); + background: var(--background-color-a); border-radius: 0.5rem; box-shadow: var(--window-box-shadow); } \ No newline at end of file diff --git a/src/components/modals/ModalsView.jsx b/src/components/modals/ModalsView.jsx index 88117cf..48c95bc 100644 --- a/src/components/modals/ModalsView.jsx +++ b/src/components/modals/ModalsView.jsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, useEffect, useRef } from "react"; import { Modal } from "../../features/modals/modal.js"; import ModalsManager from "../../features/modals/modals.js"; import { ModalView } from "./ModalView.jsx"; @@ -12,8 +12,15 @@ import styles from "./ModalsView.module.css"; * @param {import("react").className} root.className */ export const ModalsView = memo(({ modalsManager, modals, style, className }) => { + const ref = useRef(null); + + useEffect(() => { + if (modalsManager) + modalsManager.containerRef = ref; + }, [modalsManager, ref]); + return ( - + {modals?.map((modal) => )} diff --git a/src/components/modals/ModalsView.module.css b/src/components/modals/ModalsView.module.css index 0197d6b..e21f5ff 100644 --- a/src/components/modals/ModalsView.module.css +++ b/src/components/modals/ModalsView.module.css @@ -1,3 +1,4 @@ .Container { position: relative; + z-index: 9; } \ No newline at end of file diff --git a/src/components/modals/context-menu/ContextMenu.jsx b/src/components/modals/context-menu/ContextMenu.jsx index 35e6b3e..a774129 100644 --- a/src/components/modals/context-menu/ContextMenu.jsx +++ b/src/components/modals/context-menu/ContextMenu.jsx @@ -13,7 +13,7 @@ export function ContextMenu({ modal, options, shortcuts }) { {Object.entries(options).map(([label, callback]) => { modal.close(); - callback(); + callback(modal.props?.params); }}> {label} {shortcuts && Object.keys(shortcuts).includes(label) diff --git a/src/components/modals/context-menu/ContextMenu.module.css b/src/components/modals/context-menu/ContextMenu.module.css index 14da256..6e25281 100644 --- a/src/components/modals/context-menu/ContextMenu.module.css +++ b/src/components/modals/context-menu/ContextMenu.module.css @@ -1,7 +1,7 @@ .Container { padding: 0.5rem; - border-top-left-radius: 0; - background-color: var(--background-color-b); + border-top-left-radius: 0 !important; + background-color: var(--background-color-b) !important; } .Button { diff --git a/src/components/modals/dialog-box/DialogBox.jsx b/src/components/modals/dialog-box/DialogBox.jsx new file mode 100644 index 0000000..df342f7 --- /dev/null +++ b/src/components/modals/dialog-box/DialogBox.jsx @@ -0,0 +1,103 @@ +import { useEffect, useRef, useState } from "react"; +import styles from "./DialogBox.module.css"; +import Draggable from "react-draggable"; +import Vector2 from "../../../features/math/vector2.js"; +import { ReactSVG } from "react-svg"; +import utilStyles from "../../../styles/utils.module.css"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; + +export const DIALOG_CONTENT_TYPES = { + CloseButton: 0 +}; + +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); + + 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) + return; + + if (modal.size.x > screenWidth || modal.size.y > screenHeight) { + setStartPosition(new Vector2(0, 0)); + } else { + if (modal.position.x > screenWidth) { + modal.position.x = 0; + setStartPosition(modal.position); + } + if (modal.position.y > screenHeight) { + modal.position.y = 0; + setStartPosition(modal.position); + } + } + }, [initialised, modal, screenHeight, screenWidth]); + + const onClick = (event) => { + event.preventDefault(); + const type = parseInt(event.target.getAttribute("data-type")); + + switch (type) { + case DIALOG_CONTENT_TYPES.CloseButton: + modal.close(); + break; + } + }; + + return ( + + + + {title} + { modal.close(); }}> + + + + + {children} + + + ); +} \ No newline at end of file diff --git a/src/components/modals/dialog-box/DialogBox.module.css b/src/components/modals/dialog-box/DialogBox.module.css new file mode 100644 index 0000000..4a67e13 --- /dev/null +++ b/src/components/modals/dialog-box/DialogBox.module.css @@ -0,0 +1,111 @@ +.Container { + --header-height: 2.5rem; + --header-button-hover-color: rgba(255, 255, 255, 5%); + + position: absolute; + display: flex; + flex-direction: column; + min-width: 300px; + min-height: 150px; + background-color: var(--background-color-c); + border-radius: 0.5rem; + box-shadow: var(--window-box-shadow); + resize: both; + overflow: hidden; +} + +.Header { + --dialog-icon-size: 1.5rem; + --dialog-icon-margin: 0.75rem; + + display: flex; + align-items: center; + height: var(--header-height); + padding: 0.25rem; + padding-left: var(--dialog-icon-margin); + padding-right: 0; + background-color: var(--background-color-b); + cursor: grab; +} + +.Dialog-icon, +.Dialog-icon > div, +.Dialog-icon > div > svg { + height: 100%; + width: auto; +} + +.Dialog-icon { + height: var(--dialog-icon-size); + margin-right: calc(var(--dialog-icon-margin) - 0.1rem); +} + +.Dialog-icon > div { + display: flex; + align-items: center; +} + +.Header > p { + user-select: none; + width: auto; + margin: 0; + margin-right: auto; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.Header-button { + display: flex; + align-items: center; + justify-content: center; + height: var(--header-height); + margin: 0; + padding: 0.75rem; + color: var(--foreground-color-a); + background: none; + cursor: pointer; + border: none; + outline: none; + aspect-ratio: 1; +} + +.Header-button > svg { + height: 100%; +} + +.Exit-button { + --header-button-hover-color: var(--red-b); +} + +.Header-button:hover, .Header-button:focus-visible { + background-color: var(--header-button-hover-color); +} + +.Dialog-content { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + overflow: hidden; +} + +.Dialog-content button { + margin-bottom: 0.75rem; + padding: 0.5rem 1rem; + width: fit-content; + 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; +} + +.Dialog-content button:hover, .Dialog-content button:focus-visible { + background-color: var(--background-color-b); +} \ No newline at end of file diff --git a/src/components/task-bar/menus/HomeMenu.jsx b/src/components/task-bar/menus/HomeMenu.jsx index 07b31b9..ea04990 100644 --- a/src/components/task-bar/menus/HomeMenu.jsx +++ b/src/components/task-bar/menus/HomeMenu.jsx @@ -5,7 +5,7 @@ import { faCircleInfo, faFileLines, faGear, faImage, faPowerOff } from "@fortawe import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; import ApplicationsManager from "../../../features/applications/applications.js"; import { ReactSVG } from "react-svg"; -import { closeTab } from "../../../features/utils/browser.js"; +import { closeViewport } from "../../../features/utils/browser.js"; import { useKeyboardListener } from "../../../hooks/utils/keyboard.js"; import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js"; import { useEffect, useState } from "react"; @@ -61,7 +61,7 @@ export function HomeMenu({ active, setActive, search }) { - { closeTab(); }}> + { closeViewport(); }}> { diff --git a/src/components/windows/WindowView.jsx b/src/components/windows/WindowView.jsx index 27e7626..ac41ace 100644 --- a/src/components/windows/WindowView.jsx +++ b/src/components/windows/WindowView.jsx @@ -110,7 +110,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options, - + >} props.options * @param {Object>} props.shortcuts - * @returns {{ onContextMenu: Function }} + * @returns {{ onContextMenu: onContextMenuType }} */ export function useContextMenu({ modalsManager, options, shortcuts }) { - const onContextMenu = (event) => { - modalsManager.open(new Modal(ContextMenu) - .setPosition(new Vector2(event?.clientX ?? 0, event?.clientY ?? 0)) - .setProps({ options, shortcuts })); - }; + // Open a new modal when context menu is triggered + const onContextMenu = useCallback((event, params = {}) => { + let positionX = (event?.clientX ?? 0); + let positionY = (event?.clientY ?? 0); + + if (modalsManager.containerRef?.current) { + const containerRect = modalsManager.containerRef.current.getBoundingClientRect(); + positionX -= containerRect.x; + positionY -= containerRect.y / 2; + } + + const newModal = new Modal(ContextMenu) + .setPosition(new Vector2(positionX, positionY)) + .setProps({ options, shortcuts, params }); + + modalsManager.open(newModal); + return newModal; + }, [modalsManager, options, shortcuts]); useShortcuts({ options, shortcuts, useCategories: false }); diff --git a/src/hooks/modals/dialogBox.js b/src/hooks/modals/dialogBox.js new file mode 100644 index 0000000..15a0504 --- /dev/null +++ b/src/hooks/modals/dialogBox.js @@ -0,0 +1,31 @@ +import { useCallback } from "react"; +import Modal from "../../features/modals/modal.js"; +import { DialogBox } from "../../components/modals/dialog-box/DialogBox.jsx"; +import Vector2 from "../../features/math/vector2.js"; + +const DEFAULT_SIZE = new Vector2(400, 200); + +export function useDialogBox({ modalsManager }) { + const onDialogBox = useCallback((event, params = {}) => { + const size = params.size ?? DEFAULT_SIZE; + let positionX = (window.innerWidth - size.x) / 4; + let positionY = (window.innerHeight - size.y) / 4; + + if (modalsManager.containerRef?.current) { + const containerRect = modalsManager.containerRef.current.getBoundingClientRect(); + positionX -= containerRect.x / 2; + positionY -= containerRect.y / 2; + } + + const newModal = new Modal(DialogBox) + .setPosition(new Vector2(positionX, positionY)) + .setSize(size) + .setDismissible(false) + .setProps({ params }); + + modalsManager.open(newModal); + return newModal; + }, [modalsManager]); + + return { onDialogBox }; +} \ No newline at end of file
This folder is protected.
{label}
{title}