Added file selector modal

This commit is contained in:
Prozilla 2023-12-29 15:24:19 +01:00
parent 133ce49855
commit a57e680024
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
30 changed files with 533 additions and 129 deletions

View file

@ -28,10 +28,7 @@ export const touch = new Command("touch")
+ "A FILE argument that does not exist is created empty."
})
.setExecute(function(args, { currentDirectory }) {
if (args[0] === "girls\\" && args[1] === "boo**")
return `${this.name}: Cannot touch 'girls boo**': Permission denied`;
const [name, extension] = args[0].split(".");
const { name, extension } = VirtualFile.convertId(args[0]);
if (currentDirectory.findFile(name, extension))
return { blank: true };

View file

@ -21,11 +21,19 @@ import { FileProperties } from "../../modals/file-properties/FileProperties.jsx"
import { useHistory } from "../../../hooks/_utils/history.js";
import { Divider } from "../../actions/actions/Divider.jsx";
import { CODE_FORMATS } from "../../../config/apps/textEditor.config.js";
import { SELECTOR_MODE } from "../../../config/apps/fileExplorer.config.js";
/**
* @param {import("../../windows/WindowView.jsx").windowProps} props
* @param {string} props.startPath
* @param {*} props.Footer
* @param {number} props.selectorMode
* @param {import("./directory-list/DirectoryList.jsx").onSelectionChange} props.onSelectionChange
* @param {Function} props.onSelectionFinish
*/
export function FileExplorer({ startPath, app, modalsManager }) {
export function FileExplorer({ startPath, selectorMode, Footer, modalsManager, onSelectionChange, onSelectionFinish }) {
const isSelector = (Footer != null && selectorMode != null && selectorMode !== SELECTOR_MODE.NONE);
const virtualRoot = useVirtualRoot();
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
const [path, setPath] = useState(currentDirectory?.path ?? "");
@ -36,7 +44,12 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const { openWindowedModal } = useWindowedModal({ modalsManager });
const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, file) => {
<ClickAction label={!isSelector ? "Open" : "Select"} onTrigger={(event, file) => {
if (isSelector) {
onSelectionChange?.({ files: [file.id], directory: currentDirectory });
onSelectionFinish?.();
return;
}
file.open(windowsManager);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
@ -56,7 +69,7 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder) => {
folder.open(windowsManager);
changeDirectory(folder.linkedPath ?? folder.name);
}}/>
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder) => {
windowsManager.open(APPS.TERMINAL, { startPath: folder.path });
@ -138,7 +151,7 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const itemCount = currentDirectory.getItemCount(showHidden);
return (
<div className={styles.Container}>
<div className={!isSelector ? styles.Container : `${styles.Container} ${styles.Selector}`}>
<div className={styles.Header}>
<button
title="Back"
@ -221,28 +234,37 @@ export function FileExplorer({ startPath, app, modalsManager }) {
id="main"
className={styles.Main}
showHidden={showHidden}
onClickFile={(event, file) => {
onOpenFile={(event, file) => {
event.preventDefault();
if (isSelector)
return onSelectionFinish?.();
const options = {};
if (file.extension === "md" || CODE_FORMATS.includes(file.extension))
options.mode = "view";
windowsManager.openFile(file, options);
}}
onClickFolder={(event, folder) => {
onOpenFolder={(event, folder) => {
changeDirectory(folder.linkedPath ?? folder.name);
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
allowMultiSelect={selectorMode !== SELECTOR_MODE.SINGLE}
onSelectionChange={onSelectionChange}
/>
</div>
<span className={styles.Footer}>
<p className={utilStyles["Text-light"]}>
{itemCount === 1
? itemCount + " item"
: itemCount + " items"
}
</p>
</span>
{!isSelector
? <span className={styles.Footer}>
<p className={utilStyles["Text-light"]}>
{itemCount === 1
? itemCount + " item"
: itemCount + " items"
}
</p>
</span>
: <div className={`${styles.Footer} ${styles["Selector-footer"]}`}>
<Footer/>
</div>
}
</div>
);
}

View file

@ -153,4 +153,8 @@
.Footer p {
margin: 0;
font-size: 0.875em;
}
.Selector {
--footer-height: 4rem;
}

View file

@ -17,6 +17,13 @@ import { ImagePreview } from "./ImagePreview.jsx";
* @param {VirtualFolder} folder
*/
/**
* @callback onSelectionChange
* @param {object} selection
* @param {string[]} selection.files
* @param {string[]} selection.folders
*/
/**
* @param {object} props
* @param {VirtualFolder} props.directory
@ -26,17 +33,23 @@ import { ImagePreview } from "./ImagePreview.jsx";
* @param {string} props.className
* @param {fileEvent} props.onContextMenuFile
* @param {folderEvent} props.onContextMenuFolder
* @param {fileEvent} props.onClickFile
* @param {folderEvent} props.onClickFolder
* @param {fileEvent} props.onOpenFile
* @param {folderEvent} props.onOpenFolder
* @param {boolean} props.allowMultiSelect
* @param {onSelectionChange} props.onSelectionChange
*/
export function DirectoryList({ directory, showHidden = false, folderClassName, fileClassName, className,
onContextMenuFile, onContextMenuFolder, onClickFile, onClickFolder, ...props }) {
onContextMenuFile, onContextMenuFolder, onOpenFile, onOpenFolder, allowMultiSelect = true, onSelectionChange, ...props }) {
const [selectedFolders, setSelectedFolders] = useState([]);
const [selectedFiles, setSelectedFiles] = useState([]);
const ref = useRef(null);
const [rectSelectStart, setRectSelectStart] = useState(null);
const [rectSelectEnd, setRectSelectEnd] = useState(null);
useEffect(() => {
onSelectionChange?.({ files: selectedFiles, folders: selectedFolders, directory });
}, [directory, onSelectionChange, selectedFiles, selectedFolders]);
useEffect(() => {
clearSelection();
@ -79,11 +92,15 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
setSelectedFiles([]);
};
const selectFolder = (folder, exclusive = false) => {
if (!allowMultiSelect)
exclusive = true;
setSelectedFolders(exclusive ? [folder.id] : [...selectedFolders, folder.id]);
if (exclusive)
setSelectedFiles([]);
};
const selectFile = (file, exclusive = false) => {
if (!allowMultiSelect)
exclusive = true;
setSelectedFiles(exclusive ? [file.id] : [...selectedFiles, file.id]);
if (exclusive)
setSelectedFolders([]);
@ -155,7 +172,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
selectFolder(folder, !event.ctrlKey);
}}
onDoubleClick={(event) => {
onClickFolder?.(event, folder);
onOpenFolder?.(event, folder);
}}
>
<div className={styles["Folder-icon"]}>
@ -177,7 +194,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
selectFile(file, !event.ctrlKey);
}}
onDoubleClick={(event) => {
onClickFile?.(event, file);
onOpenFile?.(event, file);
}}
>
<div className={styles["File-icon"]}>

View file

@ -1,43 +0,0 @@
import { useEffect, useState } from "react";
import { SettingsManager } from "../../../features/settings/settingsManager.js";
import styles from "./Settings.module.css";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext.js";
import { WALLPAPERS_PATH } from "../../../config/apps/settings.config.js";
export function AppearanceSettings() {
const virtualRoot = useVirtualRoot();
const settingsManager = useSettingsManager();
const [wallpaper, setWallpaper] = useState(null);
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
useEffect(() => {
settings.get("wallpaper", setWallpaper);
}, [settings]);
const onChange = (event) => {
const value = event.target.value;
settings.set("wallpaper", value);
};
return (<>
<div className={styles["Option"]}>
<p className={styles["Label"]}>Wallpaper</p>
<div className={styles["Input"]}>
{virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) =>
<label className={styles["Image-select"]} key={id}>
<input
type="radio"
value={source}
aria-label="Wallpaper image"
checked={source === wallpaper ? "checked" : ""}
onChange={onChange}
tabIndex={0}
/>
<img src={source} alt={id} draggable="false"/>
</label>
)}
</div>
</div>
</>);
}

View file

@ -1,16 +1,17 @@
import styles from "./Settings.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleInfo, faHardDrive, faPalette } from "@fortawesome/free-solid-svg-icons";
import { faCircleInfo, faHardDrive, faPalette, faShapes } from "@fortawesome/free-solid-svg-icons";
import utilStyles from "../../../styles/utils.module.css";
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
import { AppearanceSettings } from "./AppearanceSettings.jsx";
import { AboutSettings } from "./AboutSettings.jsx";
import { StorageTab } from "./StorageSettings.jsx";
import { AppearanceSettings } from "./tabs/AppearanceSettings.jsx";
import { AboutSettings } from "./tabs/AboutSettings.jsx";
import { StorageTab } from "./tabs/StorageSettings.jsx";
import { AppsSettings } from "./tabs/AppsSettings.jsx";
/**
* @param {import("../../windows/WindowView.jsx").windowProps} props
*/
export function Settings({ initialTabIndex }) {
export function Settings({ initialTabIndex, modalsManager }) {
return (
<Tabs
defaultIndex={initialTabIndex ?? 0}
@ -19,6 +20,10 @@ export function Settings({ initialTabIndex }) {
selectedTabPanelClassName={styles["Active-panel"]}
>
<TabList className={styles.Tabs}>
<Tab className={styles["Tab-button"]} tabIndex="0">
<FontAwesomeIcon icon={faShapes}/>
<p className={utilStyles["Text-semibold"]}>Apps</p>
</Tab>
<Tab className={styles["Tab-button"]} tabIndex="0">
<FontAwesomeIcon icon={faPalette}/>
<p className={utilStyles["Text-semibold"]}>Appearance</p>
@ -33,7 +38,10 @@ export function Settings({ initialTabIndex }) {
</Tab>
</TabList>
<TabPanel className={styles["Tab-panel"]}>
<AppearanceSettings/>
<AppsSettings modalsManager={modalsManager}/>
</TabPanel>
<TabPanel className={styles["Tab-panel"]}>
<AppearanceSettings modalsManager={modalsManager}/>
</TabPanel>
<TabPanel className={styles["Tab-panel"]}>
<StorageTab/>

View file

@ -75,6 +75,15 @@
width: 100%;
}
.Option-list {
gap: 0.5rem;
}
.Option-horizontal {
flex-direction: row;
justify-content: space-between;
}
.Option > .Label {
color: var(--foreground-color-a);
}
@ -145,6 +154,52 @@
--hover-color: var(--red-b) !important;
}
.Icon-button {
--color: var(--foreground-color-a);
position: relative;
height: 1.25rem;
width: auto;
padding: 0;
background: none;
border: none;
outline: none;
aspect-ratio: 1;
cursor: pointer;
}
.Icon-button::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0%);
border-radius: 9999px;
transform: scale(100%);
transform-origin: center;
transition: all 200ms ease-in-out;
}
.Icon-button:hover::after, .Icon-button:focus-visible::after {
background-color: rgba(255, 255, 255, 10%);
transform: scale(150%);
}
.Icon-button:disabled {
--color: var(--foreground-color-c);
}
.Icon-button svg {
height: 100%;
}
.Icon-button svg path {
fill: var(--color);
transition: fill 100ms ease-in-out;
}
.Progress-bar-container {
width: 100%;
max-width: 35rem;
@ -158,4 +213,27 @@
display: flex;
justify-content: space-between;
width: 100%;
}
.Option > span.Label {
display: flex;
gap: 0.5rem;
align-items: center;
}
.Icon {
display: inline-block;
width: 2rem;
height: 2rem;
}
.Icon div {
width: inherit;
height: inherit;
}
.Icon div > svg {
width: inherit;
height: inherit;
object-fit: contain;
}

View file

@ -1,10 +1,10 @@
import { Button } from "../../_utils/button/Button.jsx";
import styles from "./Settings.module.css";
import utilStyles from "../../../styles/utils.module.css";
import Vector2 from "../../../features/math/vector2.js";
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
import { NAME } from "../../../config/branding.config.js";
import { Button } from "../../../_utils/button/Button.jsx";
import styles from "../Settings.module.css";
import utilStyles from "../../../../styles/utils.module.css";
import Vector2 from "../../../../features/math/vector2.js";
import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext.js";
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js";
import { NAME } from "../../../../config/branding.config.js";
export function AboutSettings() {
const windowsManager = useWindowsManager();

View file

@ -0,0 +1,75 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import AppsManager from "../../../../features/apps/appsManager.js";
import { ImagePreview } from "../../file-explorer/directory-list/ImagePreview.jsx";
import styles from "../Settings.module.css";
import { faEllipsisVertical, faThumbTack } from "@fortawesome/free-solid-svg-icons";
import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext.js";
import { useContextMenu } from "../../../../hooks/modals/contextMenu.js";
import { Actions } from "../../../actions/Actions.jsx";
import { ClickAction } from "../../../actions/actions/ClickAction.jsx";
import { removeFromArray } from "../../../../features/_utils/array.utils.js";
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext.js";
import { SettingsManager } from "../../../../features/settings/settingsManager.js";
export function AppOption({ app, pins, setPins, modalsManager }) {
const isPinned = pins.includes(app.id);
const settingsManager = useSettingsManager();
const windowsManager = useWindowsManager();
const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) =>
<Actions {...props}>
<ClickAction label="Launch" icon={AppsManager.getAppIconUrl(app.id)} onTrigger={() => windowsManager.open(app.id)}/>
<ClickAction label={isPinned ? "Unpin from taskbar" : "Pin to taskbar"} icon={faThumbTack} onTrigger={() => {
const newPins = [...pins];
if (isPinned) {
removeFromArray(app.id, pins);
} else {
newPins.push(app.id);
}
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
settings.set("pins", newPins.join(","));
}}/>
</Actions>
});
return <div className={`${styles["Option"]} ${styles["Option-horizontal"]}`}>
<span className={styles["Label"]}>
<ImagePreview className={styles["Icon"]} source={AppsManager.getAppIconUrl(app.id)}/>
{app.name}
</span>
<button className={styles["Icon-button"]} onClick={onContextMenu}>
<FontAwesomeIcon icon={faEllipsisVertical}/>
</button>
{/* <div className={styles["Button-group"]}>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={() => windowsManager.open(id)}
>
Launch
</Button>
<Button
className={`${styles.Button} ${styles["Button-red"]} ${utilStyles["Text-bold"]}`}
>
Uninstall
</Button>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={() => {
const newPins = [...pins];
if (isPinned) {
removeFromArray(id, pins);
} else {
newPins.push(id);
}
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
settings.set("pins", newPins.join(","));
}}
>
{isPinned ? "Unpin from taskbar" : "Pin to taskbar"}
</Button>
</div> */}
</div>;
}

View file

@ -0,0 +1,74 @@
import { useEffect, useState } from "react";
import { SettingsManager } from "../../../../features/settings/settingsManager.js";
import styles from "../Settings.module.css";
import utilStyles from "../../../../styles/utils.module.css";
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js";
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext.js";
import { WALLPAPERS_PATH } from "../../../../config/apps/settings.config.js";
import { useWindowedModal } from "../../../../hooks/modals/windowedModal.js";
import ModalsManager from "../../../../features/modals/modalsManager.js";
import { Button } from "../../../_utils/button/Button.jsx";
import { FileSelector } from "../../../modals/file-selector/FileSelector.jsx";
import { SELECTOR_MODE } from "../../../../config/apps/fileExplorer.config.js";
import { DEFAULT_FILE_SELECTOR_SIZE } from "../../../../config/modals.config.js";
import { IMAGE_FORMATS } from "../../../../config/apps/mediaViewer.config.js";
/**
* @param {object} props
* @param {ModalsManager} props.modalsManager
*/
export function AppearanceSettings({ modalsManager }) {
const virtualRoot = useVirtualRoot();
const settingsManager = useSettingsManager();
const [wallpaper, setWallpaper] = useState(null);
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
const { openWindowedModal } = useWindowedModal({ modalsManager });
useEffect(() => {
settings.get("wallpaper", setWallpaper);
}, [settings]);
const onChange = (event) => {
const value = event.target.value;
settings.set("wallpaper", value);
};
return (<>
<div className={styles["Option"]}>
<p className={styles["Label"]}>Wallpaper</p>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={() => {
openWindowedModal({
size: DEFAULT_FILE_SELECTOR_SIZE,
Modal: (props) => <FileSelector
type={SELECTOR_MODE.SINGLE}
allowedFormats={IMAGE_FORMATS}
onFinish={(file) => {
settings.set("wallpaper", file.source);
}}
{...props}
/>
});
}}
>
Browse
</Button>
<div className={styles["Input"]}>
{virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) =>
<label className={styles["Image-select"]} key={id}>
<input
type="radio"
value={source}
aria-label="Wallpaper image"
checked={source === wallpaper ? "checked" : ""}
onChange={onChange}
tabIndex={0}
/>
<img src={source} alt={id} draggable="false"/>
</label>
)}
</div>
</div>
</>);
}

View file

@ -0,0 +1,30 @@
import { useEffect, useState } from "react";
import styles from "../Settings.module.css";
import AppsManager from "../../../../features/apps/appsManager.js";
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext.js";
import { SettingsManager } from "../../../../features/settings/settingsManager.js";
import ModalsManager from "../../../../features/modals/modalsManager.js";
import { AppOption } from "./AppOption.jsx";
/**
* @param {object} props
* @param {ModalsManager} props.modalsManager
*/
export function AppsSettings({ modalsManager }) {
const settingsManager = useSettingsManager();
const [pins, setPins] = useState([]);
useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
settings.get("pins", (pins) => {
setPins(pins.split(","));
});
}, [settingsManager]);
return <div className={`${styles["Option"]} ${styles["Option-list"]}`}>
<p className={styles["Label"]}>Apps</p>
{AppsManager.APPS.map((app) =>
<AppOption key={app.id} app={app} modalsManager={modalsManager} pins={pins} setPins={setPins}/>
)}
</div>;
}

View file

@ -1,10 +1,10 @@
import styles from "./Settings.module.css";
import utilStyles from "../../../styles/utils.module.css";
import { round } from "../../../features/math/round.js";
import { ProgressBar } from "../../_utils/progress-bar/ProgressBar.jsx";
import { Button } from "../../_utils/button/Button.jsx";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
import { StorageManager } from "../../../features/storage/storageManager.js";
import styles from "../Settings.module.css";
import utilStyles from "../../../../styles/utils.module.css";
import { round } from "../../../../features/math/round.js";
import { ProgressBar } from "../../../_utils/progress-bar/ProgressBar.jsx";
import { Button } from "../../../_utils/button/Button.jsx";
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext.js";
import { StorageManager } from "../../../../features/storage/storageManager.js";
export function StorageTab() {
const virtualRoot = useVirtualRoot();

View file

@ -9,6 +9,10 @@ import { MarkdownLink } from "./overrides/MarkdownLink.jsx";
import { MarkdownImage } from "./overrides/MarkdownImage.jsx";
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
import SyntaxHighlighter from "react-syntax-highlighter";
import { useWindowedModal } from "../../../hooks/modals/windowedModal.js";
import { DEFAULT_FILE_SELECTOR_SIZE } from "../../../config/modals.config.js";
import { FileSelector } from "../../modals/file-selector/FileSelector.jsx";
import { SELECTOR_MODE } from "../../../config/apps/fileExplorer.config.js";
const OVERRIDES = {
a: MarkdownLink,
@ -26,6 +30,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
const [content, setContent] = useState(file?.content ?? "");
const [unsavedChanges, setUnsavedChanges] = useState(file == null);
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
const { openWindowedModal } = useWindowedModal({ modalsManager });
useEffect(() => {
(async () => {
@ -115,12 +120,27 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
};
}
console.log(currentFile);
return (
<div className={styles.Container} style={{ fontSize: zoom }}>
<HeaderMenu
options={{
"File": {
"New": newText,
"Open": () => {
openWindowedModal({
size: DEFAULT_FILE_SELECTOR_SIZE,
Modal: (props) => <FileSelector
type={SELECTOR_MODE.SINGLE}
onFinish={(file) => {
setCurrentFile(file);
setUnsavedChanges(false);
}}
{...props}
/>
});
},
"Save": saveText,
// "Save As": saveTextAs,
"Quit": () => {
@ -145,6 +165,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
shortcuts={{
"File": {
"New": ["Control", "e"],
"Open": ["Control", "o"],
"Save": ["Control", "s"],
"Quit": ["Control", "q"],
},
@ -156,17 +177,17 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
}}
/>
{currentMode === "view"
? CODE_FORMATS.includes(file?.extension)
? CODE_FORMATS.includes(currentFile?.extension)
? <SyntaxHighlighter
language={EXTENSION_TO_LANGUAGE[file?.extension] ?? file?.extension}
language={EXTENSION_TO_LANGUAGE[currentFile?.extension] ?? currentFile?.extension}
className={styles.Code}
useInlineStyles={false}
showLineNumbers={true}
>{content}</SyntaxHighlighter>
: <div ref={ref} className={styles.View}>
{file?.extension === "md"
{currentFile?.extension === "md"
? <Markdown options={{ overrides }}>{content}</Markdown>
: <p>{content}</p>
: <pre><p>{content}</p></pre>
}
</div>
: <textarea

View file

@ -32,7 +32,7 @@ export function MarkdownLink({ href, children, modalsManager, windowsManager, cu
if (!href.startsWith("http://") && !href.startsWith("https://")) {
const target = currentFile.parent.navigate(href);
if (target) {
if (target != null) {
if (target.isFile()) {
setCurrentFile(target);
} else {

View file

@ -136,7 +136,7 @@ export const Desktop = memo(() => {
}}
fileClassName={styles["Item"]}
folderClassName={styles["Item"]}
onClickFile={(event, file) => {
onOpenFile={(event, file) => {
event.preventDefault();
const options = {};
@ -147,7 +147,7 @@ export const Desktop = memo(() => {
windowsManager.openFile(file, options);
}}
onClickFolder={(event, { linkedPath, path }) => {
onOpenFolder={(event, { linkedPath, path }) => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: linkedPath ?? path });
}}
onContextMenuFile={onContextMenuFile}

View file

@ -7,7 +7,14 @@ 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";
import Modal from "../../../features/modals/modal.js";
/**
* @param {object} props
* @param {Modal} props.modal
* @param {*} props.params
* @param {*} props.children
*/
export function WindowedModal({ modal, params, children, ...props }) {
const { iconUrl, title } = params;
@ -34,7 +41,7 @@ export function WindowedModal({ modal, params, children, ...props }) {
}
}, [modal, screenHeight, screenWidth]);
return (<Draggable
return <Draggable
axis="both"
handle={".Window-handle"}
defaultPosition={startPosition}
@ -72,5 +79,5 @@ export function WindowedModal({ modal, params, children, ...props }) {
{children}
</div>
</div>
</Draggable>);
</Draggable>;
}

View file

@ -89,22 +89,4 @@
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);
}

View file

@ -1,5 +1,5 @@
import { DIALOG_CONTENT_TYPES } from "../../../config/modals.config.js";
import { WindowedModal } from "../_templates/WindowedModal.jsx";
import { WindowedModal } from "../_utils/WindowedModal.jsx";
export function DialogBox({ modal, params, children, ...props }) {
const onClick = (event) => {

View file

@ -1,5 +1,5 @@
import { ImagePreview } from "../../apps/file-explorer/directory-list/ImagePreview.jsx";
import { WindowedModal } from "../_templates/WindowedModal.jsx";
import { WindowedModal } from "../_utils/WindowedModal.jsx";
import styles from "./FileProperties.module.css";
import utilStyles from "../../../styles/utils.module.css";
import Modal from "../../../features/modals/modal.js";

View file

@ -0,0 +1,89 @@
import { useState } from "react";
import { APP_ICONS } from "../../../config/apps.config.js";
import { SELECTOR_MODE } from "../../../config/apps/fileExplorer.config.js";
import { useModals } from "../../../hooks/modals/modals.js";
import { Button } from "../../_utils/button/Button.jsx";
import { FileExplorer } from "../../apps/file-explorer/FileExplorer.jsx";
import { ModalsView } from "../ModalsView.jsx";
import { WindowedModal } from "../_utils/WindowedModal.jsx";
import styles from "./FileSelector.module.css";
import { VirtualFile } from "../../../features/virtual-drive/file/virtualFile.js";
import Modal from "../../../features/modals/modal.js";
/**
* @callback onFinish
* @param {VirtualFile|VirtualFile[]} result
*/
/**
* @param {object} props
* @param {Modal} props.modal
* @param {*} props.params
* @param {number} props.type
* @param {string[]=} props.allowedFormats
* @param {onFinish} props.onFinish
*/
export function FileSelector({ modal, params, type, allowedFormats, onFinish, ...props }) {
const multi = (type === SELECTOR_MODE.MULTIPLE);
const [modalsManager, modals] = useModals();
const [selection, setSelection] = useState(multi ? [] : null);
const [directory, setDirectory] = useState(null);
params.title = multi ? "Select files" : "Select a file";
params.iconUrl = APP_ICONS.FILE_EXPLORER;
const finish = (event) => {
event?.preventDefault();
if (directory == null || selection == null)
return;
const files = selection.map((id) => {
const { name, extension } = VirtualFile.convertId(id);
return directory.findFile(name, extension);
}).filter((file) => {
if (file == null)
return false;
const validFormat = (allowedFormats == null || allowedFormats.includes(file.extension));
return validFormat;
});
if (files.length === 0)
return;
modal.close();
onFinish?.(multi ? files : files[0]);
};
return <WindowedModal modal={modal} params={params} {...props}>
<ModalsView modalsManager={modalsManager} modals={modals}/>
<FileExplorer
modalsManager={modalsManager}
selectorMode={type}
Footer={() =>
<div className={styles.Footer}>
<span className={styles.Selection}>
{multi
? <p>Selected file(s): {selection.join(", ")}</p>
: <p>Selected file: {selection ?? ""}</p>
}
</span>
<div className={styles.Buttons}>
<Button className={styles.Button} onClick={finish}>
Confirm
</Button>
<Button className={styles.Button} onClick={() => { modal.close(); }}>
Cancel
</Button>
</div>
</div>
}
onSelectionChange={({ files, directory }) => {
setSelection(files);
setDirectory(directory);
}}
onSelectionFinish={finish}
/>
</WindowedModal>;
}

View file

@ -0,0 +1,25 @@
.Footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.Selection > p {
font-size: 1rem;
}
.Buttons {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
}
.Button {
--normal-color: var(--background-color-a);
--hover-color: var(--background-color-b);
padding: 0.35rem 0.7rem;
border-radius: 0.5rem;
}

View file

@ -77,12 +77,10 @@ export const Taskbar = memo(() => {
}), [modalsManager, pins, windows, windowsManager]);
useEffect(() => {
(async () => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
settings.get("pins", (pins) => {
setPins(pins.split(","));
});
})();
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
settings.get("pins", (pins) => {
setPins(pins.split(","));
});
}, [settingsManager]);
const updateShowHome = (value) => {

View file

@ -0,0 +1,5 @@
export const SELECTOR_MODE = {
NONE: 0,
SINGLE: 1,
MULTIPLE: 2
};

View file

@ -4,4 +4,5 @@ export const DIALOG_CONTENT_TYPES = {
CloseButton: 0
};
export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200);
export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200);
export const DEFAULT_FILE_SELECTOR_SIZE = new Vector2(700, 400);

View file

@ -1,3 +1,4 @@
import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js";
import Command from "../command.js";
export const cat = new Command("cat")
@ -8,7 +9,7 @@ export const cat = new Command("cat")
description: "Concetenate FILE(s) to standard output."
})
.setExecute(function(args, { currentDirectory, options }) {
const [name, extension] = args[0].split(".");
const { name, extension } = VirtualFile.convertId(args[0]);
const file = currentDirectory.findFile(name, extension);
if (!file)

View file

@ -1,3 +1,4 @@
import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js";
import Command from "../command.js";
export const rm = new Command("rm")
@ -6,7 +7,7 @@ export const rm = new Command("rm")
purpose: "Remove a file"
})
.setExecute((args, { currentDirectory }) => {
const [name, extension] = args[0].split(".");
const { name, extension } = VirtualFile.convertId(args[0]);
const file = currentDirectory.findFile(name, extension);
if (!file)

View file

@ -1,3 +1,4 @@
import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js";
import Command from "../command.js";
export const touch = new Command("touch")
@ -12,7 +13,7 @@ export const touch = new Command("touch")
if (args[0] === "girls\\" && args[1] === "boo**")
return `${this.name}: Cannot touch 'girls boo**': Permission denied`;
const [name, extension] = args[0].split(".");
const { name, extension } = VirtualFile.convertId(args[0]);
if (currentDirectory.findFile(name, extension))
return { blank: true };

View file

@ -76,6 +76,20 @@ export class VirtualFile extends VirtualBase {
return `${this.name}.${this.extension}`;
}
/**
* @param {string} id
* @returns {{
* name: string,
* extension: string
* }}
*/
static convertId(id) {
const sections = id.split(".");
const extension = sections.pop();
const name = sections.join(".");
return { name, extension };
}
/**
* Opens this file in an app associated with its extension
* @param {WindowsManager} windowsManager

View file

@ -334,8 +334,7 @@ export class VirtualFolder extends VirtualBase {
if (folder != null)
return folder;
// To do: add support for file names with dots
const [name, extension] = lastSegment.split(".");
const { name, extension } = VirtualFile.convertId(lastSegment);
return currentDirectory.findFile(name, extension);
} else {
return null;

View file

@ -36,8 +36,6 @@ export function useWindowedModal({ modalsManager }) {
positionY -= containerRect.y / 2;
}
console.log(WindowedModal);
const newModal = new Modal(WindowedModal)
.setPosition(new Vector2(positionX, positionY))
.setSize(size)