Added virtual file & folder links

This commit is contained in:
Prozilla 2023-12-11 20:47:45 +01:00
parent 9718530c9f
commit a307eda1d1
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
11 changed files with 495 additions and 52 deletions

View file

@ -2,8 +2,7 @@ import { useState } from "react";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
import styles from "./FileExplorer.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFile, faFileLines, faFolder, faHouse, faImage, faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
import { VirtualFile } from "../../../features/virtual-drive/virtualFile.js";
import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js";
import { useContextMenu } from "../../../hooks/modals/contextMenu.js";
import { useModals } from "../../../hooks/modals/modals.js";
@ -12,31 +11,8 @@ import { QuickAccessButton } from "./QuickAccessButton.jsx";
import { useDialogBox } from "../../../hooks/modals/dialogBox.js";
import Vector2 from "../../../features/math/vector2.js";
import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js";
/**
* @param {object} props
* @param {VirtualFile} props.file
*/
function FilePreview({ file }) {
let preview = null;
switch (file.extension) {
case "png":
preview = (<div className={styles["File-button-preview"]}>
<img src={file.source} alt={file.id} draggable="false"/>
</div>);
break;
case "txt":
case "md":
preview = <FontAwesomeIcon icon={faFileLines}/>;
break;
default:
preview = <FontAwesomeIcon icon={faFile}/>;
break;
}
return preview;
}
import { FilePreview } from "./FilePreview.jsx";
import { FolderPreview } from "./FolderPreview.jsx";
export function FileExplorer({ startPath, app }) {
const virtualRoot = useVirtualRoot();
@ -60,13 +36,13 @@ export function FileExplorer({ startPath, app }) {
"Delete": ({ name }) => { currentDirectory.findSubFolder(name)?.delete(); }
}
});
const { onContextMenu: onNew } = useContextMenu({
modalsManager,
options: {
"File": () => { currentDirectory.createFile("New File"); },
"Folder": () => { currentDirectory.createFolder("New Folder"); }
}
});
// const { onContextMenu: onNew } = useContextMenu({
// modalsManager,
// options: {
// "File": () => { currentDirectory.createFile("New File"); },
// "Folder": () => { currentDirectory.createFolder("New Folder"); }
// }
// });
const { onDialogBox } = useDialogBox({ modalsManager });
const changeDirectory = (path, absolute = false) => {
@ -159,17 +135,17 @@ export function FileExplorer({ startPath, app }) {
<QuickAccessButton name={"Images"} onClick={() => { changeDirectory("~/Images"); }} icon={faImage}/>
</div>
<div id="main" className={styles.Main}>
{currentDirectory?.getSubFolders(showHidden)?.map(({ name }, index) =>
{currentDirectory?.getSubFolders(showHidden)?.map((folder, index) =>
<button key={index} tabIndex={0} className={styles["Folder-button"]}
onContextMenu={(event) => {
onContextMenuFolder(event, { name });
onContextMenuFolder(event, { name: folder.name });
}}
onClick={() => {
changeDirectory(name);
changeDirectory(folder.linkedPath ?? folder.name);
}}
>
<FontAwesomeIcon icon={faFolder}/>
<p>{name}</p>
<FolderPreview folder={folder}/>
<p>{folder.name}</p>
</button>
)}
{currentDirectory?.getFiles(showHidden)?.map((file, index) =>

View file

@ -163,14 +163,45 @@
border-radius: 0.5rem;
}
.File-button svg, .Folder-button svg {
.File-button svg, .Folder-button > div {
width: 50%;
height: auto;
aspect-ratio: 1;
}
.Folder-button > div > svg {
width: 100%;
height: 100%;
aspect-ratio: 1;
}
.File-button p, .Folder-button p {
max-width: 100%;
margin: 0;
word-wrap: break-word;
}
.Folder-button > div {
position: relative;
}
.Folder-link-icon {
position: absolute;
display: flex;
justify-content: flex-end;
align-items: flex-end;
top: 0;
left: 0;
width: 92.5%;
height: 87.5%;
}
.Folder-link-icon svg {
width: 55%;
height: auto;
aspect-ratio: 1;
}
.Folder-link-icon svg * {
fill: var(--background-color-b);
}

View file

@ -0,0 +1,31 @@
import { faFile, faFileInvoice, faFileLines } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./FileExplorer.module.css";
import { VirtualFile } from "../../../features/virtual-drive/virtualFile.js";
/**
* @param {object} props
* @param {VirtualFile} props.file
*/
export function FilePreview({ file }) {
let preview = null;
switch (file.extension) {
case "png":
preview = (<div className={styles["File-button-preview"]}>
<img src={file.source} alt={file.id} draggable="false"/>
</div>);
break;
case "txt":
preview = <FontAwesomeIcon icon={faFileLines}/>;
break;
case "md":
preview = <FontAwesomeIcon icon={faFileInvoice}/>;
break;
default:
preview = <FontAwesomeIcon icon={faFile}/>;
break;
}
return preview;
}

View file

@ -0,0 +1,18 @@
import { faFolder, faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./FileExplorer.module.css";
import { VirtualFolder } from "../../../features/virtual-drive/virtualFolder.js";
/**
* @param {object} props
* @param {VirtualFolder} props.folder
*/
export function FolderPreview({ folder }) {
return (<div>
<FontAwesomeIcon icon={faFolder}/>
{folder.linkedPath
? <div className={styles["Folder-link-icon"]}><FontAwesomeIcon icon={faLink}/></div>
: null
}
</div>);
}

View file

@ -61,7 +61,7 @@ export function HomeMenu({ active, setActive, search }) {
<div className={classNames.join(" ")}>
<div className={styles["Container-inner"]}>
<div className={styles.Buttons}>
<button title="Shut Down" tabIndex={tabIndex} onClick={() => { closeViewport(); }}>
<button title="Shut Down" tabIndex={tabIndex} onClick={() => { closeViewport(true); }}>
<FontAwesomeIcon icon={faPowerOff}/>
</button>
<button title="Settings" tabIndex={tabIndex} onClick={() => {

View file

@ -0,0 +1,105 @@
import { VirtualFile } from "./virtualFile.js";
/**
* A link that points to a virtual file
*/
export class VirtualFileLink extends VirtualFile {
/**
* @param {string} name
* @param {VirtualFile} linkedFile
*/
constructor(name, linkedFile) {
super(name);
this.linkedFile = linkedFile;
}
/**
* @param {VirtualFile} file
* @returns {VirtualFileLink}
*/
setLinkedFile(file) {
this.linkedFile = file;
if (file) {
this.linkedPath = file.path;
this.extension = file.extension;
this.content = file.content;
this.source = file.source;
}
return this;
}
/**
* @param {string} path
* @returns {VirtualFileLink}
*/
setLinkedPath(path) {
if (this.linkedFile && this.linkedFile.path === path)
return;
const target = this.parent.navigate(path);
if (target instanceof VirtualFile) {
this.setLinkedFile(target);
} else {
this.linkedFile = null;
}
return this;
}
/**
* @returns {boolean}
*/
isValid() {
if (!this.linkedPath)
return false;
this.setLinkedPath(this.linkedPath);
return (this.linkedFile != null);
}
/**
* @returns {object | null}
*/
toJSON() {
if (this.linkedPath == null)
return;
const object = {
nam: this.name,
lnk: this.linkedPath
};
return object;
}
// Point certain methods to linked file
/** @type {VirtualFile["setAlias"]} */
setAlias(...args) {
if (this.isValid()) return this.linkedFile.setAlias(...args);
}
/** @type {VirtualFile["setSource"]} */
setSource(...args) {
if (this.isValid()) return this.linkedFile.setSource(...args);
}
/** @type {VirtualFile["setContent"]} */
setContent(...args) {
if (this.isValid()) return this.linkedFile.setContent(...args);
}
/** @type {VirtualFile["id"]} */
get id() {
return this.isValid() ? this.linkedFile.id : null;
}
/** @type {VirtualFile["read"]} */
read(...args) {
if (this.isValid()) return this.linkedFile.read(...args);
}
}

View file

@ -0,0 +1,133 @@
import { VirtualFolder } from "./virtualFolder.js";
/**
* A link that points to a virtual folder
*/
export class VirtualFolderLink extends VirtualFolder {
/**
* @param {string} name
* @param {VirtualFolder} linkedFolder
*/
constructor(name, linkedFolder) {
super(name);
this.linkedFolder = linkedFolder;
}
/**
* @param {VirtualFolder} folder
* @returns {VirtualFolderLink}
*/
setLinkedFolder(folder) {
this.linkedFolder = folder;
if (folder) {
this.linkedPath = folder.path;
this.type = folder.type;
}
return this;
}
/**
* @param {string} path
* @returns {VirtualFolderLink}
*/
setLinkedPath(path) {
if (this.linkedFolder && this.linkedFolder.path === path)
return;
const target = this.navigate(path);
if (target instanceof VirtualFolder) {
this.setLinkedFolder(target);
} else {
this.linkedFolder = null;
}
return this;
}
/**
* @returns {boolean}
*/
isValid() {
if (!this.linkedPath)
return false;
this.setLinkedPath(this.linkedPath);
return (this.linkedFolder != null);
}
/**
* @returns {object | null}
*/
toJSON() {
if (this.linkedPath == null)
return;
const object = {
nam: this.name,
lnk: this.linkedPath
};
return object;
}
// Point certain methods to linked folder
/** @type {VirtualFolder["setAlias"]} */
setAlias(...args) {
if (this.isValid()) return this.linkedFolder.setAlias(...args);
}
/** @type {VirtualFolder["hasFile"]} */
hasFile(...args) {
if (this.isValid()) return this.linkedFolder.hasFile(...args);
}
/** @type {VirtualFolder["hasFolder"]} */
hasFolder(...args) {
if (this.isValid()) return this.linkedFolder.hasFolder(...args);
}
/** @type {VirtualFolder["findFile"]} */
findFile(...args) {
if (this.isValid()) return this.linkedFolder.findFile(...args);
}
/** @type {VirtualFolder["findSubFolder"]} */
findSubFolder(...args) {
if (this.isValid()) return this.linkedFolder.findSubFolder(...args);
}
/** @type {VirtualFolder["createFile"]} */
createFile(...args) {
if (this.isValid()) return this.linkedFolder.createFile(...args);
}
/** @type {VirtualFolder["createFile"]} */
createFiles(...args) {
if (this.isValid()) return this.linkedFolder.createFiles(...args);
}
/** @type {VirtualFolder["createFolder"]} */
createFolder(...args) {
if (this.isValid()) return this.linkedFolder.createFolder(...args);
}
/** @type {VirtualFolder["createFolders"]} */
createFolders(...args) {
if (this.isValid()) return this.linkedFolder.createFolders(...args);
}
/** @type {VirtualFolder["getFiles"]} */
getFiles(...args) {
if (this.isValid()) return this.linkedFolder.getFiles(...args);
}
/** @type {VirtualFolder["getSubFolders"]} */
getSubFolders(...args) {
if (this.isValid()) return this.linkedFolder.getSubFolders(...args);
}
}

View file

@ -149,6 +149,20 @@ export class VirtualBase extends EventEmitter {
return root;
}
/**
* @returns {boolean}
*/
isFile() {
return false;
}
/**
* @returns {boolean}
*/
isFolder() {
return false;
}
/**
* @returns {object | null}
*/

View file

@ -93,6 +93,13 @@ export class VirtualFile extends VirtualBase {
});
}
/**
* @returns {boolean}
*/
isFile() {
return true;
}
/**
* @returns {object | null}
*/

View file

@ -1,4 +1,6 @@
import { removeFromArray } from "../utils/array.js";
import { VirtualFileLink } from "./VirtualFileLink.js";
import { VirtualFolderLink } from "./VirtualFolderLink.js";
import { VirtualBase } from "./virtualBase.js";
import { VirtualFile } from "./virtualFile.js";
@ -131,6 +133,51 @@ export class VirtualFolder extends VirtualBase {
return this;
}
/**
* @callback createFileLinkCallback
* @param {VirtualFileLink} newFileLink
*/
/**
* Creates a file link with a name
* @param {string} name
* @param {createFileLinkCallback} callback
* @returns {VirtualFolder}
*/
createFileLink(name, callback) {
if (!this.canBeEdited)
return;
let newFile = this.findFile(name);
if (newFile == null) {
newFile = new VirtualFileLink(name);
this.files.push(newFile);
newFile.parent = this;
}
callback?.(newFile);
newFile.confirmChanges();
return this;
}
/**
* Creates files based on an array of objects with file names and extensions
* @param {object[]} files
* @param {string} files.name
* @returns {VirtualFolder}
*/
createFileLinks(files) {
if (!this.canBeEdited)
return;
files.forEach(({ name }) => {
this.createFileLink(name);
});
this.confirmChanges();
return this;
}
/**
* @callback createFolderCallback
* @param {VirtualFolder} newFolder
@ -139,8 +186,8 @@ export class VirtualFolder extends VirtualBase {
/**
* Creates a folder with a name
* @param {string} name
* @returns {VirtualFolder}
* @param {createFolderCallback} callback
* @returns {VirtualFolder}
*/
createFolder(name, callback) {
if (!this.canBeEdited)
@ -160,14 +207,58 @@ export class VirtualFolder extends VirtualBase {
/**
* Creates folders based on an array of folder names
* @param {string[]} folders
* @param {string[]} names
* @returns {VirtualFolder}
*/
createFolders(folders) {
createFolders(names) {
if (!this.canBeEdited)
return;
folders.forEach((name) => {
names.forEach((name) => {
this.createFolder(name);
});
this.confirmChanges();
return this;
}
/**
* @callback createFolderLinkCallback
* @param {VirtualFolderLink} newFolderLink
*/
/**
* Creates a folder link with a name
* @param {string} name
* @param {createFolderLinkCallback} callback
* @returns {VirtualFolderLink}
*/
createFolderLink(name, callback) {
if (!this.canBeEdited)
return;
let newFolder = this.findSubFolder(name);
if (newFolder == null) {
newFolder = new VirtualFolderLink(name);
this.subFolders.push(newFolder);
newFolder.parent = this;
}
callback?.(newFolder);
newFolder.confirmChanges();
return this;
}
/**
* Creates folder links based on an array of folder names
* @param {string[]} names
* @returns {VirtualFolder}
*/
createFolderLinks(names) {
if (!this.canBeEdited)
return;
names.forEach((name) => {
this.createFolder(name);
});
@ -177,7 +268,7 @@ export class VirtualFolder extends VirtualBase {
/**
* Removes a file or folder from this folder
* @param {VirtualFile | VirtualFolder} child
* @param {VirtualFile | VirtualFolder | VirtualFolderLink} child
*/
remove(child) {
if (!this.canBeEdited)
@ -185,9 +276,9 @@ export class VirtualFolder extends VirtualBase {
child.parent = null;
if (child instanceof VirtualFile) {
if (child.isFile()) {
removeFromArray(child, this.files);
} else if (child instanceof VirtualFolder) {
} else if (child.isFolder()) {
removeFromArray(child, this.subFolders);
}
@ -290,6 +381,13 @@ export class VirtualFolder extends VirtualBase {
);
}
/**
* @returns {boolean}
*/
isFolder() {
return true;
}
/**
* @returns {object | null}
*/

View file

@ -1,5 +1,6 @@
import { WALLPAPERS } from "../../constants/desktop.js";
import { StorageManager } from "../storage/storageManager.js";
import { VirtualFolderLink } from "./VirtualFolderLink.js";
import { VirtualFile } from "./virtualFile.js";
import { VirtualFolder } from "./virtualFolder.js";
@ -43,6 +44,8 @@ export class VirtualRoot extends VirtualFolder {
this.createFolder("usr", (folder) => {
folder.createFolders(["bin", "sbin", "lib", "share"]);
});
const linkedPaths = {};
this.createFolder("home", (folder) => {
folder.createFolder("prozilla-os", (folder) => {
@ -53,6 +56,7 @@ export class VirtualRoot extends VirtualFolder {
});
})
.createFolder("Images", (folder) => {
linkedPaths.images = folder.path;
folder.createFolder("Wallpapers", (folder) => {
folder.setProtected(true);
for (let i = 0; i < WALLPAPERS.length; i++) {
@ -61,16 +65,28 @@ export class VirtualRoot extends VirtualFolder {
file.setSource(source);
});
}
}).createFile("ProzillaOS", "png", (file) => {
file.setSource("/media/banner-logo-title.png");
});
})
.createFolder("Documents", (folder) => {
linkedPaths.documents = folder.path;
folder.createFile("text", "txt", (file) => {
file.setContent("Hello world!");
}).createFile("info", "md", (file) => {
linkedPaths.info = file.path;
file.setProtected(true).setSource("/documents/info.md");
});
})
.createFolder("Desktop");
.createFolder("Desktop", (folder) => {
folder.createFileLink("info.md", (fileLink) => {
fileLink.setLinkedPath(linkedPaths.info);
}).createFolderLink("Images", (folderLink) => {
folderLink.setLinkedPath(linkedPaths.images);
}).createFolderLink("Documents", (folderLink) => {
folderLink.setLinkedPath(linkedPaths.documents);
});
});
});
});
@ -110,7 +126,14 @@ export class VirtualRoot extends VirtualFolder {
const shortcuts = {...object.scs};
const addFile = ({ nam: name, ext: extension, src: source, cnt: content }, parent = this) => {
const addFile = ({ nam: name, ext: extension, src: source, cnt: content, lnk: link }, parent = this) => {
if (link) {
parent.createFileLink(name, (fileLink) => {
fileLink.setLinkedPath(link);
});
return;
}
parent.createFile(name, extension, (file) => {
if (source != null) {
file.setSource(source);
@ -120,7 +143,14 @@ export class VirtualRoot extends VirtualFolder {
});
};
const addFolder = ({ nam: name, fds: folders, fls: files }, parent = this) => {
const addFolder = ({ nam: name, fds: folders, fls: files, lnk: link }, parent = this) => {
if (link) {
parent.createFolderLink(name, (folderLink) => {
folderLink.setLinkedPath(link);
});
return;
}
parent.createFolder(name, (folder) => {
if (Object.values(shortcuts).includes(folder.displayPath)) {
let alias;
@ -193,7 +223,7 @@ export class VirtualRoot extends VirtualFolder {
/**
* Adds a shortcut to a file or folder
* @param {string} name
* @param {VirtualFile | VirtualFolder} destination
* @param {VirtualFile | VirtualFolder | VirtualFolderLink} destination
* @returns {VirtualRoot}
*/
addShortcut(name, destination) {