Added interactable component

This commit is contained in:
Prozilla 2023-12-17 14:39:24 +01:00
parent e19d284a91
commit 2fab7d5193
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
25 changed files with 386 additions and 165 deletions

View file

@ -60,6 +60,7 @@
"error",
"double"
],
"object-curly-spacing": ["warn", "always"],
"default-case": "off",
"arrow-parens": "error",
"space-infix-ops": "warn",

15
public/documents/links.md Normal file
View file

@ -0,0 +1,15 @@
# Links
**ProzillaOS**
- GitHub: https://github.com/Prozilla/Prozilla-OS
**Prozilla**
- Website: https://prozilla.dev/
- Ko-fi: https://ko-fi.com/prozilla
**Crumbling City**
- Website: https://daisygames.org/crumbling-city/
- Steam page: https://store.steampowered.com/app/1520290/Crumbling_City/

View file

@ -1,5 +1,5 @@
import { useShortcuts } from "../../../hooks/utils/keyboard.js";
import { DropdownButton } from "../../utils/DropdownButton.jsx";
import { DropdownButton } from "../../utils/dropdown-button/DropdownButton.jsx";
import styles from "./HeaderMenu.module.css";
/**

View file

@ -112,6 +112,20 @@ export function FileExplorer({ startPath, app, modalsManager }) {
const directory = virtualRoot.navigate(value);
if (directory == null) {
openWindowedModal({
title: "Error",
iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER),
size: new Vector2(300, 150),
Modal: (props) =>
<DialogBox {...props}>
<p>Invalid path: "{value}"</p>
<button data-type={DIALOG_CONTENT_TYPES.CloseButton}>Ok</button>
</DialogBox>
});
return;
}
setCurrentDirectory(directory);
setPath(directory.root ? "/" : directory.path);
}
@ -185,24 +199,24 @@ export function FileExplorer({ startPath, app, modalsManager }) {
<div className={styles.Sidebar}>
<QuickAccessButton name={"Home"} onClick={() => { changeDirectory("~"); }} icon={faHouse}/>
<QuickAccessButton name={"Desktop"} onClick={() => { changeDirectory("~/Desktop"); }} icon={faDesktop}/>
<QuickAccessButton name={"Images"} onClick={() => { changeDirectory("~/Images"); }} icon={faImage}/>
<QuickAccessButton name={"Images"} onClick={() => { changeDirectory("~/Pictures"); }} icon={faImage}/>
<QuickAccessButton name={"Documents"} onClick={() => { changeDirectory("~/Documents"); }} icon={faFileLines}/>
</div>
<div id="main" className={styles.Main}>
<DirectoryList
directory={currentDirectory}
showHidden={showHidden}
onClickFile={(event, file) => {
event.preventDefault();
windowsManager.openFile(file);
}}
onClickFolder={(event, folder) => {
changeDirectory(folder.linkedPath ?? folder.name);
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
/>
</div>
<DirectoryList
directory={currentDirectory}
id="main"
className={styles.Main}
showHidden={showHidden}
onClickFile={(event, file) => {
event.preventDefault();
windowsManager.openFile(file, { mode: "view" });
}}
onClickFolder={(event, folder) => {
changeDirectory(folder.linkedPath ?? folder.name);
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
/>
</div>
<span className={styles.Footer}>
<p className={utilStyles["Text-light"]}>

View file

@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { VirtualFile } from "../../../../features/virtual-drive/virtualFile.js";
import { VirtualFolder } from "../../../../features/virtual-drive/virtualFolder.js";
import { Interactable } from "../../../utils/interactable/Interactable.jsx";
import styles from "./DirectoryList.module.css";
import { ImagePreview } from "./ImagePreview.jsx";
@ -19,36 +21,145 @@ import { ImagePreview } from "./ImagePreview.jsx";
* @param {object} props
* @param {VirtualFolder} props.directory
* @param {boolean} props.showHidden
* @param {string} props.folderClassname
* @param {string} props.fileClassname
* @param {string} props.folderClassName
* @param {string} props.fileClassName
* @param {string} props.className
* @param {fileEvent} props.onContextMenuFile
* @param {folderEvent} props.onContextMenuFolder
* @param {fileEvent} props.onClickFile
* @param {folderEvent} props.onClickFolder
*/
export function DirectoryList({ directory, showHidden = false, folderClassname, fileClassname,
onContextMenuFile, onContextMenuFolder, onClickFile, onClickFolder }) {
if (!directory)
return null;
export function DirectoryList({ directory, showHidden = false, folderClassName, fileClassName, className,
onContextMenuFile, onContextMenuFolder, onClickFile, onClickFolder, ...props }) {
const [selectedFolders, setSelectedFolders] = useState([]);
const [selectedFiles, setSelectedFiles] = useState([]);
const ref = useRef(null);
const [rectSelectStart, setRectSelectStart] = useState(null);
const [rectSelectEnd, setRectSelectEnd] = useState(null);
useEffect(() => {
clearSelection();
}, [directory]);
useEffect(() => {
const onMoveRectSelect = (event) => {
if (rectSelectStart == null)
return;
event.preventDefault();
setRectSelectEnd({ x: event.clientX, y: event.clientY });
};
const onStopRectSelect = (event) => {
if (rectSelectStart == null || rectSelectEnd == null) {
setRectSelectStart(null);
setRectSelectEnd(null);
return;
}
event.preventDefault();
setRectSelectStart(null);
setRectSelectEnd(null);
};
document.addEventListener("mousemove", onMoveRectSelect);
document.addEventListener("pointermove", onMoveRectSelect);
document.addEventListener("mouseup", onStopRectSelect);
document.addEventListener("pointerup", onStopRectSelect);
return () => {
document.removeEventListener("mousemove", onMoveRectSelect);
document.removeEventListener("pointermove", onMoveRectSelect);
document.removeEventListener("mouseup", onStopRectSelect);
document.removeEventListener("pointerup", onStopRectSelect);
};
});
if (!directory)
return;
const clearSelection = () => {
setSelectedFolders([]);
setSelectedFiles([]);
};
const selectFolder = (folder, exclusive = false) => {
setSelectedFolders(exclusive ? [folder.id] : [...selectedFolders, folder.id]);
if (exclusive)
setSelectedFiles([]);
};
const selectFile = (file, exclusive = false) => {
setSelectedFiles(exclusive ? [file.id] : [...selectedFiles, file.id]);
if (exclusive)
setSelectedFolders([]);
};
const onStartRectSelect = (event) => {
setRectSelectStart({ x: event.clientX, y: event.clientY });
};
const getRectSelectStyle = () => {
let x, y, width, height = null;
const containerRect = ref.current?.getBoundingClientRect();
if (rectSelectStart.x < rectSelectEnd.x) {
x = rectSelectStart.x;
width = rectSelectEnd.x - rectSelectStart.x;
} else {
x = rectSelectEnd.x;
width = rectSelectStart.x - rectSelectEnd.x;
}
if (rectSelectStart.y < rectSelectEnd.y) {
y = rectSelectStart.y;
height = rectSelectEnd.y - rectSelectStart.y;
} else {
y = rectSelectEnd.y;
height = rectSelectStart.y - rectSelectEnd.y;
}
if (containerRect) {
x -= containerRect.x;
y -= containerRect.y;
}
return { top: y, left: x, width, height };
};
const classNames = [styles.Container];
const folderClassNames = [styles["Folder-button"]];
const fileClassNames = [styles["File-button"]];
if (folderClassname)
folderClassNames.push(folderClassname);
if (fileClassname)
fileClassNames.push(fileClassname);
if (className)
classNames.push(className);
if (folderClassName)
folderClassNames.push(folderClassName);
if (fileClassName)
fileClassNames.push(fileClassName);
return <>
return <div
ref={ref}
className={classNames.join(" ")}
onClick={clearSelection}
onMouseDown={onStartRectSelect}
onPointerDown={onStartRectSelect}
{...props}
>
{rectSelectStart != null && rectSelectEnd != null
? <div className={styles["Selection-rect"]} style={getRectSelectStyle()}/>
: null
}
{directory?.getSubFolders(showHidden)?.map((folder) =>
<button
<Interactable
key={folder.id}
tabIndex={0}
className={folderClassNames.join(" ")}
data-selected={selectedFolders.includes(folder.id)}
onContextMenu={(event) => {
onContextMenuFolder?.(event, folder);
}}
onClick={(event) => {
selectFolder(folder, !event.ctrlKey);
}}
onDoubleClick={(event) => {
onClickFolder?.(event, folder);
}}
>
@ -56,17 +167,21 @@ export function DirectoryList({ directory, showHidden = false, folderClassname,
<ImagePreview source={folder.getIconUrl()}/>
</div>
<p>{folder.name}</p>
</button>
</Interactable>
)}
{directory?.getFiles(showHidden)?.map((file) =>
<button
<Interactable
key={file.id}
tabIndex={0}
className={fileClassNames.join(" ")}
data-selected={selectedFiles.includes(file.id)}
onContextMenu={(event) => {
onContextMenuFile?.(event, file);
}}
onClick={(event) => {
selectFile(file, !event.ctrlKey);
}}
onDoubleClick={(event) => {
onClickFile?.(event, file);
}}
>
@ -74,7 +189,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassname,
<ImagePreview source={file.getIconUrl()}/>
</div>
<p>{file.id}</p>
</button>
</Interactable>
)}
</>;
</div>;
}

View file

@ -1,3 +1,9 @@
.Container {
position: relative;
width: 100%;
height: 100%;
}
.File-button, .Folder-button {
display: flex;
gap: 0.25rem;
@ -15,11 +21,16 @@
transition: background-color 100ms ease-in-out;
}
.File-button[data-selected=true],
.Folder-button[data-selected=true] {
background-color: hsla(var(--background-color-a-hsl), 40%) !important;
}
.File-button:hover,
.Folder-button:hover,
.File-button:focus-visible,
.Folder-button:focus-visible {
background-color: hsla(var(--background-color-a-hsl), 35%);
background-color: hsla(var(--background-color-a-hsl), 20%);
}
.File-button p, .Folder-button p {
@ -37,4 +48,12 @@
width: 50%;
height: auto;
aspect-ratio: 1;
}
.Selection-rect {
opacity: 25%;
position: absolute;
border-radius: 0.25rem;
background-color: var(--blue-b);
border: 0.25rem solid var(--blue-a);
}

View file

@ -17,7 +17,7 @@ export function MediaViewer({ file, close, setTitle }) {
if (file == null) {
setTimeout(() => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Images" });
windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Pictures" });
close();
}, 10);
return;

View file

@ -1,4 +1,4 @@
import { Button } from "../../utils/Button.jsx";
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";

View file

@ -1,8 +1,8 @@
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 { 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";

View file

@ -85,29 +85,27 @@ export const Desktop = memo(() => {
onContextMenu={onContextMenu}
>
<ModalsView modalsManager={modalsManager} modals={modals}/>
<div className={styles.Content}>
<DirectoryList
directory={directory}
fileClassname={styles["Item"]}
folderClassname={styles["Item"]}
onClickFile={(event, file) => {
event.preventDefault();
<DirectoryList
directory={directory}
className={styles.Content}
fileClassName={styles["Item"]}
folderClassName={styles["Item"]}
onClickFile={(event, file) => {
event.preventDefault();
const options = {};
if (file.name === "info.md") {
options.mode = "view";
options.size = new Vector2(575, 675);
}
const options = { mode: "view" };
if (file.name === "info.md") {
options.size = new Vector2(575, 675);
}
windowsManager.openFile(file, options);
}}
onClickFolder={(event, { linkedPath, path }) => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: linkedPath ?? path });
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
/>
</div>
windowsManager.openFile(file, options);
}}
onClickFolder={(event, { linkedPath, path }) => {
windowsManager.open(APPS.FILE_EXPLORER, { startPath: linkedPath ?? path });
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
/>
{wallpaper
? <img src={wallpaper} className={styles.Wallpaper} alt="Desktop wallpaper" onError={onError}/>
: null

View file

@ -83,7 +83,7 @@ export function HomeMenu({ active, setActive, search }) {
</button>
<button title="Images" tabIndex={tabIndex} onClick={() => {
setActive(false);
windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Images" });
windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Pictures" });
}}>
<FontAwesomeIcon icon={faImage}/>
</button>

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import styles from "./DropdownButton.module.css";
import OutsideClickListener from "../../hooks/utils/outsideClick.js";
import { formatShortcut } from "../../features/utils/string.js";
import OutsideClickListener from "../../../hooks/utils/outsideClick.js";
import { formatShortcut } from "../../../features/utils/string.js";
/**
* @param {object} props

View file

@ -0,0 +1,34 @@
import { useState } from "react";
import { INTERACTIBLE_DOUBLE_CLICK_DELAY } from "../../../constants/utils.js";
let timeoutId = null;
export function Interactable({ onClick, onDoubleClick, children, ...props }) {
const [clicked, setClicked] = useState(false);
const onButtonClick = (event) => {
event.preventDefault();
event.stopPropagation();
if (timeoutId != null)
clearTimeout(timeoutId);
if (clicked) {
setClicked(false);
onDoubleClick?.(event);
return;
}
setClicked(true);
onClick?.(event);
timeoutId = setTimeout(() => {
setClicked(false);
}, INTERACTIBLE_DOUBLE_CLICK_DELAY);
};
return <button {...props} onClick={onButtonClick}>
{children}
</button>;
}

View file

@ -1,4 +1,4 @@
import { clamp } from "../../features/math/clamp.js";
import { clamp } from "../../../features/math/clamp.js";
import styles from "./ProgressBar.module.css";
/**

View file

@ -1 +1 @@
export const WALLPAPERS_PATH = "~/Images/Wallpapers";
export const WALLPAPERS_PATH = "~/Pictures/Wallpapers";

1
src/constants/utils.js Normal file
View file

@ -0,0 +1 @@
export const INTERACTIBLE_DOUBLE_CLICK_DELAY = 250;

View file

@ -15,7 +15,7 @@ export default class Application {
}
WindowContent = (props) => {
props = {...props, ...this.windowOptions};
props = { ...props, ...this.windowOptions };
if (this.windowContent == null) {
return null;

View file

@ -0,0 +1,117 @@
import { APPS } from "../../constants/applications.js";
import { WALLPAPERS } from "../../constants/desktop.js";
import AppsManager from "../applications/applications.js";
import { VirtualRoot } from "./virtualRoot.js";
/**
* Loads default data on the virtual root
* @param {VirtualRoot} virtualRoot
*/
export function loadDefaultData(virtualRoot) {
virtualRoot.createFolder("bin", (folder) => {
folder.createFiles([
{ name: "echo" },
{ name: "cd" },
{ name: "ls" },
{ name: "clear" },
]);
});
virtualRoot.createFolder("dev", (folder) => {
folder.createFiles([
{ name: "null" },
{ name: "zero" },
{ name: "random" },
]);
});
virtualRoot.createFolder("etc");
virtualRoot.createFolder("usr", (folder) => {
folder.createFolders(["bin", "sbin", "lib", "share"]);
});
const linkedPaths = {};
virtualRoot.createFolder("home", (folder) => {
folder.createFolder("prozilla-os", (folder) => {
folder.setAlias("~")
.createFolder(".config", (folder) => {
folder.createFile("desktop", "xml", (file) => {
file.setSource("/config/desktop.xml");
}).createFile("taskbar", "xml", (file) => {
file.setSource("/config/taskbar.xml");
});
})
.createFolder("Pictures", (folder) => {
folder.setIconUrl(AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder-images"));
folder.createFolder("Wallpapers", (folder) => {
folder.setProtected(true);
for (let i = 0; i < WALLPAPERS.length; i++) {
const source = WALLPAPERS[i];
folder.createFile(`Wallpaper${i + 1}`, "png", (file) => {
file.setSource(source);
});
}
}).createFile("ProzillaOS", "png", (file) => {
file.setSource("/assets/banner-logo-title.png");
}).createFolder("Crumbling City", (folder) => {
folder.createFile("Japan", "png", (file) => {
file.setSource("https://daisygames.org/media/Games/Crumbling%20City/CrumblingCityRelease.png");
}).createFile("City Center", "png", (file) => {
file.setSource("https://daisygames.org/media/Games/Crumbling%20City/Screenshot_City_Firegun.png");
}).createFile("Farms", "png", (file) => {
file.setSource("https://daisygames.org/media/Games/Crumbling%20City/Screenshot_Farms_Hammer.png");
});
});
linkedPaths.images = folder.path;
})
.createFolder("Documents", (folder) => {
folder.setIconUrl(AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder-text"));
folder.createFile("text", "txt", (file) => {
file.setContent("Hello world!");
}).createFile("info", "md", (file) => {
file.setProtected(true)
.setSource("/documents/info.md")
.setIconUrl(AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file-info"));
linkedPaths.info = file.path;
}).createFile("links", "md", (file) => {
file.setProtected(true)
.setSource("/documents/links.md");
linkedPaths.links = file.path;
});
linkedPaths.documents = folder.path;
})
.createFolder("Desktop", (folder) => {
folder.createFileLink("info.md", (fileLink) => {
fileLink.setLinkedPath(linkedPaths.info);
}).createFileLink("links.md", (fileLink) => {
fileLink.setLinkedPath(linkedPaths.links);
}).createFolderLink("Pictures", (folderLink) => {
folderLink.setLinkedPath(linkedPaths.images);
}).createFolderLink("Documents", (folderLink) => {
folderLink.setLinkedPath(linkedPaths.documents);
});
});
});
});
virtualRoot.createFolder("lib");
virtualRoot.createFolder("sbin");
virtualRoot.createFolder("tmp");
virtualRoot.createFolder("var");
virtualRoot.createFolder("boot");
virtualRoot.createFolder("proc", (folder) => {
folder.createFiles([
{ name: "cpuinfo" },
{ name: "meminfo" },
]);
});
virtualRoot.createFolder("var");
virtualRoot.createFolder("opt");
virtualRoot.createFolder("media");
virtualRoot.createFolder("mnt");
virtualRoot.createFolder("srv");
}

View file

@ -32,7 +32,7 @@ export class VirtualBase extends EventEmitter {
/**
* @param {string} alias
* @returns {ThisType}
* @returns {VirtualBase}
*/
setAlias(alias) {
if (this.alias === alias || !this.canBeEdited)

View file

@ -29,7 +29,7 @@ export class VirtualFolder extends VirtualBase {
/**
* @param {string} alias
* @returns {ThisType}
* @returns {VirtualFolder}
*/
setAlias(alias) {
return super.setAlias(alias);
@ -39,7 +39,7 @@ export class VirtualFolder extends VirtualBase {
* Returns true if this folder contains a file matching a name and extension
* @param {string} name
* @param {string} extension
* @returns {ThisType}
* @returns {VirtualFolder}
*/
hasFile(name, extension) {
return this.findFile(name, extension) !== null;
@ -130,7 +130,7 @@ export class VirtualFolder extends VirtualBase {
if (!this.canBeEdited)
return;
files.forEach(({name, extension}) => {
files.forEach(({ name, extension }) => {
this.createFile(name, extension);
});

View file

@ -1,8 +1,6 @@
import { APPS } from "../../constants/applications.js";
import { WALLPAPERS } from "../../constants/desktop.js";
import AppsManager from "../applications/applications.js";
import { StorageManager } from "../storage/storageManager.js";
import { VirtualFolderLink } from "./VirtualFolderLink.js";
import { loadDefaultData } from "./defaultData.js";
import { VirtualFile } from "./virtualFile.js";
import { VirtualFolder } from "./virtualFolder.js";
@ -24,98 +22,7 @@ export class VirtualRoot extends VirtualFolder {
}
loadDefaultData() {
this.createFolder("bin", (folder) => {
folder.createFiles([
{ name: "echo" },
{ name: "cd" },
{ name: "ls" },
{ name: "clear" },
]);
});
this.createFolder("dev", (folder) => {
folder.createFiles([
{ name: "null" },
{ name: "zero" },
{ name: "random" },
]);
});
this.createFolder("etc");
this.createFolder("usr", (folder) => {
folder.createFolders(["bin", "sbin", "lib", "share"]);
});
const linkedPaths = {};
this.createFolder("home", (folder) => {
folder.createFolder("prozilla-os", (folder) => {
folder.setAlias("~")
.createFolder(".config", (folder) => {
folder.createFile("desktop", "xml", (file) => {
file.setSource("/config/desktop.xml");
}).createFile("taskbar", "xml", (file) => {
file.setSource("/config/taskbar.xml");
});
})
.createFolder("Images", (folder) => {
folder.setIconUrl(AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder-images"));
folder.createFolder("Wallpapers", (folder) => {
folder.setProtected(true);
for (let i = 0; i < WALLPAPERS.length; i++) {
const source = WALLPAPERS[i];
folder.createFile(`Wallpaper${i + 1}`, "png", (file) => {
file.setSource(source);
});
}
}).createFile("ProzillaOS", "png", (file) => {
file.setSource("/assets/banner-logo-title.png");
});
linkedPaths.images = folder.path;
})
.createFolder("Documents", (folder) => {
folder.setIconUrl(AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "folder-text"));
folder.createFile("text", "txt", (file) => {
file.setContent("Hello world!");
}).createFile("info", "md", (file) => {
file.setProtected(true)
.setSource("/documents/info.md")
.setIconUrl(AppsManager.getAppIconUrl(APPS.FILE_EXPLORER, "file-info"));
linkedPaths.info = file.path;
});
linkedPaths.documents = folder.path;
})
.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);
});
});
});
});
this.createFolder("lib");
this.createFolder("sbin");
this.createFolder("tmp");
this.createFolder("var");
this.createFolder("boot");
this.createFolder("proc", (folder) => {
folder.createFiles([
{ name: "cpuinfo" },
{ name: "meminfo" },
]);
});
this.createFolder("var");
this.createFolder("opt");
this.createFolder("media");
this.createFolder("mnt");
this.createFolder("srv");
loadDefaultData(this);
}
loadData() {
@ -132,7 +39,7 @@ export class VirtualRoot extends VirtualFolder {
if (object == null)
return;
const shortcuts = {...object.scs};
const shortcuts = { ...object.scs };
const addFile = ({
nam: name,