Added file properties modal
This commit is contained in:
parent
b7bc57d9ce
commit
7334b6ceb5
23 changed files with 558 additions and 174 deletions
|
|
@ -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) =>
|
||||
<Actions {...props}>
|
||||
<ClickAction label="Open" onTrigger={(event, file) => {
|
||||
|
|
@ -31,6 +38,24 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
|||
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
|
||||
file.delete();
|
||||
}}/>
|
||||
<ClickAction label="Properties" icon={faCircleInfo} onTrigger={(event, file) => {
|
||||
openWindowedModal({
|
||||
title: `${file.id} ${TITLE_SEPARATOR} Properties`,
|
||||
iconUrl: file.getIconUrl(),
|
||||
size: new Vector2(400, 500),
|
||||
Modal: (props) =>
|
||||
<FileProperties file={file} {...props}/>
|
||||
});
|
||||
// modalsManager.open(new Modal(FileProperties)
|
||||
// .setPosition(new Vector2(positionX, positionY))
|
||||
// .setProps({
|
||||
// triggerParams: params,
|
||||
// className: STYLES.CONTEXT_MENU,
|
||||
// onAnyTrigger: () => {
|
||||
// newModal.close();
|
||||
// }
|
||||
// }));
|
||||
}}/>
|
||||
</Actions>
|
||||
});
|
||||
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 (
|
||||
<div className={styles.Container}>
|
||||
<div className={styles.Header}>
|
||||
|
|
@ -97,15 +123,16 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
|||
<FontAwesomeIcon icon={faArrowUp}/>
|
||||
</button>
|
||||
<button title="New" tabIndex={0} className={styles["Icon-button"]}
|
||||
onClick={(event) => {
|
||||
onDialogBox(event, {
|
||||
app,
|
||||
onClick={() => {
|
||||
openWindowedModal({
|
||||
title: "Error",
|
||||
iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER),
|
||||
size: new Vector2(300, 150),
|
||||
children: <>
|
||||
<p>This folder is protected.</p>
|
||||
<button data-type={DIALOG_CONTENT_TYPES.CloseButton}>Ok</button>
|
||||
</>
|
||||
Modal: (props) =>
|
||||
<DialogBox {...props}>
|
||||
<p>This folder is protected.</p>
|
||||
<button data-type={DIALOG_CONTENT_TYPES.CloseButton}>Ok</button>
|
||||
</DialogBox>
|
||||
});
|
||||
|
||||
// if (currentDirectory.canBeEdited) {
|
||||
|
|
@ -156,6 +183,14 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className={styles.Footer}>
|
||||
<p className={utilStyles["Text-light"]}>
|
||||
{itemCount === 1
|
||||
? itemCount + " item"
|
||||
: itemCount + " items"
|
||||
}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}}
|
||||
>
|
||||
<FolderIcon folder={folder} iconUrl={folder.getIconUrl()}/>
|
||||
<div className={styles["Folder-icon"]}>
|
||||
<ImagePreview source={folder.getIconUrl()}/>
|
||||
</div>
|
||||
<p>{folder.name}</p>
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -69,7 +70,9 @@ export function DirectoryList({ directory, showHidden = false, folderClassname,
|
|||
onClickFile?.(event, file);
|
||||
}}
|
||||
>
|
||||
<FileIcon file={file} iconUrl={file.getIconUrl()}/>
|
||||
<div className={styles["File-icon"]}>
|
||||
<ImagePreview source={file.getIconUrl()}/>
|
||||
</div>
|
||||
<p>{file.id}</p>
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 = <ImagePreview source={iconUrl}/>;
|
||||
} else {
|
||||
switch (file.extension) {
|
||||
case "png":
|
||||
preview = <ImagePreview source={file.source}/>;
|
||||
break;
|
||||
case "txt":
|
||||
case "md":
|
||||
preview = <ImagePreview source={AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file-text")}/>;
|
||||
break;
|
||||
case "xml":
|
||||
preview = <ImagePreview source={AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file-code")}/>;
|
||||
break;
|
||||
default:
|
||||
preview = <ImagePreview source={AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file")}/>;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (<div className={styles["File-icon"]}>
|
||||
{preview}
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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 = <ImagePreview source={iconUrl}/>;
|
||||
} else if (folder.linkedPath) {
|
||||
preview = <ImagePreview source={AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder-link")}/>;
|
||||
} else {
|
||||
preview = <ImagePreview source={AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder")}/>;
|
||||
}
|
||||
|
||||
return (<div className={styles["Folder-icon"]}>
|
||||
{preview}
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -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 <FontAwesomeIcon icon={faFile}/>;
|
||||
|
||||
return (<div className={styles["Image-preview"]}>
|
||||
const classNames = [styles["Image-preview"]];
|
||||
if (className != null)
|
||||
classNames.push(className);
|
||||
|
||||
return (<div className={classNames.join(" ")} {...props}>
|
||||
{source.endsWith(".svg")
|
||||
? <ReactSVG src={source}/>
|
||||
: <img src={source} onError={() => { setLoadingFailed(true); }} alt="Preview" draggable="false"/>
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
76
src/components/modals/.templates/WindowedModal.jsx
Normal file
76
src/components/modals/.templates/WindowedModal.jsx
Normal file
|
|
@ -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 (<Draggable
|
||||
axis="both"
|
||||
handle={".Window-handle"}
|
||||
defaultPosition={startPosition}
|
||||
position={null}
|
||||
scale={1}
|
||||
bounds={{
|
||||
top: -modal.position.y - 1,
|
||||
bottom: screenHeight - 55 - modal.position.y,
|
||||
left: -modal.size.x + 85 - modal.position.x,
|
||||
right: screenWidth - 5 - modal.position.x
|
||||
}}
|
||||
cancel="button"
|
||||
nodeRef={nodeRef}
|
||||
>
|
||||
<div
|
||||
className={styles.Container}
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
width: modal.size.x,
|
||||
height: modal.size.y,
|
||||
}}
|
||||
>
|
||||
<div className={`${styles.Header} Window-handle`}>
|
||||
<ReactSVG
|
||||
className={styles["Window-icon"]}
|
||||
src={iconUrl}
|
||||
/>
|
||||
<p className={utilStyles["Text-semibold"]}>{title}</p>
|
||||
<button aria-label="Close" className={`${styles["Header-button"]} ${styles["Exit-button"]}`} tabIndex={0}
|
||||
onClick={() => { modal.close(); }}>
|
||||
<FontAwesomeIcon icon={faXmark}/>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles["Window-content"]} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>);
|
||||
}
|
||||
110
src/components/modals/.templates/WindowedModal.module.css
Normal file
110
src/components/modals/.templates/WindowedModal.module.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 (<Draggable
|
||||
axis="both"
|
||||
handle={".Dialog-handle"}
|
||||
defaultPosition={startPosition}
|
||||
position={null}
|
||||
scale={1}
|
||||
bounds={{
|
||||
top: -modal.position.y - 1,
|
||||
bottom: screenHeight - 55 - modal.position.y,
|
||||
left: -modal.size.x + 85 - modal.position.x,
|
||||
right: screenWidth - 5 - modal.position.x
|
||||
}}
|
||||
cancel="button"
|
||||
nodeRef={nodeRef}
|
||||
>
|
||||
<div
|
||||
className={styles.Container}
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
width: modal.size.x,
|
||||
height: modal.size.y,
|
||||
}}
|
||||
>
|
||||
<div className={`${styles.Header} Dialog-handle`}>
|
||||
<ReactSVG
|
||||
className={styles["Dialog-icon"]}
|
||||
src={process.env.PUBLIC_URL + `/assets/applications/icons/${app.id}.svg`}
|
||||
/>
|
||||
<p className={utilStyles["Text-semibold"]}>{title}</p>
|
||||
<button aria-label="Close" className={`${styles["Header-button"]} ${styles["Exit-button"]}`} tabIndex={0} id="close-dialog"
|
||||
onClick={() => { modal.close(); }}>
|
||||
<FontAwesomeIcon icon={faXmark}/>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles["Dialog-content"]} onClick={onClick}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>);
|
||||
return <WindowedModal modal={modal} params={params} onClick={onClick} {...props}>
|
||||
{children}
|
||||
</WindowedModal>;
|
||||
}
|
||||
41
src/components/modals/file-properties/FileProperties.jsx
Normal file
41
src/components/modals/file-properties/FileProperties.jsx
Normal file
|
|
@ -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 <WindowedModal className={styles.Container} modal={modal} params={params} {...props}>
|
||||
<span className={styles["Section"]}>
|
||||
<ImagePreview className={styles["Icon"]} source={file.getIconUrl()}/>
|
||||
<p className={`${styles["Line"]} ${utilStyles["Text-bold"]}`}>{file.id}</p>
|
||||
</span>
|
||||
<span className={styles["Section"]}>
|
||||
<p className={styles["Line"]}>Type: {file.getType()}</p>
|
||||
<p className={styles["Line"]}>
|
||||
Opens with:
|
||||
<ImagePreview className={styles["App-icon"]} source={AppsManager.getAppIconUrl(associatedApp.id)}/>
|
||||
{associatedApp.name}
|
||||
</p>
|
||||
</span>
|
||||
<span className={styles["Section"]}>
|
||||
<p className={styles["Line"]}>Location: {file.path}</p>
|
||||
<p className={styles["Line"]}>Size: {StorageManager.getByteSize(file.source ?? file.content)} bytes</p>
|
||||
<p className={styles["Line"]}>Size on drive: {StorageManager.getByteSize(file.toString())} bytes</p>
|
||||
</span>
|
||||
<span className={styles["Section"]}>
|
||||
<p className={styles["Line"]}>Attributes: {file.isProtected ? "Protected" : "N/A"}</p>
|
||||
</span>
|
||||
</WindowedModal>;
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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 (<AppButton
|
||||
windowsManager={windowsManager}
|
||||
modalsManager={modalsManager}
|
||||
pins={pins}
|
||||
app={app}
|
||||
|
|
@ -73,7 +74,7 @@ export const Taskbar = memo(() => {
|
|||
active={isActive}
|
||||
visible={shouldBeShown}
|
||||
/>);
|
||||
}), [modalsManager, pins, windows]);
|
||||
}), [modalsManager, pins, windows, windowsManager]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
<Actions avoidTaskbar={false} {...props}>
|
||||
|
|
@ -49,6 +47,9 @@ export const AppButton = memo(({ app, modalsManager, pins, active, visible }) =>
|
|||
if (!visible)
|
||||
classNames.push(styles.Hidden);
|
||||
|
||||
if (!windowsManager)
|
||||
return;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={app.id}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export const SCREEN_MARGIN = 32;
|
||||
export const SCREEN_MARGIN = 32;
|
||||
export const TITLE_SEPARATOR = "-";
|
||||
|
|
@ -107,4 +107,9 @@ export class VirtualFileLink extends VirtualFile {
|
|||
read(...args) {
|
||||
if (this.isValid()) return this.linkedFile.read(...args);
|
||||
}
|
||||
|
||||
/** @type {VirtualFile["getIconUrl"]} */
|
||||
getIconUrl(...args) {
|
||||
if (this.isValid()) return this.iconUrl ?? this.linkedFile.getIconUrl(...args);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { APPS } from "../../constants/applications.js";
|
||||
import AppsManager from "../applications/applications.js";
|
||||
import { VirtualFolder } from "./virtualFolder.js";
|
||||
|
||||
/**
|
||||
|
|
@ -59,6 +61,16 @@ export class VirtualFolderLink extends VirtualFolder {
|
|||
return (this.linkedFolder != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getIconUrl() {
|
||||
if (this.iconUrl != null)
|
||||
return this.iconUrl;
|
||||
|
||||
return AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder-link");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object | null}
|
||||
*/
|
||||
|
|
@ -135,4 +147,9 @@ export class VirtualFolderLink extends VirtualFolder {
|
|||
open(...args) {
|
||||
if (this.isValid()) return this.linkedFolder.open(...args);
|
||||
}
|
||||
|
||||
/** @type {VirtualFolder["getItemCount"]} */
|
||||
getItemCount(...args) {
|
||||
if (this.isValid()) return this.linkedFolder.getItemCount(...args);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { APPS } from "../../constants/applications.js";
|
||||
import AppsManager from "../applications/applications.js";
|
||||
import { EventEmitter } from "../utils/events.js";
|
||||
import { VirtualRoot } from "./virtualRoot.js";
|
||||
|
||||
|
|
@ -86,9 +88,19 @@ export class VirtualBase extends EventEmitter {
|
|||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getIconUrl = () => {
|
||||
return this.iconUrl ?? this.linkedFile?.iconUrl ?? this.linkedFolder?.iconUrl;
|
||||
};
|
||||
getIconUrl() {
|
||||
return this.iconUrl
|
||||
?? this.linkedFile?.iconUrl
|
||||
?? this.linkedFolder?.iconUrl
|
||||
?? AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getType() {
|
||||
return "None";
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (!this.canBeEdited)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { APPS } from "../../constants/applications.js";
|
||||
import AppsManager from "../applications/applications.js";
|
||||
import WindowsManager from "../windows/windows.js";
|
||||
import { VirtualBase } from "./virtualBase.js";
|
||||
|
||||
|
|
@ -109,6 +111,58 @@ export class VirtualFile extends VirtualBase {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getIconUrl() {
|
||||
if (this.iconUrl != null)
|
||||
return this.iconUrl;
|
||||
|
||||
let iconUrl = null;
|
||||
|
||||
switch (this.extension) {
|
||||
case "png":
|
||||
iconUrl = this.source;
|
||||
break;
|
||||
case "txt":
|
||||
case "md":
|
||||
iconUrl = AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file-text");
|
||||
break;
|
||||
case "xml":
|
||||
iconUrl = AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file-code");
|
||||
break;
|
||||
default:
|
||||
iconUrl = AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file");
|
||||
break;
|
||||
}
|
||||
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getType() {
|
||||
let type = "";
|
||||
|
||||
switch (this.extension) {
|
||||
case "png":
|
||||
type = "PNG Image";
|
||||
break;
|
||||
case "txt":
|
||||
type = "Text";
|
||||
break;
|
||||
case "md":
|
||||
type = "Markdown source";
|
||||
break;
|
||||
case "xml":
|
||||
type = "XML source";
|
||||
break;
|
||||
}
|
||||
|
||||
return `${type} file (.${this.extension.toLowerCase()})`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object | null}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { APPS } from "../../constants/applications.js";
|
||||
import AppsManager from "../applications/applications.js";
|
||||
import { removeFromArray } from "../utils/array.js";
|
||||
import WindowsManager from "../windows/windows.js";
|
||||
import { VirtualFileLink } from "./VirtualFileLink.js";
|
||||
|
|
@ -368,7 +369,7 @@ export class VirtualFolder extends VirtualBase {
|
|||
/**
|
||||
* Returns all files inside this folder
|
||||
* @param {Boolean=false} showHidden
|
||||
* @returns {Array<VirtualFile>}
|
||||
* @returns {VirtualFile[]}
|
||||
*/
|
||||
getFiles(showHidden = false) {
|
||||
if (showHidden)
|
||||
|
|
@ -380,9 +381,9 @@ export class VirtualFolder extends VirtualBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns all subfolders inside this folder
|
||||
* Returns all sub-folders inside this folder
|
||||
* @param {Boolean=false} showHidden
|
||||
* @returns {Array<VirtualFolder>}
|
||||
* @returns {VirtualFolder[]}
|
||||
*/
|
||||
getSubFolders(showHidden = false) {
|
||||
if (showHidden)
|
||||
|
|
@ -393,6 +394,18 @@ export class VirtualFolder extends VirtualBase {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of files and sub-folders inside this folder
|
||||
* @param {Boolean=false} showHidden
|
||||
* @returns {number}
|
||||
*/
|
||||
getItemCount(showHidden = false) {
|
||||
const filesCount = this.getFiles(showHidden)?.length ?? 0;
|
||||
const foldersCount = this.getSubFolders(showHidden)?.length ?? 0;
|
||||
|
||||
return filesCount + foldersCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
|
@ -400,6 +413,16 @@ export class VirtualFolder extends VirtualBase {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getIconUrl() {
|
||||
if (this.iconUrl != null)
|
||||
return this.iconUrl;
|
||||
|
||||
return AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object | null}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ModalsManager from "../../features/modals/modals.js";
|
|||
import { STYLES } from "../../components/actions/Actions.jsx";
|
||||
|
||||
/**
|
||||
* @typedef {Function} onContextMenuType
|
||||
* @callback onContextMenuType
|
||||
* @param {object} event
|
||||
* @param {object} params
|
||||
* @returns {Modal}
|
||||
|
|
|
|||
|
|
@ -1,11 +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";
|
||||
import { DEFAULT_DIALOG_SIZE } from "../../constants/modals.js";
|
||||
import ModalsManager from "../../features/modals/modals.js";
|
||||
|
||||
export function useDialogBox({ modalsManager }) {
|
||||
const onDialogBox = useCallback((event, params = {}) => {
|
||||
/**
|
||||
* @callback windowedModalType
|
||||
* @param {object} props
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback openWindowedModalType
|
||||
* @param {object} params
|
||||
* @param {string} params.iconUrl
|
||||
* @param {string} params.title
|
||||
* @param {Vector2} params.size
|
||||
* @param {windowedModalType} params.Modal
|
||||
* @returns {Modal}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {ModalsManager} props.modalsManager
|
||||
* @returns {{ openWindowedModal: openWindowedModalType }}
|
||||
*/
|
||||
export function useWindowedModal({ modalsManager }) {
|
||||
const openWindowedModal = useCallback(({ Modal: WindowedModal, ...params }) => {
|
||||
const size = params.size ?? DEFAULT_DIALOG_SIZE;
|
||||
let positionX = (window.innerWidth - size.x) / 4;
|
||||
let positionY = (window.innerHeight - size.y) / 4;
|
||||
|
|
@ -16,7 +36,9 @@ export function useDialogBox({ modalsManager }) {
|
|||
positionY -= containerRect.y / 2;
|
||||
}
|
||||
|
||||
const newModal = new Modal(DialogBox)
|
||||
console.log(WindowedModal);
|
||||
|
||||
const newModal = new Modal(WindowedModal)
|
||||
.setPosition(new Vector2(positionX, positionY))
|
||||
.setSize(size)
|
||||
.setDismissible(false)
|
||||
|
|
@ -26,5 +48,5 @@ export function useDialogBox({ modalsManager }) {
|
|||
return newModal;
|
||||
}, [modalsManager]);
|
||||
|
||||
return { onDialogBox };
|
||||
return { openWindowedModal };
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
--grey-a: #c8d6e5;
|
||||
--grey-b: #8395a7;
|
||||
--dark-grey-a: hsl(211, 14%, 40%);
|
||||
--dark-grey-b: hsl(212, 29%, 19%);
|
||||
--dark-grey-c: hsl(212, 29%, 15%);
|
||||
--dark-grey-b: hsl(var(--background-color-a-hsl));
|
||||
--dark-grey-c: hsl(var(--background-color-c-hsl));
|
||||
--dark-grey-ca: hsl(212, 27%, 12%);
|
||||
--dark-grey-d: hsl(212, 14%, 10%);
|
||||
--dark-grey-e: hsl(212, 12%, 8%);
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
--background-color-d: var(--dark-grey-e);
|
||||
|
||||
--background-color-a-hsl: 212, 29%, 19%;
|
||||
--background-color-c-hsl: 212, 29%, 15%;
|
||||
|
||||
--scrollbar-color: hsla(211, 29%, 40%, 25%);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue