Refactored settings app

This commit is contained in:
Prozilla 2023-08-15 12:58:58 +02:00
parent be303e8a68
commit 219e712e9f
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
11 changed files with 197 additions and 166 deletions

21
package-lock.json generated
View file

@ -22,6 +22,7 @@
"react-draggable": "^4.4.5",
"react-scripts": "5.0.1",
"react-svg": "^16.1.18",
"react-tabs": "^6.0.2",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@ -14960,6 +14961,26 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-tabs": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz",
"integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==",
"dependencies": {
"clsx": "^2.0.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/react-tabs/node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View file

@ -20,6 +20,7 @@
"react-draggable": "^4.4.5",
"react-scripts": "5.0.1",
"react-svg": "^16.1.18",
"react-tabs": "^6.0.2",
"web-vitals": "^2.1.4"
},
"scripts": {

View file

@ -4,7 +4,7 @@ import { WindowsManagerProvider } from "./hooks/windows/WindowsManagerContext.js
import { WindowsView } from "./components/windows/WindowsView.jsx";
import { VirtualRootProvider } from "./hooks/virtual-drive/VirtualRootContext.js";
import { Desktop } from "./components/desktop/Desktop.jsx";
import { SettingsProvider } from "./hooks/settings/SettingsContext.js";
import { SettingsManagerProvider } from "./hooks/settings/SettingsContext.js";
import { ModalsView } from "./components/modals/ModalsView.jsx";
import { useEffect } from "react";
@ -24,14 +24,14 @@ function App() {
return (
<VirtualRootProvider>
<WindowsManagerProvider>
<SettingsProvider>
<SettingsManagerProvider>
<div className={styles.App}>
<Taskbar/>
<WindowsView/>
<ModalsView/>
<Desktop/>
</div>
</SettingsProvider>
</SettingsManagerProvider>
</WindowsManagerProvider>
</VirtualRootProvider>
);

View file

@ -0,0 +1,42 @@
import { Button } from "../../utils/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";
export function AboutSettings() {
const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot();
return (<>
<div className={styles["Option"]}>
<p className={styles["Label"]}>About ProzillaOS</p>
<p className={utilStyles["Text-light"]}>ProzillaOS is a web-based operating system inspired by Ubuntu Linux and Windows made with React.js by Prozilla.</p>
<div className={styles["Button-group"]}>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={(event) => {
event.preventDefault();
windowsManager.open("text-editor", {
mode: "view",
file: virtualRoot.navigate("~/Documents").findFile("info", "md"),
size: new Vector2(575, 675),
});
}}
>
Open info.md
</Button>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={(event) => {
event.preventDefault();
window.open("https://github.com/Prozilla/prozilla-os");
}}
>
View source
</Button>
</div>
</div>
</>);
}

View file

@ -0,0 +1,42 @@
import { useEffect, useState } from "react";
import { SettingsManager } from "../../../features/settings/settings.js";
import styles from "./Settings.module.css";
import { useVirtualRoot } from "../../../hooks/virtual-drive/VirtualRootContext.js";
import { useSettingsManager } from "../../../hooks/settings/SettingsContext.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("~/Images")?.getFiles()?.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,168 +1,47 @@
import { useEffect, useState } from "react";
import styles from "./Settings.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleInfo, faHardDrive, faPalette } from "@fortawesome/free-solid-svg-icons";
import { useVirtualRoot } from "../../../hooks/virtual-drive/VirtualRootContext.js";
import { useSettings } from "../../../hooks/settings/SettingsContext.js";
import { SettingsManager } from "../../../features/settings/settings.js";
import { VirtualRoot } from "../../../features/virtual-drive/virtual-root.js";
import { StorageManager } from "../../../features/storage/storage.js";
import { round } from "../../../features/math/round.js";
import { Button } from "../../utils/Button.jsx";
import { ProgressBar } from "../../utils/ProgressBar.jsx";
import utilStyles from "../../../styles/utils.module.css";
import { useWindowsManager } from "../../../hooks/windows/WindowsManagerContext.js";
import Vector2 from "../../../features/math/vector2.js";
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
import { AppearanceSettings } from "./AppearanceSettings.jsx";
import { AboutSettings } from "./AboutSettings.jsx";
import { StorageTab } from "./StorageSettings.jsx";
/**
* @param {object} props
* @param {VirtualRoot} props.virtualRoot
* @param {SettingsManager} props.settingsManager
*/
function AppearanceTab({ virtualRoot, settingsManager }) {
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("~/Images")?.getFiles()?.map(({ name, id, source }) =>
<label className={styles["Image-select"]} key={id}>
<input
type="radio"
value={source}
aria-label="Wallpaper image"
checked={source === wallpaper ? "checked" : ""}
onChange={onChange}
/>
<img src={source} alt={id} draggable="false"/>
</label>
)}
</div>
</div>
</>);
}
/**
* @param {object} root
* @param {VirtualRoot} root.virtualRoot
*/
function StorageTab({ virtualRoot }) {
const maxBytes = StorageManager.MAX_BYTES;
const usedBytes = StorageManager.getByteSize(virtualRoot.toString());
const maxKB = StorageManager.byteToKilobyte(maxBytes);
const usedKB = StorageManager.byteToKilobyte(usedBytes);
const freeKB = maxKB - usedKB;
return (<>
<div className={`${styles["Option"]} ${styles["Progress-bar-container"]}`}>
<p className={styles["Label"]}>Virtual Drive ({round(maxKB, 1)} KB)</p>
<ProgressBar fillPercentage={usedKB / maxKB * 100} className={styles["Progress-bar"]}/>
<span className={styles["Progress-bar-labels"]}>
<p className={utilStyles["Text-light"]}>{round(usedKB, 1)} KB used</p>
<p className={utilStyles["Text-light"]}>{round(freeKB, 1)} KB free</p>
</span>
</div>
<div className={styles["Option"]}>
<p className={styles["Label"]}>Manage data</p>
<Button
className={`${styles.Button} ${styles["Button-red"]} ${utilStyles["Text-bold"]}`}
onClick={() => { virtualRoot.reset?.(); }}
>
Reset
</Button>
</div>
</>);
}
function AboutTab({ windowsManager, virtualRoot }) {
return (<>
<div className={styles["Option"]}>
<p className={styles["Label"]}>About ProzillaOS</p>
<p className={utilStyles["Text-light"]}>ProzillaOS is a web-based operating system inspired by Ubuntu Linux and Windows made with React.js by Prozilla.</p>
<div className={styles["Button-group"]}>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={(event) => {
event.preventDefault();
windowsManager.open("text-editor", {
mode: "view",
file: virtualRoot.navigate("~/Documents").findFile("info", "md"),
size: new Vector2(575, 675),
});
}}
>
Open info.md
</Button>
<Button
className={`${styles.Button} ${utilStyles["Text-bold"]}`}
onClick={(event) => {
event.preventDefault();
window.open("https://github.com/Prozilla/prozilla-os");
}}
>
View source
</Button>
</div>
</div>
</>);
}
/**
* @param {number} param0
* @param {object} props
* @param {number} props.initialTabIndex
*/
export function Settings({ initialTabIndex }) {
const [tabIndex, setTabIndex] = useState(initialTabIndex ?? 0);
const virtualRoot = useVirtualRoot();
const settingsManager = useSettings();
const windowsManager = useWindowsManager();
const getClassName = (index) => {
const classNames = [styles["Tab-button"]];
if (index === tabIndex)
classNames.push(styles["Active-tab"]);
return classNames.join(" ");
};
return (
<div className={styles.Container}>
<div className={styles.Tabs}>
<button title="Appearance" className={getClassName(0)} onClick={() => { setTabIndex(0); }}>
<Tabs
defaultIndex={initialTabIndex ?? 0}
className={styles.Container}
selectedTabClassName={styles["Active-tab"]}
selectedTabPanelClassName={styles["Active-panel"]}
>
<TabList className={styles.Tabs}>
<Tab className={styles["Tab-button"]} tabIndex="0">
<FontAwesomeIcon icon={faPalette}/>
<p className={utilStyles["Text-semibold"]}>Appearance</p>
</button>
<button title="Storage" className={getClassName(1)} onClick={() => { setTabIndex(1); }}>
</Tab>
<Tab className={styles["Tab-button"]} tabIndex="0">
<FontAwesomeIcon icon={faHardDrive}/>
<p className={utilStyles["Text-semibold"]}>Storage</p>
</button>
<button title="Storage" className={getClassName(2)} onClick={() => { setTabIndex(2); }}>
</Tab>
<Tab className={styles["Tab-button"]} tabIndex="0">
<FontAwesomeIcon icon={faCircleInfo}/>
<p className={utilStyles["Text-semibold"]}>About</p>
</button>
</div>
<div className={styles["Tab-panel"]}>
{tabIndex === 0
? <AppearanceTab virtualRoot={virtualRoot} settingsManager={settingsManager}/>
: tabIndex === 1
? <StorageTab virtualRoot={virtualRoot}/>
: tabIndex === 2
? <AboutTab virtualRoot={virtualRoot} windowsManager={windowsManager}/>
: null
}
</div>
</div>
</Tab>
</TabList>
<TabPanel className={styles["Tab-panel"]}>
<AppearanceSettings/>
</TabPanel>
<TabPanel className={styles["Tab-panel"]}>
<StorageTab/>
</TabPanel>
<TabPanel className={styles["Tab-panel"]}>
<AboutSettings/>
</TabPanel>
</Tabs>
);
}

