Added file selector modal
This commit is contained in:
parent
133ce49855
commit
a57e680024
30 changed files with 533 additions and 129 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -153,4 +153,8 @@
|
|||
.Footer p {
|
||||
margin: 0;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.Selector {
|
||||
--footer-height: 4rem;
|
||||
}
|
||||
|
|
@ -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"]}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>);
|
||||
}
|
||||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
75
src/components/apps/settings/tabs/AppOption.jsx
Normal file
75
src/components/apps/settings/tabs/AppOption.jsx
Normal 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>;
|
||||
}
|
||||
74
src/components/apps/settings/tabs/AppearanceSettings.jsx
Normal file
74
src/components/apps/settings/tabs/AppearanceSettings.jsx
Normal 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>
|
||||
</>);
|
||||
}
|
||||
30
src/components/apps/settings/tabs/AppsSettings.jsx
Normal file
30
src/components/apps/settings/tabs/AppsSettings.jsx
Normal 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>;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
89
src/components/modals/file-selector/FileSelector.jsx
Normal file
89
src/components/modals/file-selector/FileSelector.jsx
Normal 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>;
|
||||
}
|
||||
25
src/components/modals/file-selector/FileSelector.module.css
Normal file
25
src/components/modals/file-selector/FileSelector.module.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
5
src/config/apps/fileExplorer.config.js
Normal file
5
src/config/apps/fileExplorer.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const SELECTOR_MODE = {
|
||||
NONE: 0,
|
||||
SINGLE: 1,
|
||||
MULTIPLE: 2
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue