Added settings feature

This commit is contained in:
Prozilla 2023-08-05 14:31:37 +02:00
parent 919afa3c92
commit 37a5248a6e
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
20 changed files with 361 additions and 20 deletions

View file

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<options>
<wallpaper>/media/wallpapers/Wallpaper1.png</wallpaper>
</options>

View file

@ -3,16 +3,20 @@ import { Taskbar } from "./components/task-bar/TaskBar.jsx";
import { WindowsManagerProvider } from "./hooks/windows/WindowsManagerContext.js";
import { WindowsView } from "./components/windows/WindowsView.jsx";
import { VirtualRootProvider } from "./hooks/virtual-drive/VirtualRootContext.js";
import { SETTINGS } from "./config/settings.js";
import { Desktop } from "./components/desktop/Desktop.jsx";
import { SettingsProvider } from "./hooks/settings/SettingsContext.js";
function App() {
return (
<VirtualRootProvider>
<WindowsManagerProvider>
<div className={styles.App} style={{ backgroundImage: `url(${SETTINGS.wallpaper})` }}>
<Taskbar/>
<WindowsView/>
</div>
<SettingsProvider>
<div className={styles.App}>
<Taskbar/>
<WindowsView/>
<Desktop/>
</div>
</SettingsProvider>
</WindowsManagerProvider>
</VirtualRootProvider>
);

View file

@ -4,6 +4,5 @@
left: 0;
width: 100%;
height: 100%;
background-size: cover;
text-align: center;
}

View file

@ -37,6 +37,7 @@ export function FileExplorer({ startPath }) {
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
const [path, setPath] = useState(currentDirectory.path);
const windowsManager = useWindowsManager();
const [showHidden] = useState(true);
const changeDirectory = (path, absolute = false) => {
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path);
@ -107,7 +108,7 @@ export function FileExplorer({ startPath }) {
</button>
</div>
<div className={styles.Main}>
{currentDirectory.files.map((file, index) =>
{currentDirectory.getFiles(showHidden).map((file, index) =>
<button key={index} title={file.id} className={styles["File-button"]} onClick={(event) => {
event.preventDefault();
windowsManager.openFile(file);
@ -116,7 +117,7 @@ export function FileExplorer({ startPath }) {
<p>{file.id}</p>
</button>
)}
{currentDirectory.subFolders.map(({ name }, index) =>
{currentDirectory.getSubFolders(showHidden).map(({ name }, index) =>
<button key={index} title={name} className={styles["Folder-button"]} onClick={() => {
changeDirectory(name);
}}>

View file

@ -115,6 +115,7 @@
flex: 1;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
height: 100%;
padding: 0.5rem;
overflow: auto;

View file

@ -65,7 +65,7 @@ export function TextEditor({ file, setTitle, close, mode }) {
if (currentFile == null)
return saveTextAs();
currentFile.content = content;
currentFile.setContent(content);
onChange({ target: { value: content } });
};

View file

@ -0,0 +1,21 @@
import { useState } from "react";
import { SettingsManager } from "../../features/settings/settings.js";
import { useSettings } from "../../hooks/settings/SettingsContext.js";
import styles from "./Desktop.module.css";
import { useEffect } from "react";
export function Desktop() {
const settingsManager = useSettings();
const [wallpaper, setWallpaper] = useState(null);
useEffect(() => {
(async () => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
settings.get("wallpaper", setWallpaper);
})();
}, [settingsManager]);
return (
<div className={styles.Container} style={{ backgroundImage: `url(${wallpaper})` }}/>
);
}

View file

@ -0,0 +1,10 @@
.Container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-size: cover;
background-position: center;
}

View file

@ -11,9 +11,9 @@ export function WindowsView() {
return (<div>
{windows.sort((windowA, windowB) =>
windowA.lastInteraction - windowB.lastInteraction
).map(({ id, app, size, position, options }, index) =>
).map(({ id, app, size, position, options }) =>
<Window
onInteract={() => { windowsManager.focus(windows[index]); }}
onInteract={() => { windowsManager.focus(id); }}
id={id}
key={id}
app={app}

View file

@ -1,3 +0,0 @@
export const SETTINGS = {
wallpaper: "/media/wallpapers/Wallpaper1.png"
};

View file

@ -51,6 +51,7 @@ export default class ApplicationsManager {
break;
case "txt":
case "md":
case "xml":
app = this.getApplication("text-editor");
break;
}

View file

@ -0,0 +1,160 @@
// eslint-disable-next-line no-unused-vars
import { VirtualFile } from "../virtual-drive/virtual-file.js";
// eslint-disable-next-line no-unused-vars
import { VirtualRoot } from "../virtual-drive/virtual-root.js";
export class Settings {
xmlDoc = null;
/**
* @type {VirtualRoot}
*/
#virtualRoot = null;
/**
* @param {VirtualRoot} virtualRoot
* @param {String} path
*/
constructor(virtualRoot, path) {
this.#virtualRoot = virtualRoot;
this.path = path;
this.file = this.#virtualRoot.navigate(this.path);
if (this.file == null) {
console.warn(`Unable to read settings from path: ${this.path}\nNo such file or directory.`);
return null;
} else if (!(this.file instanceof VirtualFile)) {
console.warn(`Unable to read settings from path: ${this.path}\nPath does not point to VirtualFile.`);
return null;
} else if (this.file.extension !== "xml") {
console.warn(`Unable to read settings from path: ${this.path}\nFile does not have extension "xml".`);
return null;
}
}
/**
* Reads the xml doc from the given path and assigns it to itself
* @returns {Promise<Settings>}
*/
async read() {
if (!this.file)
return;
const text = await this.file.read();
if (!text)
return;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, "text/xml");
this.xmlDoc = xmlDoc;
}
async write() {
if (!this.file)
return;
const serializer = new XMLSerializer();
const xmlString = serializer.serializeToString(this.xmlDoc);
this.file.setContent(xmlString);
}
/**
* Checks if xml doc is missing
* @returns {Promise<Boolean>}
*/
async isMissingXmlDoc() {
if (this.xmlDoc == null)
await this.read();
return (this.xmlDoc == null);
}
/**
* @callback valueCallback
* @param {String} value
*/
/**
* Gets a value by a given key if it exists or calls a callback function whenever the value changes
* @param {String} key
* @param {valueCallback} callback
* @returns {Promise<String|null>}
*/
async get(key, callback) {
if (await this.isMissingXmlDoc())
return null;
let value = this.xmlDoc.getElementsByTagName(key)?.[0]?.textContent;
if (callback) {
callback(value);
this.file.on(VirtualFile.EVENT_NAMES.CONTENT_CHANGE, async () => {
await this.read();
const newValue = await this.get(key);
if (newValue !== value) {
callback(newValue);
value = newValue;
}
});
}
return value;
}
/**
* @param {String} key
* @param {String} value
*/
async set(key, value) {
if (await this.isMissingXmlDoc())
return;
if (this.xmlDoc.getElementsByTagName(key)?.[0])
this.xmlDoc.getElementsByTagName(key)[0].textContent = value;
await this.write();
}
}
export class SettingsManager {
/**
* @type {Object<String, String>}
*/
static VIRTUAL_PATHS = {
"desktop": "~/.config/desktop.xml"
};
/**
* @type {Object<String, Settings>}
*/
#pathToSettings = {};
/**
* @type {VirtualRoot}
*/
#virtualRoot = null;
/**
* @param {VirtualRoot} virtualRoot
*/
constructor(virtualRoot) {
this.#virtualRoot = virtualRoot;
Object.values(SettingsManager.VIRTUAL_PATHS).forEach((path) => {
this.#pathToSettings[path] = new Settings(this.#virtualRoot, path);
});
}
/**
* @param {String} path
* @returns {Settings}
*/
get(path) {
return this.#pathToSettings[path];
}
}

View file

@ -0,0 +1,49 @@
export class EventEmitter {
/**
* @type {Object<String, String>}
*/
static EVENT_NAMES = {};
/**
* @type {Object<String, Array<Function>>}
*/
#events = {}
/**
* Add event listener for an event
* @param {EVENT_NAMES} eventName
* @param {Function} callback
*/
on(eventName, callback) {
if (!this.#events[eventName]) {
this.#events[eventName] = [];
}
this.#events[eventName].push(callback);
}
/**
* Remove event listener for an event
* @param {EVENT_NAMES} eventName
* @param {Function} callback
*/
off(eventName, callback) {
if (this.#events[eventName]) {
this.#events[eventName] = this.#events[eventName].filter(
(listener) => listener !== callback
);
}
}
/**
* Dispatch event
* @param {EVENT_NAMES} eventName
* @param {*} data
*/
emit(eventName, data) {
if (this.#events[eventName]) {
this.#events[eventName].forEach((listener) => {
listener(data);
});
}
}
}

View file

@ -1,11 +1,13 @@
import { EventEmitter } from "../utils/events.js";
// eslint-disable-next-line no-unused-vars
import { VirtualRoot } from "./virtual-root.js";
export class VirtualBase {
export class VirtualBase extends EventEmitter {
/**
* @param {String} name
*/
constructor(name) {
super();
this.name = name;
}

View file

@ -4,6 +4,14 @@ import { VirtualBase } from "./virtual-base.js";
* A virtual file that can be stored inside a folder
*/
export class VirtualFile extends VirtualBase {
static NON_TEXT_EXTENSIONS = [
"png"
];
static EVENT_NAMES = {
CONTENT_CHANGE: "contentchange"
};
/**
* @param {String} name
* @param {String=} extension
@ -29,6 +37,7 @@ export class VirtualFile extends VirtualBase {
setSource(source) {
this.source = source;
this.content = null;
this.emit(VirtualFile.EVENT_NAMES.CONTENT_CHANGE, this);
return this;
}
@ -40,6 +49,7 @@ export class VirtualFile extends VirtualBase {
setContent(content) {
this.content = content;
this.source = null;
this.emit(VirtualFile.EVENT_NAMES.CONTENT_CHANGE, this);
return this;
}
@ -49,4 +59,25 @@ export class VirtualFile extends VirtualBase {
return `${this.name}.${this.extension}`;
}
/**
* @return {Promise<String|null>}
*/
async read() {
if (this.content != null)
return this.content;
const isText = !VirtualFile.NON_TEXT_EXTENSIONS.includes(this.extension);
if (!isText) {
return this.source;
}
return await fetch(this.source).then((response) =>
response.text()
).catch((error) => {
console.error(`Error while reading file with id "${this.id}":`, error);
return null;
});
}
}

View file

@ -268,4 +268,28 @@ export class VirtualFolder extends VirtualBase {
item.delete();
});
}
/**
* @param {Boolean=false} showHidden
*/
getSubFolders(showHidden = false) {
if (showHidden)
return this.subFolders;
return this.subFolders.filter(({ name }) =>
!name.startsWith(".")
);
}
/**
* @param {Boolean=false} showHidden
*/
getFiles(showHidden = false) {
if (showHidden)
return this.files;
return this.files.filter(({ name }) =>
!name.startsWith(".")
);
}
}

View file

@ -74,8 +74,17 @@ export default class WindowsManager {
/**
* @param {Object} window
*/
focus(window) {
focus(windowId) {
windowId = windowId.toString();
if (!this.windowIds.includes(windowId)) {
console.log(`Failed to focus window ${windowId}: window not found`);
return;
}
const window = this.windows[windowId];
window.lastInteraction = new Date().valueOf();
this.updateWindows(this.windows);
}

View file

@ -0,0 +1,27 @@
import { createContext, useContext } from "react";
import { SettingsManager } from "../../features/settings/settings.js";
import { useVirtualRoot } from "../virtual-drive/VirtualRootContext.js";
const SettingsContext = createContext();
/**
* Note: needs to be inside a virtual root provider
* @returns {React.Provider<SettingsManager>}
*/
export function SettingsProvider({ children }) {
const virtualRoot = useVirtualRoot();
const settingsManager = new SettingsManager(virtualRoot);
return (
<SettingsContext.Provider value={settingsManager}>
{children}
</SettingsContext.Provider>
);
}
/**
* @returns {SettingsManager}
*/
export function useSettings() {
return useContext(SettingsContext);
}

View file

@ -57,15 +57,12 @@ export function useScrollWithShadow(options) {
if (!element)
return;
console.log(element);
setScrollStart(horizontal ? element.scrollLeft : element.scrollTop);
setScrollLength(horizontal ? element.scrollWidth : element.scrollHeight);
setClientLength(horizontal ? element.clientWidth : element.clientHeight);
}, [horizontal]);
const onUpdate = (event) => {
console.log(event);
updateValues(event.target);
};
@ -75,7 +72,6 @@ export function useScrollWithShadow(options) {
};
if (ref.current && !initiated) {
console.log(ref.current);
setInitiated(true);
updateValues(ref.current);
}

View file

@ -37,6 +37,11 @@ function initVirtualRoot(virtualRoot) {
virtualRoot.createFolder("home", (folder) => {
folder.createFolder("prozilla-os", (folder) => {
folder.setAlias("~")
.createFolder(".config", (folder) => {
folder.createFile("desktop", "xml", (file) => {
file.setSource("/config/desktop.xml");
});
})
.createFolder("Images", (folder) => {
folder.createFile("Wallpaper1", "png", (file) => {
file.setSource("/media/wallpapers/Wallpaper1.png")