View file

@ -17,6 +17,7 @@
height: 100%;
max-width: 50%;
padding: 0.5rem;
margin: 0;
background-color: var(--background-color-ba);
resize: horizontal;
overflow: hidden;
@ -27,13 +28,10 @@
gap: 0.5rem;
align-items: center;
width: 100%;
min-width: fit-content;
padding: 0.75rem;
background: none;
border: none;
border-radius: 0.5rem;
outline: none;
cursor: pointer;
transition: background-color 100ms ease-in-out;
font-size: 0.875rem;
}
.Tab-button.Active-tab {
@ -64,6 +62,10 @@
overflow: auto;
}
.Tab-panel:not(.Active-panel) {
display: none;
}
.Option {
display: flex;
flex-direction: column;
@ -100,14 +102,20 @@
height: 0;
}
.Image-select > input:hover + img,
.Image-select > input:focus-visible + img {
filter: brightness(110%);
}
.Image-select > input:checked + img {
outline: 0.25rem solid var(--foreground-color-a);
outline: 0.25em solid var(--foreground-color-a);
}
.Image-select > img {
width: 100%;
height: 100%;
border-radius: 0.5rem;
transition: filter 100ms ease-in-out;
cursor: pointer;
}
@ -115,7 +123,7 @@
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
margin: 0.75rem 0;
}
.Button-group .Button {

View file

@ -0,0 +1,38 @@
import styles from "./Settings.module.css";
import utilStyles from "../../../styles/utils.module.css";
import { round } from "../../../features/math/round.js";
import { ProgressBar } from "../../utils/ProgressBar.jsx";
import { Button } from "../../utils/Button.jsx";
import { useVirtualRoot } from "../../../hooks/virtual-drive/VirtualRootContext.js";
import { StorageManager } from "../../../features/storage/storage.js";
export function StorageTab() {
const virtualRoot = useVirtualRoot();
const maxBytes = StorageManager.MAX_BYTES;
const usedBytes = StorageManager.getByteSize(virtualRoot.toString());
const maxKB = StorageManager.byteToKilobyte(maxBytes);
const usedKB = StorageManager.byteToKilobyte(usedBytes);
const freeKB = maxKB - usedKB;
return (<>
<div className={`${styles["Option"]} ${styles["Progress-bar-container"]}`}>
<p className={styles["Label"]}>Virtual Drive ({round(maxKB, 1)} KB)</p>
<ProgressBar fillPercentage={usedKB / maxKB * 100} className={styles["Progress-bar"]}/>
<span className={styles["Progress-bar-labels"]}>
<p className={utilStyles["Text-light"]}>{round(usedKB, 1)} KB used</p>
<p className={utilStyles["Text-light"]}>{round(freeKB, 1)} KB free</p>
</span>
</div>
<div className={styles["Option"]}>
<p className={styles["Label"]}>Manage data</p>
<Button
className={`${styles.Button} ${styles["Button-red"]} ${utilStyles["Text-bold"]}`}
onClick={() => { virtualRoot.reset?.(); }}
>
Reset
</Button>
</div>
</>);
}

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { SettingsManager } from "../../features/settings/settings.js";
import { useSettings } from "../../hooks/settings/SettingsContext.js";
import { useSettingsManager } from "../../hooks/settings/SettingsContext.js";
import styles from "./Desktop.module.css";
import { useEffect } from "react";
import { useModals } from "../../hooks/modals/Modals.js";
@ -9,7 +9,7 @@ import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js"
import { useContextMenu } from "../../hooks/modals/ContextMenu.js";
export function Desktop() {
const settingsManager = useSettings();
const settingsManager = useSettingsManager();
const [wallpaper, setWallpaper] = useState(null);
const [modalsManager, modals] = useModals();
const windowsManager = useWindowsManager();

View file

@ -10,7 +10,7 @@ const SettingsContext = createContext();
* @param props.children
* @returns {import("react").Provider<SettingsManager>}
*/
export function SettingsProvider({ children }) {
export function SettingsManagerProvider({ children }) {
const virtualRoot = useVirtualRoot();
const settingsManager = new SettingsManager(virtualRoot);
@ -24,6 +24,6 @@ export function SettingsProvider({ children }) {
/**
* @returns {SettingsManager}
*/
export function useSettings() {
export function useSettingsManager() {
return useContext(SettingsContext);
}

View file

@ -87,7 +87,7 @@ h4 { font-size: 1.4em; }
h5 { font-size: 1.2em; }
h6 { font-size: 1em; }
button {
button, li[data-rttab="true"] {
margin: 0;
border: none;
outline: none;