diff --git a/src/components/applications/file-explorer/FileExplorer.jsx b/src/components/applications/file-explorer/FileExplorer.jsx index 0c6d094..accae59 100644 --- a/src/components/applications/file-explorer/FileExplorer.jsx +++ b/src/components/applications/file-explorer/FileExplorer.jsx @@ -2,16 +2,22 @@ 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, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUp, faCaretLeft, faCaretRight, faCircleInfo, 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"; -import { useDialogBox } from "../../../hooks/modals/dialogBox.js"; +import { useWindowedModal } from "../../../hooks/modals/windowedModal.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"; +import utilStyles from "../../../styles/utils.module.css"; +import { DialogBox } from "../../modals/dialog-box/DialogBox.jsx"; +import AppsManager from "../../../features/applications/applications.js"; +import { APPS } from "../../../constants/applications.js"; +import { TITLE_SEPARATOR } from "../../../constants/windows.js"; +import { FileProperties } from "../../modals/file-properties/FileProperties.jsx"; /** * @param {import("../../windows/WindowView.jsx").windowProps} props @@ -23,6 +29,7 @@ export function FileExplorer({ startPath, app, modalsManager }) { const windowsManager = useWindowsManager(); const [showHidden] = useState(true); + const { openWindowedModal } = useWindowedModal({ modalsManager }); const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) => { @@ -31,6 +38,24 @@ export function FileExplorer({ startPath, app, modalsManager }) { { file.delete(); }}/> + { + openWindowedModal({ + title: `${file.id} ${TITLE_SEPARATOR} Properties`, + iconUrl: file.getIconUrl(), + size: new Vector2(400, 500), + Modal: (props) => + + }); + // modalsManager.open(new Modal(FileProperties) + // .setPosition(new Vector2(positionX, positionY)) + // .setProps({ + // triggerParams: params, + // className: STYLES.CONTEXT_MENU, + // onAnyTrigger: () => { + // newModal.close(); + // } + // })); + }}/> }); const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) => @@ -50,7 +75,6 @@ export function FileExplorer({ startPath, app, modalsManager }) { // "Folder": () => { currentDirectory.createFolder("New Folder"); } // } // }); - const { onDialogBox } = useDialogBox({ modalsManager }); const changeDirectory = (path, absolute = false) => { if (currentDirectory == null) @@ -84,6 +108,8 @@ export function FileExplorer({ startPath, app, modalsManager }) { } }; + const itemCount = currentDirectory.getItemCount(showHidden); + return (
@@ -97,15 +123,16 @@ export function FileExplorer({ startPath, app, modalsManager }) { - + Modal: (props) => + +

This folder is protected.

+ +
}); // if (currentDirectory.canBeEdited) { @@ -156,6 +183,14 @@ export function FileExplorer({ startPath, app, modalsManager }) { />
+ +

+ {itemCount === 1 + ? itemCount + " item" + : itemCount + " items" + } +

+
); } \ No newline at end of file diff --git a/src/components/applications/file-explorer/FileExplorer.module.css b/src/components/applications/file-explorer/FileExplorer.module.css index 44c2b19..94cdb7a 100644 --- a/src/components/applications/file-explorer/FileExplorer.module.css +++ b/src/components/applications/file-explorer/FileExplorer.module.css @@ -112,6 +112,7 @@ } .Main { + position: relative; flex: 1; display: flex; flex-wrap: wrap; @@ -119,4 +120,23 @@ height: 100%; padding: 0.5rem; overflow: auto; +} + +.Footer { + position: absolute; + display: flex; + justify-content: flex-start; + align-items: center; + bottom: 0; + left: 0; + width: 100%; + height: 1.75rem; + padding: 0 0.75rem; + border-top: 0.25rem solid var(--background-color-a); + background-color: var(--background-color-c); +} + +.Footer p { + margin: 0; + font-size: 0.875em; } \ No newline at end of file diff --git a/src/components/applications/file-explorer/directory-list/DirectoryList.jsx b/src/components/applications/file-explorer/directory-list/DirectoryList.jsx index 6df84cd..bc67dff 100644 --- a/src/components/applications/file-explorer/directory-list/DirectoryList.jsx +++ b/src/components/applications/file-explorer/directory-list/DirectoryList.jsx @@ -1,8 +1,7 @@ import { VirtualFile } from "../../../../features/virtual-drive/virtualFile.js"; import { VirtualFolder } from "../../../../features/virtual-drive/virtualFolder.js"; -import { FileIcon } from "./FileIcon.jsx"; -import { FolderIcon } from "./FolderIcon.jsx"; import styles from "./DirectoryList.module.css"; +import { ImagePreview } from "./ImagePreview.jsx"; /** * @callback fileEvent @@ -53,7 +52,9 @@ export function DirectoryList({ directory, showHidden = false, folderClassname, onClickFolder?.(event, folder); }} > - +
+ +

{folder.name}

)} @@ -69,7 +70,9 @@ export function DirectoryList({ directory, showHidden = false, folderClassname, onClickFile?.(event, file); }} > - +
+ +

{file.id}

)} diff --git a/src/components/applications/file-explorer/directory-list/FileIcon.jsx b/src/components/applications/file-explorer/directory-list/FileIcon.jsx deleted file mode 100644 index e54ee45..0000000 --- a/src/components/applications/file-explorer/directory-list/FileIcon.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import styles from "./DirectoryList.module.css"; -import { VirtualFile } from "../../../../features/virtual-drive/virtualFile.js"; -import { ImagePreview } from "./ImagePreview.jsx"; -import AppsManager from "../../../../features/applications/applications.js"; -import { APPS } from "../../../../constants/applications.js"; - -/** - * @param {object} props - * @param {VirtualFile} props.file - * @param {string} props.iconUrl - */ -export function FileIcon({ file, iconUrl }) { - let preview = null; - - if (iconUrl && file.extension !== "png") { - preview = ; - } else { - switch (file.extension) { - case "png": - preview = ; - break; - case "txt": - case "md": - preview = ; - break; - case "xml": - preview = ; - break; - default: - preview = ; - break; - } - } - - return (
- {preview} -
); -} \ No newline at end of file diff --git a/src/components/applications/file-explorer/directory-list/FolderIcon.jsx b/src/components/applications/file-explorer/directory-list/FolderIcon.jsx deleted file mode 100644 index b159e14..0000000 --- a/src/components/applications/file-explorer/directory-list/FolderIcon.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import styles from "./DirectoryList.module.css"; -import { VirtualFolder } from "../../../../features/virtual-drive/virtualFolder.js"; -import { ImagePreview } from "./ImagePreview.jsx"; -import AppsManager from "../../../../features/applications/applications.js"; -import { APPS } from "../../../../constants/applications.js"; - -/** - * @param {object} props - * @param {VirtualFolder} props.folder - * @param {string} props.iconUrl - */ -export function FolderIcon({ folder, iconUrl }) { - let preview = null; - - if (iconUrl) { - preview = ; - } else if (folder.linkedPath) { - preview = ; - } else { - preview = ; - } - - return (
- {preview} -
); -} \ No newline at end of file diff --git a/src/components/applications/file-explorer/directory-list/ImagePreview.jsx b/src/components/applications/file-explorer/directory-list/ImagePreview.jsx index d9d62ca..65e83d3 100644 --- a/src/components/applications/file-explorer/directory-list/ImagePreview.jsx +++ b/src/components/applications/file-explorer/directory-list/ImagePreview.jsx @@ -4,13 +4,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFile } from "@fortawesome/free-solid-svg-icons"; import { ReactSVG } from "react-svg"; -export function ImagePreview({ source }) { +export function ImagePreview({ source, className, ...props }) { const [loadingFailed, setLoadingFailed] = useState(false); if (loadingFailed) return ; - return (
+ const classNames = [styles["Image-preview"]]; + if (className != null) + classNames.push(className); + + return (
{source.endsWith(".svg") ? : { setLoadingFailed(true); }} alt="Preview" draggable="false"/> diff --git a/src/components/applications/text-editor/TextEditor.jsx b/src/components/applications/text-editor/TextEditor.jsx index 66af1e3..27afba5 100644 --- a/src/components/applications/text-editor/TextEditor.jsx +++ b/src/components/applications/text-editor/TextEditor.jsx @@ -4,6 +4,7 @@ import { HeaderMenu } from "../.common/HeaderMenu.jsx"; import Markdown from "markdown-to-jsx"; import { DEFAULT_ZOOM, ZOOM_FACTOR } from "../../../constants/applications/textEditor.js"; import AppsManager from "../../../features/applications/applications.js"; +import { TITLE_SEPARATOR } from "../../../constants/windows.js"; /** * @param {import("../../windows/WindowView.jsx").windowProps} props @@ -52,7 +53,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app }) { if (currentMode === "view") label += " (preview)"; - setTitle(`${label} - ${app.name}`); + setTitle(`${label} ${TITLE_SEPARATOR} ${app.name}`); }, [currentFile, setTitle, unsavedChanges, currentMode, app.name]); const newText = () => { diff --git a/src/components/modals/.templates/WindowedModal.jsx b/src/components/modals/.templates/WindowedModal.jsx new file mode 100644 index 0000000..62b9512 --- /dev/null +++ b/src/components/modals/.templates/WindowedModal.jsx @@ -0,0 +1,76 @@ +import { useEffect, useRef, useState } from "react"; +import { useScreenDimensions } from "../../../hooks/utils/screen.js"; +import Vector2 from "../../../features/math/vector2.js"; +import styles from "./WindowedModal.module.css"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import Draggable from "react-draggable"; +import { ReactSVG } from "react-svg"; +import utilStyles from "../../../styles/utils.module.css"; + +export function WindowedModal({ modal, params, children, ...props }) { + const { iconUrl, title } = params; + + const nodeRef = useRef(null); + + const [startPosition, setStartPosition] = useState(modal.position); + const [screenWidth, screenHeight] = useScreenDimensions(); + + useEffect(() => { + if (screenWidth == null || screenHeight == null) + 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); + } + } + }, [modal, screenHeight, screenWidth]); + + return ( +
+
+ +

{title}

+ +
+
+ {children} +
+
+
); +} \ No newline at end of file diff --git a/src/components/modals/.templates/WindowedModal.module.css b/src/components/modals/.templates/WindowedModal.module.css new file mode 100644 index 0000000..f37055a --- /dev/null +++ b/src/components/modals/.templates/WindowedModal.module.css @@ -0,0 +1,110 @@ +.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) !important; + resize: both; + overflow: hidden; +} + +.Header { + --window-icon-size: 1.5rem; + --window-icon-margin: 0.75rem; + + display: flex; + align-items: center; + height: var(--header-height); + padding: 0.25rem; + padding-left: var(--window-icon-margin); + padding-right: 0; + background-color: var(--background-color-b); + cursor: grab; +} + +.Window-icon, +.Window-icon > div, +.Window-icon > div > svg { + height: 100%; + width: auto; +} + +.Window-icon { + height: var(--window-icon-size); + margin-right: calc(var(--window-icon-margin) - 0.1rem); +} + +.Window-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); +} + +.Window-content { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + overflow: hidden; +} + +.Window-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; +} + +.Window-content button:hover, +.Window-content button:focus-visible { + background-color: var(--background-color-b); +} \ No newline at end of file diff --git a/src/components/modals/dialog-box/DialogBox.jsx b/src/components/modals/dialog-box/DialogBox.jsx index 32126d7..6fae257 100644 --- a/src/components/modals/dialog-box/DialogBox.jsx +++ b/src/components/modals/dialog-box/DialogBox.jsx @@ -1,40 +1,7 @@ -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"; 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 [startPosition, setStartPosition] = useState(modal.position); - const [screenWidth, screenHeight] = useScreenDimensions(); - - useEffect(() => { - if (screenWidth == null || screenHeight == null) - 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); - } - } - }, [modal, screenHeight, screenWidth]); +import { WindowedModal } from "../.templates/WindowedModal.jsx"; +export function DialogBox({ modal, params, children, ...props }) { const onClick = (event) => { event.preventDefault(); const type = parseInt(event.target.getAttribute("data-type")); @@ -46,43 +13,7 @@ export function DialogBox({ modal, params }) { } }; - return ( -
-
- -

{title}

- -
-
- {children} -
-
-
); + return + {children} + ; } \ No newline at end of file diff --git a/src/components/modals/file-properties/FileProperties.jsx b/src/components/modals/file-properties/FileProperties.jsx new file mode 100644 index 0000000..abaf2f5 --- /dev/null +++ b/src/components/modals/file-properties/FileProperties.jsx @@ -0,0 +1,41 @@ +import { ImagePreview } from "../../applications/file-explorer/directory-list/ImagePreview.jsx"; +import { WindowedModal } from "../.templates/WindowedModal.jsx"; +import styles from "./FileProperties.module.css"; +import utilStyles from "../../../styles/utils.module.css"; +import Modal from "../../../features/modals/modal.js"; +import { VirtualFile } from "../../../features/virtual-drive/virtualFile.js"; +import { StorageManager } from "../../../features/storage/storageManager.js"; +import AppsManager from "../../../features/applications/applications.js"; + +/** + * @param {object} props + * @param {Modal} props.modal + * @param {object} props.params + * @param {VirtualFile} props.file + */ +export function FileProperties({ modal, params, file, ...props }) { + const associatedApp = AppsManager.getFileApp(file.extension); + + return + + +

{file.id}

+
+ +

Type: {file.getType()}

+

+ Opens with: + + {associatedApp.name} +

+
+ +

Location: {file.path}

+

Size: {StorageManager.getByteSize(file.source ?? file.content)} bytes

+

Size on drive: {StorageManager.getByteSize(file.toString())} bytes

+
+ +

Attributes: {file.isProtected ? "Protected" : "N/A"}

+
+
; +} \ No newline at end of file diff --git a/src/components/modals/file-properties/FileProperties.module.css b/src/components/modals/file-properties/FileProperties.module.css new file mode 100644 index 0000000..80adc48 --- /dev/null +++ b/src/components/modals/file-properties/FileProperties.module.css @@ -0,0 +1,90 @@ +.Container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + padding: 1rem; +} + +.Icon { + width: 3rem; + height: 3rem; +} + +.Icon div { + width: inherit; + height: inherit; + object-fit: contain; +} + +.Icon svg { + max-width: 100%; + max-height: 100%; +} + +.Section { + --divider-width: 0.25rem; + + position: relative; + display: flex; + gap: 0.25rem; + justify-content: flex-start; + align-items: flex-start; + flex-direction: column; + width: 100%; + padding: 1rem 0; + padding-bottom: calc(1rem + var(--divider-width)); +} + +.Section:first-child { + gap: 0.5rem; + align-items: center; + flex-direction: row; + padding-top: 0; +} + +.Section:first-child .Line { + font-size: 1.25rem; +} + +.Section:not(:last-child)::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: var(--divider-width); + border-radius: 0.25rem; + background-color: var(--background-color-a); +} + +.Line { + margin: 0; + text-align: left; + padding-left: 2rem; + text-indent: -2rem; +} + +.App-icon { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: middle; + margin-left: 0.5rem; + margin-right: 0.25rem; +} + +.App-icon div { + position: relative; + width: inherit; + height: inherit; + object-fit: contain; +} + +.App-icon svg { + position: absolute; + top: 0; + left: 0; + max-width: 100%; + max-height: 100%; +} \ No newline at end of file diff --git a/src/components/taskbar/Taskbar.jsx b/src/components/taskbar/Taskbar.jsx index 8fa0106..937016c 100644 --- a/src/components/taskbar/Taskbar.jsx +++ b/src/components/taskbar/Taskbar.jsx @@ -66,6 +66,7 @@ export const Taskbar = memo(() => { const isActive = windows.map((window) => window.app.id).includes(app.id); const shouldBeShown = (pins.includes(app.id) || isActive); return ( { active={isActive} visible={shouldBeShown} />); - }), [modalsManager, pins, windows]); + }), [modalsManager, pins, windows, windowsManager]); useEffect(() => { (async () => { diff --git a/src/components/taskbar/app-icon/AppIcon.jsx b/src/components/taskbar/app-icon/AppIcon.jsx index c2d1044..5c48071 100644 --- a/src/components/taskbar/app-icon/AppIcon.jsx +++ b/src/components/taskbar/app-icon/AppIcon.jsx @@ -1,6 +1,5 @@ import { memo } from "react"; import Application from "../../../features/applications/application.js"; -import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; import styles from "./AppIcon.module.css"; import { ReactSVG } from "react-svg"; import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext.js"; @@ -19,10 +18,9 @@ import { removeFromArray } from "../../../features/utils/array.js"; * @param {string[]} props.pins * @param {boolean} props.focused */ -export const AppButton = memo(({ app, modalsManager, pins, active, visible }) => { +export const AppButton = memo(({ app, windowsManager, modalsManager, pins, active, visible }) => { const isPinned = pins.includes(app.id); - const windowsManager = useWindowsManager(); const settingsManager = useSettingsManager(); const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) => @@ -49,6 +47,9 @@ export const AppButton = memo(({ app, modalsManager, pins, active, visible }) => if (!visible) classNames.push(styles.Hidden); + if (!windowsManager) + return; + return (