Enabled strict type checking

This commit is contained in:
Prozilla 2024-06-16 21:45:00 +02:00
parent ea4e274798
commit 88af6bf7ad
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
157 changed files with 2811 additions and 1085 deletions

View file

@ -2,6 +2,7 @@
"recommendations": [ "recommendations": [
"bierner.github-markdown-preview", "bierner.github-markdown-preview",
"yahyabatulu.vscode-markdown-alert", "yahyabatulu.vscode-markdown-alert",
"clinyong.vscode-css-modules" "clinyong.vscode-css-modules",
"yoavbls.pretty-ts-errors"
] ]
} }

1725
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.2.0", "@eslint/js": "^9.2.0",
"@types/eslint": "^8.56.10",
"@types/gh-pages": "^6.1.0", "@types/gh-pages": "^6.1.0",
"@types/node": "^20.12.8", "@types/node": "^20.12.8",
"@types/react": "^18.3.1", "@types/react": "^18.3.1",
@ -51,9 +52,11 @@
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-refresh": "^0.4.7", "eslint-plugin-react-refresh": "^0.4.7",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
"stylelint": "^16.6.1",
"tsx": "^4.15.4", "tsx": "^4.15.4",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^7.8.0" "typescript-eslint": "^7.8.0",
"vite-plugin-checker": "^0.6.4"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View file

@ -1,3 +1,3 @@
<options> <options>
<pins>file-explorer,terminal,settings,media-viewer,browser,text-editor,wordle,balls,minesweeper</pins> <pins>file-explorer,terminal,settings,media-viewer,browser,text-editor,wordle,ball-maze,minesweeper</pins>
</options> </options>

View file

@ -28,8 +28,8 @@ function fetchRepositoryTree(callback: (tree: string) => void) {
).then((response: ReponseType) => { ).then((response: ReponseType) => {
const items = response.tree; const items = response.tree;
const files = []; const files: string[] = [];
const folders = []; const folders: string[] = [];
items.forEach(({ path, type }) => { items.forEach(({ path, type }) => {
if (type === "tree") { if (type === "tree") {

View file

@ -1,5 +1,5 @@
import fs from "node:fs"; import fs from "node:fs";
import { APP_DESCRIPTIONS, APP_NAMES, APPS } from "../src/config/apps.config"; import { APP_DESCRIPTIONS, APP_NAMES, AppKey, APPS } from "../src/config/apps.config";
import { ANSI } from "../src/config/apps/terminal.config"; import { ANSI } from "../src/config/apps/terminal.config";
import { NAME, TAG_LINE } from "../src/config/branding.config"; import { NAME, TAG_LINE } from "../src/config/branding.config";
import { WALLPAPERS } from "../src/config/desktop.config"; import { WALLPAPERS } from "../src/config/desktop.config";
@ -91,8 +91,8 @@ function generate404Page(template: string) {
function generateAppPages(template: string) { function generateAppPages(template: string) {
for (const [key, value] of Object.entries(APPS)) { for (const [key, value] of Object.entries(APPS)) {
const appId = value; const appId = value;
const appName = Object.keys(APP_NAMES).includes(key) ? APP_NAMES[key] as string : appId; const appName = key in APP_NAMES ? APP_NAMES[key as AppKey] as string : appId;
const appDescription = Object.keys(APP_DESCRIPTIONS).includes(key) ? APP_DESCRIPTIONS[key] as string : TAG_LINE; const appDescription = Object.keys(APP_DESCRIPTIONS).includes(key) ? APP_DESCRIPTIONS[key as AppKey] as string : TAG_LINE;
if (appId === "index") { if (appId === "index") {
console.log("Invalid app ID found: " + appId); console.log("Invalid app ID found: " + appId);

View file

@ -8,7 +8,7 @@ import { ModalsManagerProvider } from "./hooks/modals/modalsManagerProvider";
import { SettingsManagerProvider } from "./hooks/settings/settingsManagerProvider"; import { SettingsManagerProvider } from "./hooks/settings/settingsManagerProvider";
import { Router } from "./components/router/Router"; import { Router } from "./components/router/Router";
export default function App(): ReactElement { export function App(): ReactElement {
useEffect(() => { useEffect(() => {
const onContextMenu = (event: Event) => { const onContextMenu = (event: Event) => {
event.preventDefault(); event.preventDefault();

View file

@ -13,31 +13,30 @@ interface ButtonProps {
[key: string]: unknown; [key: string]: unknown;
} }
export function Button(props: ButtonProps) { export function Button({ className, href, children, icon, target, ...props }: ButtonProps) {
let { className = "" } = props; const classNames = [styles.Button];
const { href, children, icon, target } = props; if (className != null)
classNames.push(className);
className = `${styles.Button} ${className}`;
if (href != null) { if (href != null) {
className = `${styles.ButtonLink} ${className}`; classNames.push(styles.ButtonLink);
return (<a return (<a
{...props}
href={href} href={href}
target={target ?? "_blank"} target={target ?? "_blank"}
rel="noreferrer" rel="noreferrer"
tabIndex={0} tabIndex={0}
{...props} className={classNames.join(" ")}
className={className}
> >
{children} {children}
<FontAwesomeIcon icon={icon ?? faExternalLink}/> <FontAwesomeIcon icon={icon ?? faExternalLink}/>
</a>); </a>);
} else { } else {
return (<button return (<button
tabIndex={0}
{...props} {...props}
className={className} tabIndex={0}
className={classNames.join(" ")}
> >
{children} {children}
{icon != null {icon != null

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import styles from "./DropdownButton.module.css"; import styles from "./DropdownButton.module.css";
import OutsideClickListener from "../../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../../hooks/_utils/outsideClick";
import { formatShortcut } from "../../../features/_utils/string.utils"; import { formatShortcut } from "../../../features/_utils/string.utils";
export function DropdownButton({ label, options, shortcuts }: { label: string; options: { [s: string]: Function; }; shortcuts: { [s: string]: string[]; }; }) { export function DropdownButton({ label, options, shortcuts }: { label: string; options: { [s: string]: Function; }; shortcuts: { [s: string]: string[]; }; }) {

View file

@ -1,12 +1,19 @@
import { useState } from "react"; import { MouseEventHandler, ReactNode, useState } from "react";
import { INTERACTIBLE_DOUBLE_CLICK_DELAY } from "../../../config/utils.config"; import { INTERACTIBLE_DOUBLE_CLICK_DELAY } from "../../../config/utils.config";
let timeoutId = null; let timeoutId: NodeJS.Timeout | null = null;
export function Interactable({ onClick, onDoubleClick, children, ...props }) { interface InteractableProps {
onClick: Function;
onDoubleClick: Function;
children: ReactNode;
[key: string]: unknown;
}
export function Interactable({ onClick, onDoubleClick, children, ...props }: InteractableProps) {
const [clicked, setClicked] = useState(false); const [clicked, setClicked] = useState(false);
const onButtonClick = (event) => { const onButtonClick = (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -28,7 +35,7 @@ export function Interactable({ onClick, onDoubleClick, children, ...props }) {
}, INTERACTIBLE_DOUBLE_CLICK_DELAY); }, INTERACTIBLE_DOUBLE_CLICK_DELAY);
}; };
return <button {...props} onClick={onButtonClick}> return <button {...props} onClick={onButtonClick as unknown as MouseEventHandler}>
{children} {children}
</button>; </button>;
} }

View file

@ -1,15 +1,6 @@
import { CSSProperties } from "react"; import { CSSProperties } from "react";
import { clamp } from "../../../features/math/clamp";
import styles from "./ProgressBar.module.css"; import styles from "./ProgressBar.module.css";
import { clamp } from "../../../features/_utils/math.utils";
/**
* @param {object} props
* @param {number} props.fillPercentage
* @param {string} props.fillColor
* @param {string} props.backgroundColor
* @param {string} props.align
* @param {string} props.className
*/
interface ProgressBarProps { interface ProgressBarProps {
fillPercentage: number; fillPercentage: number;

View file

@ -2,7 +2,7 @@ import { Children, cloneElement, isValidElement, ReactElement, ReactNode, Ref }
import { useShortcuts } from "../../hooks/_utils/keyboard"; import { useShortcuts } from "../../hooks/_utils/keyboard";
import styles from "./Actions.module.css"; import styles from "./Actions.module.css";
import { useScreenBounds } from "../../hooks/_utils/screen"; import { useScreenBounds } from "../../hooks/_utils/screen";
import { STYLES } from "../../config/actions.config"; import { ActionsManager } from "../../features/actions/actionsManager";
export interface ActionProps { export interface ActionProps {
actionId?: string; actionId?: string;
@ -15,11 +15,13 @@ export interface ActionProps {
} }
export interface ActionsProps { export interface ActionsProps {
mode?: string;
className?: string; className?: string;
onAnyTrigger?: (event: Event, triggerParams: unknown, ...args: unknown[]) => void; onAnyTrigger?: (event: Event, triggerParams: unknown, ...args: unknown[]) => void;
children?: ReactNode; children?: ReactNode;
triggerParams?: unknown; triggerParams?: unknown;
avoidTaskbar?: boolean; avoidTaskbar?: boolean;
[key: string]: unknown;
} }
/** /**
@ -33,13 +35,13 @@ export interface ActionsProps {
* }} * }}
* /> * />
*/ */
export function Actions({ children, className, onAnyTrigger, triggerParams, avoidTaskbar = true }: ActionsProps): ReactElement { export function Actions({ children, mode, className, onAnyTrigger, triggerParams, avoidTaskbar = true }: ActionsProps): ReactElement {
const isListener = (className === STYLES.SHORTCUTS_LISTENER); const isListener = (mode === ActionsManager.MODES.shortcutsListener);
const { ref, initiated, alignLeft, alignTop } = useScreenBounds({ avoidTaskbar }); const { ref, initiated, alignLeft, alignTop } = useScreenBounds({ avoidTaskbar });
const options = {}; const options: Record<number, Function> = {};
const shortcuts = {}; const shortcuts: Record<number, string[]> = {};
let actionId = 0; let actionId = 0;
const iterateOverChildren = (children: ReactNode): ReactNode => { const iterateOverChildren = (children: ReactNode): ReactNode => {
@ -89,6 +91,8 @@ export function Actions({ children, className, onAnyTrigger, triggerParams, avoi
return iterateOverChildren(children) as ReactElement; return iterateOverChildren(children) as ReactElement;
const classNames = [styles.Actions]; const classNames = [styles.Actions];
if (mode != null)
classNames.push(styles[mode]);
if (className != null) if (className != null)
classNames.push(className); classNames.push(className);
if (alignLeft) if (alignLeft)

View file

@ -3,7 +3,7 @@ import styles from "../Actions.module.css";
import { faCaretRight, IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { faCaretRight, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { ReactElement, useState } from "react"; import { ReactElement, useState } from "react";
import { ActionProps } from "../Actions"; import { ActionProps } from "../Actions";
import OutsideClickListener from "../../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../../hooks/_utils/outsideClick";
interface DropdownActionProps extends ActionProps { interface DropdownActionProps extends ActionProps {
showOnHover?: boolean; showOnHover?: boolean;
@ -24,9 +24,18 @@ export function DropdownAction({ label, icon, children, showOnHover = true }: Dr
key={label} key={label}
className={classNames.join(" ")} className={classNames.join(" ")}
tabIndex={0} tabIndex={0}
onMouseEnter={showOnHover ? () => { setShowContent(true); } : null} onMouseEnter={() => {
onMouseLeave={showOnHover ? () => { setShowContent(false); } : null} if (showOnHover)
onClick={!showOnHover ? () => { setShowContent(!showContent); } : null} setShowContent(true);
}}
onMouseLeave={() => {
if (showOnHover)
setShowContent(false);
}}
onClick={() => {
if (!showOnHover)
setShowContent(!showContent);
}}
> >
<span className={styles.Label}> <span className={styles.Label}>
{icon && <div className={styles.Icon}><FontAwesomeIcon icon={icon as IconDefinition}/></div>} {icon && <div className={styles.Icon}><FontAwesomeIcon icon={icon as IconDefinition}/></div>}

View file

@ -32,7 +32,7 @@ export function RadioAction({ actionId, options, initialIndex, onTrigger }: Radi
{options.map(({ label, shortcut }, index) => {options.map(({ label, shortcut }, index) =>
<button key={label} className={styles.Button} tabIndex={0} onClick={(event) => { <button key={label} className={styles.Button} tabIndex={0} onClick={(event) => {
setActiveIndex(index); setActiveIndex(index);
onTrigger(event as unknown as Event, index); onTrigger?.(event as unknown as Event, index);
}}> }}>
<span className={styles.Label}> <span className={styles.Label}>
<div className={styles.Icon}> <div className={styles.Icon}>

View file

@ -1,6 +1,11 @@
import { ReactNode } from "react";
import styles from "../Actions.module.css"; import styles from "../Actions.module.css";
export function TextDisplay({ children }) { interface TextDisplayProps {
children: ReactNode;
}
export function TextDisplay({ children }: TextDisplayProps) {
return <p className={styles.TextDisplay}> return <p className={styles.TextDisplay}>
{children} {children}
</p>; </p>;

View file

@ -14,7 +14,7 @@ export function ToggleAction({ actionId, label, shortcut, initialValue, onTrigge
const [active, setActive] = useState(initialValue ?? false); const [active, setActive] = useState(initialValue ?? false);
return (<button key={actionId} className={styles.Button} tabIndex={0} onClick={(event) => { return (<button key={actionId} className={styles.Button} tabIndex={0} onClick={(event) => {
onTrigger(event as unknown as Event, !active); onTrigger?.(event as unknown as Event, !active);
setActive(!active); setActive(!active);
}}> }}>
<span className={styles.Label}> <span className={styles.Label}>

View file

@ -1,4 +1,5 @@
.HeaderMenu { .HeaderMenu {
position: relative;
display: flex; display: flex;
width: 100%; width: 100%;
height: 1.5rem; height: 1.5rem;

View file

@ -1,15 +1,15 @@
import styles from "./HeaderMenu.module.css"; import styles from "./HeaderMenu.module.css";
import { ActionsProps, Actions } from "../../../actions/Actions"; import { ActionsProps, Actions } from "../../../actions/Actions";
import { STYLES } from "../../../../config/actions.config";
import { useZIndex } from "../../../../hooks/z-index/zIndex"; import { useZIndex } from "../../../../hooks/z-index/zIndex";
import { ZIndexManager } from "../../../../features/z-index/zIndexManager"; import { ZIndexManager } from "../../../../features/z-index/zIndexManager";
import { ActionsManager } from "../../../../features/actions/actionsManager";
export function HeaderMenu({ children, ...props }: ActionsProps) { export function HeaderMenu({ children, ...props }: ActionsProps) {
const zIndex = useZIndex({ groupIndex: ZIndexManager.GROUPS.MODALS, index: 0 }); const zIndex = useZIndex({ groupIndex: ZIndexManager.GROUPS.MODALS, index: 5 });
return <div className={styles.HeaderMenu} style={{ zIndex }}> return <div className={styles.HeaderMenu} style={{ zIndex }}>
<Actions className={STYLES.HEADER_MENU} {...props}> <Actions mode={ActionsManager.MODES.headerMenu} {...props}>
{children} {children}
</Actions> </Actions>
</div>; </div>;

View file

@ -3,8 +3,8 @@ import styles from "./WebView.module.css";
import { WindowProps } from "../../../windows/WindowView"; import { WindowProps } from "../../../windows/WindowView";
interface WebViewProps extends WindowProps { interface WebViewProps extends WindowProps {
source: string; source?: string;
title: string; title?: string;
} }
export const WebView: FC<WebViewProps> = forwardRef<HTMLIFrameElement>(({ source, focus, ...props }: WebViewProps, ref) => { export const WebView: FC<WebViewProps> = forwardRef<HTMLIFrameElement>(({ source, focus, ...props }: WebViewProps, ref) => {
@ -13,7 +13,7 @@ export const WebView: FC<WebViewProps> = forwardRef<HTMLIFrameElement>(({ source
useEffect(() => { useEffect(() => {
window.focus(); window.focus();
const onBlur = (event) => { const onBlur = (event: Event) => {
if (hovered) { if (hovered) {
focus?.(event); focus?.(event);
} }
@ -39,7 +39,6 @@ export const WebView: FC<WebViewProps> = forwardRef<HTMLIFrameElement>(({ source
<iframe <iframe
ref={ref} ref={ref}
src={source} src={source}
title={props.title ?? "Web view"}
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts"
{...props} {...props}

View file

@ -26,7 +26,7 @@ export function Browser({ url: startUrl = HOME_URL, focus }: BrowserProps) {
}, [history, stateIndex]); }, [history, stateIndex]);
const reload = () => { const reload = () => {
if (ref.current == null) if (ref.current == null || ref.current.contentWindow == null)
return; return;
ref.current.contentWindow.location.href = url; ref.current.contentWindow.location.href = url;

View file

@ -4,10 +4,10 @@ import styles from "./Calculator.module.css";
import { WindowProps } from "../../windows/WindowView"; import { WindowProps } from "../../windows/WindowView";
export function Calculator({ active }: WindowProps) { export function Calculator({ active }: WindowProps) {
const [input, setInput] = useState("0"); const [input, setInput] = useState<string | null>("0");
const [firstNumber, setFirstNumber] = useState<number>(null); const [firstNumber, setFirstNumber] = useState<number | null>(null);
const [secondNumber, setSecondNumber] = useState<number>(null); const [secondNumber, setSecondNumber] = useState<number | null>(null);
const [operation, setOperation] = useState<string>(null); const [operation, setOperation] = useState<string | null>(null);
const [isIntermediate, setIsIntermediate] = useState(false); const [isIntermediate, setIsIntermediate] = useState(false);
const reset = useCallback(() => { const reset = useCallback(() => {
@ -20,7 +20,7 @@ export function Calculator({ active }: WindowProps) {
const addInput = useCallback((string: string) => { const addInput = useCallback((string: string) => {
let hasReset = false; let hasReset = false;
if (secondNumber != null) { if (secondNumber != null) {
if (isIntermediate) { if (isIntermediate && input != null) {
setFirstNumber(parseFloat(input)); setFirstNumber(parseFloat(input));
setSecondNumber(null); setSecondNumber(null);
setInput(null); setInput(null);
@ -36,10 +36,10 @@ export function Calculator({ active }: WindowProps) {
if (string === "-") { if (string === "-") {
if (input === "0") { if (input === "0") {
setInput("-0"); setInput("-0");
} else { } else if (input != null) {
setInput((parseFloat(input) * -1).toString()); setInput((parseFloat(input) * -1).toString());
} }
} else if (string === "%") { } else if (string === "%" && input != null) {
setInput((parseFloat(input) / 100).toString()); setInput((parseFloat(input) / 100).toString());
} else if (input === "0" || input === "-0" || input == null || hasReset) { } else if (input === "0" || input === "-0" || input == null || hasReset) {
if (string === ".") { if (string === ".") {
@ -54,7 +54,7 @@ export function Calculator({ active }: WindowProps) {
}, [input, isIntermediate, reset, secondNumber]); }, [input, isIntermediate, reset, secondNumber]);
const calculate = useCallback((intermediate = false) => { const calculate = useCallback((intermediate = false) => {
if (firstNumber != null) { if (firstNumber != null && input != null) {
setSecondNumber(parseFloat(input)); setSecondNumber(parseFloat(input));
const a = firstNumber; const a = firstNumber;
@ -85,7 +85,7 @@ export function Calculator({ active }: WindowProps) {
const changeOperation = useCallback((operation: string) => { const changeOperation = useCallback((operation: string) => {
if (firstNumber != null && secondNumber == null) { if (firstNumber != null && secondNumber == null) {
calculate(true); calculate(true);
} else { } else if (input != null) {
setFirstNumber(parseFloat(input)); setFirstNumber(parseFloat(input));
setSecondNumber(null); setSecondNumber(null);
setInput(null); setInput(null);

View file

@ -7,14 +7,14 @@ import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext"
import { useContextMenu } from "../../../hooks/modals/contextMenu"; import { useContextMenu } from "../../../hooks/modals/contextMenu";
import { QuickAccessButton } from "./QuickAccessButton"; import { QuickAccessButton } from "./QuickAccessButton";
import { useWindowedModal } from "../../../hooks/modals/windowedModal"; import { useWindowedModal } from "../../../hooks/modals/windowedModal";
import Vector2 from "../../../features/math/vector2"; import { Vector2 } from "../../../features/math/vector2";
import { DIALOG_CONTENT_TYPES } from "../../../config/modals.config"; import { DIALOG_CONTENT_TYPES } from "../../../config/modals.config";
import { DirectoryList, OnSelectionChangeParams } from "./directory-list/DirectoryList"; import { DirectoryList, FileEventHandler, FolderEventHandler, OnSelectionChangeParams } from "./directory-list/DirectoryList";
import { Actions } from "../../actions/Actions"; import { Actions } from "../../actions/Actions";
import { ClickAction } from "../../actions/actions/ClickAction"; import { ClickAction } from "../../actions/actions/ClickAction";
import utilStyles from "../../../styles/utils.module.css"; import utilStyles from "../../../styles/utils.module.css";
import { DialogBox } from "../../modals/dialog-box/DialogBox"; import { DialogBox } from "../../modals/dialog-box/DialogBox";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import { APPS, APP_ICONS, APP_NAMES } from "../../../config/apps.config"; import { APPS, APP_ICONS, APP_NAMES } from "../../../config/apps.config";
import { TITLE_SEPARATOR } from "../../../config/windows.config"; import { TITLE_SEPARATOR } from "../../../config/windows.config";
import { FileProperties } from "../../modals/file-properties/FileProperties"; import { FileProperties } from "../../modals/file-properties/FileProperties";
@ -26,7 +26,7 @@ import { WindowProps } from "../../windows/WindowView";
import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder";
import { VirtualFile } from "../../../features/virtual-drive/file"; import { VirtualFile } from "../../../features/virtual-drive/file";
import { VirtualFolderLink } from "../../../features/virtual-drive/folder/virtualFolderLink"; import { VirtualFolderLink } from "../../../features/virtual-drive/folder/virtualFolderLink";
import ImportButton from "./ImportButton"; import { ImportButton } from "./ImportButton";
import { useAlert } from "../../../hooks/modals/alert"; import { useAlert } from "../../../hooks/modals/alert";
import { VirtualRoot } from "../../../features/virtual-drive/root/virtualRoot"; import { VirtualRoot } from "../../../features/virtual-drive/root/virtualRoot";
@ -42,7 +42,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
const isSelector = (Footer != null && selectorMode != null && selectorMode !== SELECTOR_MODE.NONE); const isSelector = (Footer != null && selectorMode != null && selectorMode !== SELECTOR_MODE.NONE);
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const [currentDirectory, setCurrentDirectory] = useState<VirtualFolder>(virtualRoot.navigate(startPath ?? "~") as VirtualFolder); const [currentDirectory, setCurrentDirectory] = useState<VirtualFolder>(virtualRoot?.navigate(startPath ?? "~") as VirtualFolder);
const [path, setPath] = useState<string>(currentDirectory?.path ?? ""); const [path, setPath] = useState<string>(currentDirectory?.path ?? "");
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const [showHidden] = useState(true); const [showHidden] = useState(true);
@ -52,38 +52,38 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
const { openWindowedModal } = useWindowedModal(); const { openWindowedModal } = useWindowedModal();
const { onContextMenu: onContextMenuFile } = useContextMenu({ Actions: (props) => const { onContextMenu: onContextMenuFile } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<ClickAction label={!isSelector ? "Open" : "Select"} onTrigger={(event, file: VirtualFile) => { <ClickAction label={!isSelector ? "Open" : "Select"} onTrigger={(event, file) => {
if (isSelector) { if (isSelector) {
onSelectionChange?.({ files: [file.id], directory: currentDirectory }); onSelectionChange?.({ files: [(file as VirtualFile).id], directory: currentDirectory });
onSelectionFinish?.(); onSelectionFinish?.();
return; return;
} }
file.open(windowsManager); if (windowsManager != null) (file as VirtualFile).open(windowsManager);
}}/> }}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file: VirtualFile) => { <ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
file.delete(); (file as VirtualFile).delete();
}}/> }}/>
<ClickAction label="Properties" icon={faCircleInfo} onTrigger={(event, file: VirtualFile) => { <ClickAction label="Properties" icon={faCircleInfo} onTrigger={(event, file) => {
openWindowedModal({ openWindowedModal({
title: `${file.id} ${TITLE_SEPARATOR} Properties`, title: `${(file as VirtualFile).id} ${TITLE_SEPARATOR} Properties`,
iconUrl: file.getIconUrl(), iconUrl: (file as VirtualFile).getIconUrl(),
size: new Vector2(400, 500), size: new Vector2(400, 500),
Modal: (props: object) => <FileProperties file={file} {...props}/> Modal: (props: object) => <FileProperties file={file as VirtualFile} {...props}/>
}); });
}}/> }}/>
</Actions> </Actions>
}); });
const { onContextMenu: onContextMenuFolder } = useContextMenu({ Actions: (props) => const { onContextMenu: onContextMenuFolder } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder: VirtualFolder & VirtualFolderLink) => { <ClickAction label="Open" onTrigger={(event, folder) => {
changeDirectory(folder.linkedPath ?? folder.name); changeDirectory((folder as VirtualFolderLink).linkedPath ?? (folder as VirtualFolder).name);
}}/> }}/>
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder: VirtualFolder) => { <ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder) => {
windowsManager.open(APPS.TERMINAL, { startPath: folder.path }); windowsManager?.open(APPS.TERMINAL, { startPath: (folder as VirtualFolder).path });
}}/> }}/>
<Divider/> <Divider/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder: VirtualFolder) => { <ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
folder.delete(); (folder as VirtualFolder).delete();
}}/> }}/>
</Actions> </Actions>
}); });
@ -102,7 +102,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
if (currentDirectory == null) if (currentDirectory == null)
absolute = true; absolute = true;
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path); const directory = absolute ? virtualRoot?.navigate(path) : currentDirectory.navigate(path);
if (directory != null) { if (directory != null) {
setCurrentDirectory(directory as VirtualFolder); setCurrentDirectory(directory as VirtualFolder);
@ -116,7 +116,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
return; return;
const path = history[stateIndex]; const path = history[stateIndex];
const directory = virtualRoot.navigate(path); const directory = virtualRoot?.navigate(path);
if (directory != null) { if (directory != null) {
setCurrentDirectory(directory as VirtualFolder); setCurrentDirectory(directory as VirtualFolder);
setPath(directory.root ? "/" : directory.path); setPath(directory.root ? "/" : directory.path);
@ -124,9 +124,10 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
}, [history, stateIndex, virtualRoot]); }, [history, stateIndex, virtualRoot]);
useEffect(() => { useEffect(() => {
const onError = (error: { message: string }) => { type Error = { message: string };
const onError = (error: unknown) => {
alert({ alert({
title: error.message, title: (error as Error).message,
text: "You have exceeded the virtual drive capacity. Files and folders will not be saved until more storage is freed.", text: "You have exceeded the virtual drive capacity. Files and folders will not be saved until more storage is freed.",
iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER), iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER),
size: new Vector2(300, 200), size: new Vector2(300, 200),
@ -134,10 +135,10 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
}); });
}; };
virtualRoot.on(VirtualRoot.EVENT_NAMES.ERROR, onError); virtualRoot?.on(VirtualRoot.EVENT_NAMES.ERROR, onError);
return () => { return () => {
virtualRoot.off(VirtualRoot.EVENT_NAMES.ERROR, onError); virtualRoot?.off(VirtualRoot.EVENT_NAMES.ERROR, onError);
}; };
}, []); }, []);
@ -152,17 +153,17 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
if (value === "") if (value === "")
value = "~"; value = "~";
const directory = virtualRoot.navigate(value); const directory = virtualRoot?.navigate(value);
if (directory == null) { if (directory == null) {
openWindowedModal({ openWindowedModal({
title: "Error", title: "Error",
iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER), iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER),
size: new Vector2(300, 150), size: new Vector2(300, 150),
Modal: (props) => Modal: (props: {}) =>
<DialogBox {...props}> <DialogBox {...props}>
<p>Invalid path: "{value}"</p> <p>Invalid path: "{value}"</p>
<button data-type={DIALOG_CONTENT_TYPES.CloseButton}>Ok</button> <button data-type={DIALOG_CONTENT_TYPES.closeButton}>Ok</button>
</DialogBox> </DialogBox>
}); });
return; return;
@ -201,7 +202,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
tabIndex={0} tabIndex={0}
className={styles.IconButton} className={styles.IconButton}
onClick={() => { changeDirectory(".."); }} onClick={() => { changeDirectory(".."); }}
disabled={currentDirectory.isRoot} disabled={currentDirectory.isRoot != null && currentDirectory.isRoot}
> >
<FontAwesomeIcon icon={faArrowUp}/> <FontAwesomeIcon icon={faArrowUp}/>
</button> </button>
@ -262,19 +263,19 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
className={styles.Main} className={styles.Main}
showHidden={showHidden} showHidden={showHidden}
onOpenFile={(event, file) => { onOpenFile={(event, file) => {
(event as Event).preventDefault(); event.preventDefault();
if (isSelector) if (isSelector)
return void onSelectionFinish?.(); return void onSelectionFinish?.();
const options: Record<string, string> = {}; const options: Record<string, string> = {};
if (file.extension === "md" || CODE_FORMATS.includes(file.extension)) if (file.extension === "md" || (file.extension != null && CODE_FORMATS.includes(file.extension)))
options.mode = "view"; options.mode = "view";
windowsManager.openFile(file, options); windowsManager?.openFile(file, options);
}} }}
onOpenFolder={(event, folder) => { onOpenFolder={(event, folder) => {
changeDirectory((folder as VirtualFolderLink).linkedPath ?? folder.name); changeDirectory((folder as VirtualFolderLink).linkedPath ?? folder.name);
}} }}
onContextMenuFile={onContextMenuFile} onContextMenuFile={onContextMenuFile as unknown as FileEventHandler}
onContextMenuFolder={onContextMenuFolder} onContextMenuFolder={onContextMenuFolder as unknown as FolderEventHandler}
allowMultiSelect={selectorMode !== SELECTOR_MODE.SINGLE} allowMultiSelect={selectorMode !== SELECTOR_MODE.SINGLE}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
/> />

View file

@ -9,19 +9,22 @@ interface ImportButtonProps {
directory: VirtualFolder; directory: VirtualFolder;
} }
export default function ImportButton({ directory }: ImportButtonProps): ReactElement { export function ImportButton({ directory }: ImportButtonProps): ReactElement {
const onChange = (event: InputEvent) => { const onChange = (event: InputEvent) => {
const files = (event.target as HTMLInputElement).files; const files = (event.target as HTMLInputElement).files;
if (files == null)
return;
Array.from(files).forEach((file: File) => { Array.from(files).forEach((file: File) => {
const { name, extension } = VirtualFile.convertId(file.name); const { name, extension } = VirtualFile.splitId(file.name);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event: Event) => { reader.onload = (event: Event) => {
const { result } = event.target as FileReader; const { result } = event.target as FileReader;
// Create a file with the same name and extension, with a base64 string as a source // Create a file with the same name and extension, with a base64 string as a source
directory.createFile(name, extension, (virtualFile) => { directory.createFile(name, extension as string | undefined, (virtualFile) => {
virtualFile.setSource(result as string); virtualFile.setSource(result as string);
}); });
}; };

View file

@ -1,8 +1,15 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
import styles from "./FileExplorer.module.css"; import styles from "./FileExplorer.module.css";
import utilStyles from "../../../styles/utils.module.css"; import utilStyles from "../../../styles/utils.module.css";
import { MouseEventHandler } from "react";
export function QuickAccessButton({ onClick, icon, name }) { interface QuickAcessButton {
onClick: MouseEventHandler;
icon: FontAwesomeIconProps["icon"];
name: string;
}
export function QuickAccessButton({ onClick, icon, name }: QuickAcessButton) {
return ( return (
<button <button
tabIndex={0} tabIndex={0}

View file

@ -4,7 +4,7 @@ import { VirtualFolder } from "../../../../features/virtual-drive/folder/virtual
import { Interactable } from "../../../_utils/interactable/Interactable"; import { Interactable } from "../../../_utils/interactable/Interactable";
import styles from "./DirectoryList.module.css"; import styles from "./DirectoryList.module.css";
import { ImagePreview } from "./ImagePreview"; import { ImagePreview } from "./ImagePreview";
import Vector2 from "../../../../features/math/vector2"; import { Vector2 } from "../../../../features/math/vector2";
export interface OnSelectionChangeParams { export interface OnSelectionChangeParams {
files?: string[]; files?: string[];
@ -12,8 +12,8 @@ export interface OnSelectionChangeParams {
directory?: VirtualFolder; directory?: VirtualFolder;
}; };
type FileEventHandler = (event: object, file: VirtualFile) => void; export type FileEventHandler = (event: Event, file: VirtualFile) => void;
type FolderEventHandler = (event: object, folder: VirtualFolder) => void; export type FolderEventHandler = (event: Event, folder: VirtualFolder) => void;
interface DirectoryListProps { interface DirectoryListProps {
directory: VirtualFolder; directory: VirtualFolder;
@ -31,13 +31,13 @@ interface DirectoryListProps {
} }
export function DirectoryList({ directory, showHidden = false, folderClassName, fileClassName, className, export function DirectoryList({ directory, showHidden = false, folderClassName, fileClassName, className,
onContextMenuFile, onContextMenuFolder, onOpenFile, onOpenFolder, allowMultiSelect = true, onSelectionChange, ...props }: DirectoryListProps): ReactElement { onContextMenuFile, onContextMenuFolder, onOpenFile, onOpenFolder, allowMultiSelect = true, onSelectionChange, ...props }: DirectoryListProps): ReactElement | null {
const [selectedFolders, setSelectedFolders] = useState<string[]>([]); const [selectedFolders, setSelectedFolders] = useState<string[]>([]);
const [selectedFiles, setSelectedFiles] = useState<string[]>([]); const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const ref = useRef(null); const ref = useRef(null);
const [rectSelectStart, setRectSelectStart] = useState<Vector2>(null); const [rectSelectStart, setRectSelectStart] = useState<Vector2 | null>(null);
const [rectSelectEnd, setRectSelectEnd] = useState<Vector2>(null); const [rectSelectEnd, setRectSelectEnd] = useState<Vector2 | null>(null);
useEffect(() => { useEffect(() => {
onSelectionChange?.({ files: selectedFiles, folders: selectedFolders, directory }); onSelectionChange?.({ files: selectedFiles, folders: selectedFolders, directory });
@ -77,7 +77,7 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
}); });
if (!directory) if (!directory)
return; return null;
const clearSelection = () => { const clearSelection = () => {
setSelectedFolders([]); setSelectedFolders([]);
@ -102,8 +102,12 @@ export function DirectoryList({ directory, showHidden = false, folderClassName,
setRectSelectStart({ x: event.clientX, y: event.clientY } as Vector2); setRectSelectStart({ x: event.clientX, y: event.clientY } as Vector2);
}; };
const getRectSelectStyle = () => { const getRectSelectStyle = () => {
let x: number, y: number, width: number, height: number = null; let x: number, y: number, width: number, height: number = 0;
const containerRect = (ref.current as HTMLElement)?.getBoundingClientRect();
if (ref.current == null || rectSelectStart == null || rectSelectEnd == null)
return { top: 0, left: 0, width: 0, height: 0 };
const containerRect = (ref.current as HTMLElement).getBoundingClientRect();
if (rectSelectStart.x < rectSelectEnd.x) { if (rectSelectStart.x < rectSelectEnd.x) {
x = rectSelectStart.x; x = rectSelectStart.x;

View file

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import styles from "./ImagePreview.module.css"; import styles from "./ImagePreview.module.css";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import AppsManager from "../../../../features/apps/appsManager"; import { AppsManager } from "../../../../features/apps/appsManager";
import { APPS } from "../../../../config/apps.config"; import { APPS } from "../../../../config/apps.config";
interface ImagePreviewProps { interface ImagePreviewProps {

View file

@ -5,12 +5,12 @@ import { DropdownAction } from "../../actions/actions/DropdownAction";
import { HeaderMenu } from "../_utils/header-menu/HeaderMenu"; import { HeaderMenu } from "../_utils/header-menu/HeaderMenu";
import { ClickAction } from "../../actions/actions/ClickAction"; import { ClickAction } from "../../actions/actions/ClickAction";
import { ChipsManager } from "../../../features/apps/logic-sim/chips/chipsManager"; import { ChipsManager } from "../../../features/apps/logic-sim/chips/chipsManager";
import App from "../../../features/apps/app"; import { App } from "../../../features/apps/app";
import { useAppFolder } from "../../../hooks/apps/appFolder"; import { useAppFolder } from "../../../hooks/apps/appFolder";
import { openUrl } from "../../../features/_utils/browser.utils"; import { openUrl } from "../../../features/_utils/browser.utils";
interface CircuitViewProps { interface CircuitViewProps {
app: App; app?: App;
} }
export function CircuitView({ app }: CircuitViewProps) { export function CircuitView({ app }: CircuitViewProps) {
@ -22,7 +22,7 @@ export function CircuitView({ app }: CircuitViewProps) {
if (canvasRef.current == null && circuit.canvas != null) if (canvasRef.current == null && circuit.canvas != null)
return; return;
circuit.init(canvasRef.current as HTMLCanvasElement); circuit.init(canvasRef.current as unknown as HTMLCanvasElement);
return () => { return () => {
circuit.cleanup(); circuit.cleanup();
@ -34,10 +34,12 @@ export function CircuitView({ app }: CircuitViewProps) {
<DropdownAction label="Circuit" showOnHover={false}> <DropdownAction label="Circuit" showOnHover={false}>
<ClickAction label="New" onTrigger={() => { circuit.reset(); }}/> <ClickAction label="New" onTrigger={() => { circuit.reset(); }}/>
<ClickAction label="Save" onTrigger={() => { <ClickAction label="Save" onTrigger={() => {
ChipsManager.saveCircuit(circuit, virtualFolder); if (virtualFolder != null)
ChipsManager.saveCircuit(circuit, virtualFolder);
}}/> }}/>
<ClickAction label="Load" onTrigger={() => { <ClickAction label="Load" onTrigger={() => {
ChipsManager.loadCircuit(circuit, virtualFolder); if (virtualFolder != null)
ChipsManager.loadCircuit(circuit, virtualFolder);
}}/> }}/>
</DropdownAction> </DropdownAction>
<DropdownAction label="Add" showOnHover={false}> <DropdownAction label="Add" showOnHover={false}>

View file

@ -15,27 +15,22 @@ export function MediaViewer({ file, close, setTitle }: MediaViewerProps) {
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
useEffect(() => { useEffect(() => {
if (file != null) if (file != null) setTitle?.(file.id);
setTitle(file.id);
}, [file, setTitle]); }, [file, setTitle]);
if (file == null) { if (file == null) {
setTimeout(() => { setTimeout(() => {
windowsManager.open(APPS.FILE_EXPLORER, { path: "~/Pictures" }); windowsManager?.open(APPS.FILE_EXPLORER, { path: "~/Pictures" });
close(); close?.();
}, 10); }, 10);
return; return;
} }
if (!IMAGE_FORMATS.includes(file.extension)) if (file.extension == null || !IMAGE_FORMATS.includes(file.extension)) return <p>Invalid file format.</p>;
return (<p>Invalid file format.</p>);
if (file.source == null) if (file.source == null) return <p>File failed to load.</p>;
return (<p>File failed to load.</p>);
return ( return <div className={styles.MediaViewer}>
<div className={styles.MediaViewer}> <img src={file.source} alt={file.id} draggable="false"/>
<img src={file.source} alt={file.id} draggable="false"/> </div>;
</div>
);
} }

View file

@ -1,7 +1,7 @@
import { Button } from "../../../_utils/button/Button"; import { Button } from "../../../_utils/button/Button";
import styles from "../Settings.module.css"; import styles from "../Settings.module.css";
import utilStyles from "../../../../styles/utils.module.css"; import utilStyles from "../../../../styles/utils.module.css";
import Vector2 from "../../../../features/math/vector2"; import { Vector2 } from "../../../../features/math/vector2";
import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext"; import { useWindowsManager } from "../../../../hooks/windows/windowsManagerContext";
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext";
import { NAME } from "../../../../config/branding.config"; import { NAME } from "../../../../config/branding.config";
@ -10,31 +10,29 @@ export function AboutSettings() {
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
return (<> return <div className={styles.Option}>
<div className={styles.Option}> <p className={styles.Label}>About {NAME}</p>
<p className={styles.Label}>About {NAME}</p> <p className={utilStyles.TextLight}>{NAME} is a web-based operating system inspired by Ubuntu Linux and Windows made with React.js by Prozilla.</p>
<p className={utilStyles.TextLight}>{NAME} is a web-based operating system inspired by Ubuntu Linux and Windows made with React.js by Prozilla.</p> <div className={styles.ButtonGroup}>
<div className={styles.ButtonGroup}> <Button
<Button className={`${styles.Button} ${utilStyles.TextBold}`}
className={`${styles.Button} ${utilStyles.TextBold}`} onClick={(event: Event) => {
onClick={(event: Event) => { event.preventDefault();
event.preventDefault(); windowsManager?.open("text-editor", {
windowsManager.open("text-editor", { mode: "view",
mode: "view", file: virtualRoot?.navigate("~/Documents/Info.md"),
file: virtualRoot.navigate("~/Documents/Info.md"), size: new Vector2(575, 675),
size: new Vector2(575, 675), });
}); }}
}} >
> Open Info.md
Open Info.md </Button>
</Button> <Button
<Button className={`${styles.Button} ${utilStyles.TextBold}`}
className={`${styles.Button} ${utilStyles.TextBold}`} href="https://github.com/Prozilla/ProzillaOS"
href="https://github.com/Prozilla/ProzillaOS" >
> View source
View source </Button>
</Button>
</div>
</div> </div>
</>); </div>;
} }

View file

@ -1,5 +1,5 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import AppsManager from "../../../../features/apps/appsManager"; import { AppsManager } from "../../../../features/apps/appsManager";
import { ImagePreview } from "../../file-explorer/directory-list/ImagePreview"; import { ImagePreview } from "../../file-explorer/directory-list/ImagePreview";
import styles from "../Settings.module.css"; import styles from "../Settings.module.css";
import { faEllipsisVertical, faThumbTack } from "@fortawesome/free-solid-svg-icons"; import { faEllipsisVertical, faThumbTack } from "@fortawesome/free-solid-svg-icons";
@ -10,7 +10,8 @@ import { ClickAction } from "../../../actions/actions/ClickAction";
import { removeFromArray } from "../../../../features/_utils/array.utils"; import { removeFromArray } from "../../../../features/_utils/array.utils";
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext"; import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext";
import { SettingsManager } from "../../../../features/settings/settingsManager"; import { SettingsManager } from "../../../../features/settings/settingsManager";
import App from "../../../../features/apps/app"; import { App } from "../../../../features/apps/app";
import { MouseEventHandler } from "react";
interface AppOptionProps { interface AppOptionProps {
app: App; app: App;
@ -26,7 +27,7 @@ export function AppOption({ app, pins, setPins: _setPins }: AppOptionProps) {
const { onContextMenu } = useContextMenu({ Actions: (props) => const { onContextMenu } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<ClickAction label="Launch" icon={AppsManager.getAppIconUrl(app.id)} onTrigger={() => windowsManager.open(app.id)}/> <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={() => { <ClickAction label={isPinned ? "Unpin from taskbar" : "Pin to taskbar"} icon={faThumbTack} onTrigger={() => {
const newPins = [...pins]; const newPins = [...pins];
if (isPinned) { if (isPinned) {
@ -35,8 +36,8 @@ export function AppOption({ app, pins, setPins: _setPins }: AppOptionProps) {
newPins.push(app.id); newPins.push(app.id);
} }
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings.set("pins", newPins.join(",")); void settings?.set("pins", newPins.join(","));
}}/> }}/>
</Actions> </Actions>
}); });
@ -46,7 +47,7 @@ export function AppOption({ app, pins, setPins: _setPins }: AppOptionProps) {
<ImagePreview className={styles.Icon} source={AppsManager.getAppIconUrl(app.id)}/> <ImagePreview className={styles.Icon} source={AppsManager.getAppIconUrl(app.id)}/>
{app.name} {app.name}
</span> </span>
<button className={styles.IconButton} onClick={onContextMenu}> <button className={styles.IconButton} onClick={onContextMenu as unknown as MouseEventHandler}>
<FontAwesomeIcon icon={faEllipsisVertical}/> <FontAwesomeIcon icon={faEllipsisVertical}/>
</button> </button>
{/* <div className={styles["Button-group"]}> {/* <div className={styles["Button-group"]}>

View file

@ -19,24 +19,24 @@ export function AppearanceSettings() {
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const settingsManager = useSettingsManager(); const settingsManager = useSettingsManager();
const [theme, setTheme] = useState(0); const [theme, setTheme] = useState(0);
const [wallpaper, setWallpaper] = useState<string>(null); const [wallpaper, setWallpaper] = useState<string | null>(null);
const desktopSettings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); const desktopSettings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
const themeSettings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.theme); const themeSettings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.theme);
const { openWindowedModal } = useWindowedModal(); const { openWindowedModal } = useWindowedModal();
useEffect(() => { useEffect(() => {
void desktopSettings.get("wallpaper", setWallpaper); void desktopSettings?.get("wallpaper", setWallpaper);
void themeSettings.get("theme", (value: string) => { setTheme(parseInt(value)); }); void themeSettings?.get("theme", (value: string) => { setTheme(parseInt(value)); });
}, [desktopSettings, themeSettings]); }, [desktopSettings, themeSettings]);
const onWallpaperChange = (event: Event) => { const onWallpaperChange = (event: Event) => {
const value = (event.target as HTMLInputElement).value; const value = (event.target as HTMLInputElement).value;
void desktopSettings.set("wallpaper", value); void desktopSettings?.set("wallpaper", value);
}; };
const onThemeChange = (event: Event) => { const onThemeChange = (event: Event) => {
const value = (event.target as HTMLInputElement).value; const value = (event.target as HTMLInputElement).value;
void themeSettings.set("theme", value); void themeSettings?.set("theme", value);
}; };
return (<> return (<>
@ -57,11 +57,12 @@ export function AppearanceSettings() {
onClick={() => { onClick={() => {
openWindowedModal({ openWindowedModal({
size: DEFAULT_FILE_SELECTOR_SIZE, size: DEFAULT_FILE_SELECTOR_SIZE,
Modal: (props) => <FileSelector Modal: (props: object) => <FileSelector
type={SELECTOR_MODE.SINGLE} type={SELECTOR_MODE.SINGLE}
allowedFormats={IMAGE_FORMATS} allowedFormats={IMAGE_FORMATS}
onFinish={(file: VirtualFile) => { onFinish={(file) => {
void desktopSettings.set("wallpaper", file.source); if ((file as VirtualFile).source != null)
void desktopSettings?.set("wallpaper", (file as VirtualFile).source as string);
}} }}
{...props} {...props}
/> />
@ -71,17 +72,17 @@ export function AppearanceSettings() {
Browse Browse
</Button> </Button>
<div className={`${styles.Input} ${styles.ImageSelectContainer}`}> <div className={`${styles.Input} ${styles.ImageSelectContainer}`}>
{(virtualRoot.navigate(WALLPAPERS_PATH) as VirtualFolder)?.getFiles()?.map(({ id, source }) => {(virtualRoot?.navigate(WALLPAPERS_PATH) as VirtualFolder)?.getFiles()?.map(({ id, source }) =>
<label className={styles.ImageSelect} key={id}> <label className={styles.ImageSelect} key={id}>
<input <input
type="radio" type="radio"
value={source} value={source ?? ""}
aria-label="Wallpaper image" aria-label="Wallpaper image"
checked={source === wallpaper} checked={source === wallpaper}
onChange={onWallpaperChange as unknown as ChangeEventHandler} onChange={onWallpaperChange as unknown as ChangeEventHandler}
tabIndex={0} tabIndex={0}
/> />
<img src={source} alt={id} draggable="false"/> <img src={source ?? ""} alt={id} draggable="false"/>
</label> </label>
)} )}
</div> </div>

View file

@ -1,18 +1,18 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import styles from "../Settings.module.css"; import styles from "../Settings.module.css";
import AppsManager from "../../../../features/apps/appsManager"; import { AppsManager } from "../../../../features/apps/appsManager";
import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext"; import { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext";
import { SettingsManager } from "../../../../features/settings/settingsManager"; import { SettingsManager } from "../../../../features/settings/settingsManager";
import { AppOption } from "./AppOption"; import { AppOption } from "./AppOption";
export function AppsSettings() { export function AppsSettings() {
const settingsManager = useSettingsManager(); const settingsManager = useSettingsManager();
const [pins, setPins] = useState([]); const [pins, setPins] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings.get("pins", (pins) => { void settings?.get("pins", (newPins) => {
setPins(pins.split(",")); setPins(newPins.split(","));
}); });
}, [settingsManager]); }, [settingsManager]);

View file

@ -1,16 +1,16 @@
import styles from "../Settings.module.css"; import styles from "../Settings.module.css";
import utilStyles from "../../../../styles/utils.module.css"; import utilStyles from "../../../../styles/utils.module.css";
import { round } from "../../../../features/math/round";
import { ProgressBar } from "../../../_utils/progress-bar/ProgressBar"; import { ProgressBar } from "../../../_utils/progress-bar/ProgressBar";
import { Button } from "../../../_utils/button/Button"; import { Button } from "../../../_utils/button/Button";
import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext";
import { StorageManager } from "../../../../features/storage/storageManager"; import { StorageManager } from "../../../../features/storage/storageManager";
import { round } from "../../../../features/_utils/math.utils";
export function StorageTab() { export function StorageTab() {
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const maxBytes = StorageManager.MAX_BYTES; const maxBytes = StorageManager.MAX_BYTES;
const usedBytes = StorageManager.getByteSize(virtualRoot.toString()); const usedBytes = StorageManager.getByteSize(virtualRoot?.toString() ?? "");
const maxKB = StorageManager.byteToKilobyte(maxBytes); const maxKB = StorageManager.byteToKilobyte(maxBytes);
const usedKB = StorageManager.byteToKilobyte(usedBytes); const usedKB = StorageManager.byteToKilobyte(usedBytes);
@ -29,7 +29,7 @@ export function StorageTab() {
<p className={styles.Label}>Manage data</p> <p className={styles.Label}>Manage data</p>
<Button <Button
className={`${styles.Button} ${styles.ButtonDanger} ${utilStyles.TextBold}`} className={`${styles.Button} ${styles.ButtonDanger} ${utilStyles.TextBold}`}
onClick={() => { virtualRoot.reset?.(); }} onClick={() => { virtualRoot?.reset(); }}
> >
Reset Reset
</Button> </Button>

View file

@ -28,7 +28,7 @@ function ansiToJSON(input: string, use_classes: boolean | undefined): AnserJsonE
* Create a class string. * Create a class string.
* @returns class name(s) * @returns class name(s)
*/ */
function createClass(bundle: AnserJsonEntry): string { function createClass(bundle: AnserJsonEntry): string | null {
const classNames = []; const classNames = [];
if (bundle.bg) { if (bundle.bg) {
@ -111,7 +111,7 @@ function convertBundleIntoReact(linkify: boolean, useClasses: boolean, bundle: A
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g; const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
let index = 0; let index = 0;
let match: RegExpExecArray; let match: RegExpExecArray | null;
while ((match = linkRegex.exec(bundle.content)) !== null) { while ((match = linkRegex.exec(bundle.content)) !== null) {
const [, pre, url] = match; const [, pre, url] = match;
@ -145,7 +145,7 @@ function convertBundleIntoReact(linkify: boolean, useClasses: boolean, bundle: A
return React.createElement("span", { style, key, className }, content); return React.createElement("span", { style, key, className }, content);
} }
export default function Ansi(props: { children?: string | undefined; linkify?: boolean | undefined; className?: string | undefined; useClasses?: boolean | undefined; }) { export function Ansi(props: { children?: string | undefined; linkify?: boolean | undefined; className?: string | undefined; useClasses?: boolean | undefined; }) {
const { className, useClasses, children, linkify } = props; const { className, useClasses, children, linkify } = props;
return React.createElement( return React.createElement(
"code", "code",

View file

@ -1,6 +1,6 @@
import { CSSProperties, MutableRefObject, useState } from "react"; import { CSSProperties, MutableRefObject, useState } from "react";
import styles from "./Terminal.module.css"; import styles from "./Terminal.module.css";
import Ansi from "./Ansi"; import { Ansi } from "./Ansi";
interface InputLineProps { interface InputLineProps {
@ -16,7 +16,8 @@ export function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown, inputRe
const [cursorPosition, setCursorPosition] = useState(0); const [cursorPosition, setCursorPosition] = useState(0);
const checkCursorPosition = () => { const checkCursorPosition = () => {
setCursorPosition(inputRef.current?.selectionStart); const selectionStart = inputRef.current?.selectionStart;
if (selectionStart != null) setCursorPosition(selectionStart);
}; };
return ( return (

View file

@ -1,16 +1,16 @@
import { FC, forwardRef } from "react"; import { FC, forwardRef } from "react";
import Ansi from "./Ansi"; import { Ansi } from "./Ansi";
import styles from "./Terminal.module.css"; import styles from "./Terminal.module.css";
interface OutputLineProps { interface OutputLineProps {
text: string; text?: string;
} }
export const OutputLine: FC<OutputLineProps> = forwardRef<HTMLDivElement>(({ text }: OutputLineProps, ref) => { export const OutputLine: FC<OutputLineProps> = forwardRef<HTMLDivElement>(({ text }: OutputLineProps, ref) => {
const lines = text?.split("\n"); const lines = text?.split("\n");
return <div ref={ref}> return <div ref={ref}>
{lines.map((line, index) => {lines?.map((line, index) =>
<Ansi key={index} className={styles.Output} useClasses>{line === "" ? " " : line}</Ansi> <Ansi key={index} className={styles.Output} useClasses>{line === "" ? " " : line}</Ansi>
)} )}
</div>; </div>;

View file

@ -1,19 +1,20 @@
import { MouseEventHandler, useEffect, useRef, useState } from "react"; import { MouseEventHandler, MutableRefObject, useEffect, useRef, useState } from "react";
import styles from "./Terminal.module.css"; import styles from "./Terminal.module.css";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext";
import { clamp } from "../../../features/math/clamp";
import { OutputLine } from "./OutputLine"; import { OutputLine } from "./OutputLine";
import { InputLine } from "./InputLine"; import { InputLine } from "./InputLine";
import { ANSI, HOSTNAME, USERNAME, WELCOME_MESSAGE } from "../../../config/apps/terminal.config"; import { ANSI, HOSTNAME, USERNAME, WELCOME_MESSAGE } from "../../../config/apps/terminal.config";
import CommandsManager from "../../../features/apps/terminal/commands"; import { CommandsManager } from "../../../features/apps/terminal/commands";
import { removeFromArray } from "../../../features/_utils/array.utils"; import { removeFromArray } from "../../../features/_utils/array.utils";
import Stream from "../../../features/apps/terminal/stream"; import { Stream } from "../../../features/apps/terminal/stream";
import { formatError } from "../../../features/apps/terminal/_utils/terminal.utils"; import { formatError } from "../../../features/apps/terminal/_utils/terminal.utils";
import { WindowProps } from "../../windows/WindowView"; import { WindowProps } from "../../windows/WindowView";
import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder";
import { CommandResponse } from "../../../features/apps/terminal/command"; import { CommandResponse } from "../../../features/apps/terminal/command";
import { APP_NAMES } from "../../../config/apps.config"; import { APP_NAMES } from "../../../config/apps.config";
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext"; import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext";
import { SettingsManager } from "../../../features/settings/settingsManager";
import { clamp } from "../../../features/_utils/math.utils";
interface TerminalProps extends WindowProps { interface TerminalProps extends WindowProps {
path: string; path: string;
@ -35,18 +36,19 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
isInput: false, isInput: false,
}]); }]);
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~")); const [currentDirectory, setCurrentDirectory] = useState(virtualRoot?.navigate(startPath ?? "~"));
const inputRef = useRef(null); const inputRef = useRef(null);
const [historyIndex, setHistoryIndex] = useState(0); const [historyIndex, setHistoryIndex] = useState(0);
const [stream, setStream] = useState<Stream>(null); const [stream, setStream] = useState<Stream | null>(null);
const [streamOutput, setStreamOutput] = useState<string>(null); const [streamOutput, setStreamOutput] = useState<string | null>(null);
const ref = useRef(null); const ref = useRef(null);
const [streamFocused, setStreamFocused] = useState(false); const [streamFocused, setStreamFocused] = useState(false);
const settingsManager = useSettingsManager(); const settingsManager = useSettingsManager();
useEffect(() => { useEffect(() => {
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`); if (currentDirectory != null)
}, [currentDirectory.path, currentDirectory.root, setTitle]); setTitle?.(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
}, [currentDirectory?.path, currentDirectory?.root, setTitle]);
useEffect(() => { useEffect(() => {
if (!inputRef.current || !active) if (!inputRef.current || !active)
@ -75,7 +77,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
}, [inputValue]); }, [inputValue]);
const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:` const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:`
+ `${ANSI.fg.blue + (currentDirectory.root ? "/" : currentDirectory.path) + ANSI.reset}$ `; + `${ANSI.fg.blue + ((currentDirectory?.root || currentDirectory == null) ? "/" : currentDirectory?.path) + ANSI.reset}$ `;
const updatedHistory = history; const updatedHistory = history;
const pushHistory = (entry: HistoryEntry) => { const pushHistory = (entry: HistoryEntry) => {
@ -100,11 +102,11 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
} }
}; };
let lastOutput: CommandResponse = null; let lastOutput: CommandResponse | null = null;
stream.on(Stream.EVENT_NAMES.new, (text: string) => { stream.on(Stream.EVENT_NAMES.new, (text) => {
void (async () => { void (async () => {
let output: CommandResponse = text; let output: CommandResponse = text as CommandResponse;
for (const pipe of pipes) { for (const pipe of pipes) {
if (output instanceof Stream) if (output instanceof Stream)
@ -141,35 +143,31 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
const rawInputValueStart = value.indexOf(" ") + 1; const rawInputValueStart = value.indexOf(" ") + 1;
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart); const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
const timestamp = Date.now(); const timestamp = Date.now();
value = value.trim(); value = value.trim();
if (value === "") return;
if (value === "") // Parse arguments
return;
// Get arguments
const args = value.match(/(?:[^\s"]+|"[^"]*")+/g); const args = value.match(/(?:[^\s"]+|"[^"]*")+/g);
if (args == null) return;
if (args[0].toLowerCase() === "sudo" && args.length > 1) { if (args[0].toLowerCase() === "sudo" && args.length >= 2) args.shift();
args.shift();
}
// Get command // Get command
const commandName = args.shift().toLowerCase(); const commandName = args.shift()?.toLowerCase();
if (commandName == null) return;
const command = CommandsManager.find(commandName); const command = CommandsManager.find(commandName);
if (!command) if (!command) return formatError(commandName, "Command not found");
return formatError(commandName, "Command not found");
// Get options // Get options
const options: string[] = []; const options: string[] = [];
const inputs = {}; const inputs: Record<string, string> = {};
args.filter((arg: string) => arg.startsWith("-")).forEach((option: string) => { args.filter((arg: string) => arg.startsWith("-")).forEach((option: string) => {
const addOption = (key: string) => { const addOption = (key: string) => {
if (options.includes(key)) if (options.includes(key))
return; return;
options.push(key); options.push(key);
const commandOption = command.getOption(options[options.length - 1]); const commandOption = command.getOption(options[options.length - 1]);
if (commandOption?.isInput) { if (commandOption?.isInput) {
@ -200,7 +198,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
return formatError(commandName, `Incorrect usage: ${commandName} requires at least 1 option`); return formatError(commandName, `Incorrect usage: ${commandName} requires at least 1 option`);
// Execute command // Execute command
let response: CommandResponse = null; let response: CommandResponse | null = null;
try { try {
response = await command.execute(args, { response = await command.execute(args, {
@ -216,7 +214,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
exit, exit,
inputs, inputs,
timestamp, timestamp,
settingsManager, settingsManager: settingsManager as SettingsManager,
}); });
if (response == null) if (response == null)
@ -244,15 +242,15 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
// Piping is used to chain commands // Piping is used to chain commands
let pipes = value.split(" | "); let pipes = value.split(" | ");
const completedPipes = []; const completedPipes: string[] = [];
let output = null; let output: CommandResponse | null = null;
for (const pipe of pipes) { for (const pipe of pipes) {
if (output instanceof Stream) if (output instanceof Stream)
continue; continue;
// Output from the previous command gets added as an argument for the next command // Output from the previous command gets added as an argument for the next command
output = await handleInput(output ? `${pipe} ${output}` : pipe); output = await handleInput(output ? `${pipe} ${output as string}` : pipe);
completedPipes.push(pipe); completedPipes.push(pipe);
} }
@ -264,7 +262,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
if (output instanceof Stream) { if (output instanceof Stream) {
connectStream(output, pipes); connectStream(output, pipes);
} else { } else {
promptOutput(`${output}\n`); promptOutput(`${output as string}\n`);
} }
} }
}; };
@ -284,7 +282,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
if (index === 0) { if (index === 0) {
setInputValue(""); setInputValue("");
} else { } else {
setInputValue(inputHistory[inputHistory.length - index].value); setInputValue(inputHistory[inputHistory.length - index].value ?? "");
} }
setHistoryIndex(index); setHistoryIndex(index);
@ -352,9 +350,9 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
onMouseDown={onMouseDown as unknown as MouseEventHandler} onMouseDown={onMouseDown as unknown as MouseEventHandler}
onContextMenu={onContextMenu as unknown as MouseEventHandler} onContextMenu={onContextMenu as unknown as MouseEventHandler}
onClick={(event) => { onClick={(event) => {
if (window.getSelection().toString() === "") { if (window.getSelection()?.toString() === "") {
event.preventDefault(); event.preventDefault();
(inputRef.current as HTMLInputElement)?.focus(); (inputRef.current as HTMLInputElement | null)?.focus();
} }
}} }}
> >
@ -366,7 +364,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
prefix={prefix} prefix={prefix}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onChange={onChange} onChange={onChange}
inputRef={inputRef} inputRef={inputRef as unknown as MutableRefObject<HTMLInputElement>}
/> />
: <OutputLine text={streamOutput ?? ""}/> : <OutputLine text={streamOutput ?? ""}/>
} }

View file

@ -1,9 +1,9 @@
import { ReactNode, useEffect, useRef, useState } from "react"; import { ReactNode, Ref, useEffect, useRef, useState } from "react";
import styles from "./TextEditor.module.css"; import styles from "./TextEditor.module.css";
import { HeaderMenu } from "../_utils/header-menu/HeaderMenu"; import { HeaderMenu } from "../_utils/header-menu/HeaderMenu";
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import { CODE_FORMATS, DEFAULT_ZOOM, EXTENSION_TO_LANGUAGE, ZOOM_FACTOR } from "../../../config/apps/textEditor.config"; import { CODE_FORMATS, DEFAULT_ZOOM, EXTENSION_TO_LANGUAGE, ZOOM_FACTOR } from "../../../config/apps/textEditor.config";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import { TITLE_SEPARATOR } from "../../../config/windows.config"; import { TITLE_SEPARATOR } from "../../../config/windows.config";
import { MarkdownLink } from "./overrides/MarkdownLink"; import { MarkdownLink } from "./overrides/MarkdownLink";
import { MarkdownImage } from "./overrides/MarkdownImage"; import { MarkdownImage } from "./overrides/MarkdownImage";
@ -17,9 +17,9 @@ import { VirtualFile } from "../../../features/virtual-drive/file";
import { WindowProps } from "../../windows/WindowView"; import { WindowProps } from "../../windows/WindowView";
import { DropdownAction } from "../../actions/actions/DropdownAction"; import { DropdownAction } from "../../actions/actions/DropdownAction";
import { ClickAction } from "../../actions/actions/ClickAction"; import { ClickAction } from "../../actions/actions/ClickAction";
import ModalsManager from "../../../features/modals/modalsManager"; import { ModalsManager } from "../../../features/modals/modalsManager";
import App from "../../../features/apps/app"; import { App } from "../../../features/apps/app";
import WindowsManager from "../../../features/windows/windowsManager"; import { WindowsManager } from "../../../features/windows/windowsManager";
import { MarkdownBlockquote } from "./overrides/MarkdownBlockquote"; import { MarkdownBlockquote } from "./overrides/MarkdownBlockquote";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext";
import { APP_NAMES } from "../../../config/apps.config"; import { APP_NAMES } from "../../../config/apps.config";
@ -47,10 +47,10 @@ interface TextEditorProps extends WindowProps {
} }
export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app, modalsManager }: TextEditorProps) { export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app, modalsManager }: TextEditorProps) {
const ref = useRef(); const ref = useRef<HTMLDivElement | HTMLTextAreaElement>();
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const [currentFile, setCurrentFile] = useState(file); const [currentFile, setCurrentFile] = useState<VirtualFile | null>(file as VirtualFile);
const [currentMode, setCurrentMode] = useState<TextEditorProps["mode"]>(mode); const [currentMode, setCurrentMode] = useState<TextEditorProps["mode"]>(mode);
const [content, setContent] = useState(file?.content ?? ""); const [content, setContent] = useState(file?.content ?? "");
const [unsavedChanges, setUnsavedChanges] = useState(file == null); const [unsavedChanges, setUnsavedChanges] = useState(file == null);
@ -76,12 +76,12 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
const iconUrl = currentFile.getIconUrl(); const iconUrl = currentFile.getIconUrl();
if (iconUrl) if (iconUrl)
setIconUrl(iconUrl); setIconUrl?.(iconUrl);
if (newContent?.trim() === "") if (newContent?.trim() === "")
setCurrentMode("edit"); setCurrentMode("edit");
} else { } else if (app != null) {
setIconUrl(AppsManager.getAppIconUrl(app.id)); setIconUrl?.(AppsManager.getAppIconUrl(app.id));
} }
setContent(newContent); setContent(newContent);
@ -90,7 +90,7 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
(ref.current as HTMLElement).scrollTo(0, 0); (ref.current as HTMLElement).scrollTo(0, 0);
} }
})(); })();
}, [app.id, currentFile, setIconUrl]); }, [app?.id, currentFile, setIconUrl]);
useEffect(() => { useEffect(() => {
// Update title // Update title
@ -102,12 +102,12 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
if (currentMode === "view") if (currentMode === "view")
label += " (preview)"; label += " (preview)";
setTitle(`${label} ${TITLE_SEPARATOR} ${app.name}`); setTitle?.(app != null ? `${label} ${TITLE_SEPARATOR} ${app.name}` : label);
}, [currentFile, setTitle, unsavedChanges, currentMode, app.name]); }, [currentFile, setTitle, unsavedChanges, currentMode, app?.name]);
useEffect(() => { useEffect(() => {
if (!initialised && currentFile == null && path != null) { if (!initialised && currentFile == null && path != null) {
const newFile = virtualRoot.navigate(path); const newFile = virtualRoot?.navigate(path);
if (newFile == null || !newFile.isFile()) if (newFile == null || !newFile.isFile())
return; return;
@ -147,7 +147,7 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
return setContent(value); return setContent(value);
}; };
const overrides = {}; const overrides: Record<string, object> = {};
for (const [key, value] of Object.entries(OVERRIDES)) { for (const [key, value] of Object.entries(OVERRIDES)) {
overrides[key] = { overrides[key] = {
component: value, component: value,
@ -169,10 +169,10 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
<ClickAction label="Open" onTrigger={() => { <ClickAction label="Open" onTrigger={() => {
openWindowedModal({ openWindowedModal({
size: DEFAULT_FILE_SELECTOR_SIZE, size: DEFAULT_FILE_SELECTOR_SIZE,
Modal: (props) => <FileSelector Modal: (props: object) => <FileSelector
type={SELECTOR_MODE.SINGLE} type={SELECTOR_MODE.SINGLE}
onFinish={(file: VirtualFile) => { onFinish={(file) => {
setCurrentFile(file); setCurrentFile(file as VirtualFile);
setUnsavedChanges(false); setUnsavedChanges(false);
}} }}
{...props} {...props}
@ -182,15 +182,15 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
<Divider/> <Divider/>
<ClickAction label="Save" onTrigger={() => { saveText(); }} shortcut={["Control", "s"]}/> <ClickAction label="Save" onTrigger={() => { saveText(); }} shortcut={["Control", "s"]}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} disabled={currentFile == null} onTrigger={() => { <ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} disabled={currentFile == null} onTrigger={() => {
currentFile.parent.open(windowsManager); if (windowsManager != null) currentFile?.parent?.open(windowsManager);
}}/> }}/>
<Divider/> <Divider/>
<ClickAction label="Quit" onTrigger={() => { close(); }} shortcut={["Control", "q"]}/> <ClickAction label="Quit" onTrigger={() => { close?.(); }} shortcut={["Control", "q"]}/>
</DropdownAction> </DropdownAction>
<DropdownAction label="View" showOnHover={false}> <DropdownAction label="View" showOnHover={false}>
<ClickAction label={currentMode === "view" ? "Edit mode" : "Preview mode"} onTrigger={() => { <ClickAction label={currentMode === "view" ? "Edit mode" : "Preview mode"} onTrigger={() => {
setCurrentMode(currentMode === "view" ? "edit" : "view"); setCurrentMode(currentMode === "view" ? "edit" : "view");
}} shortcut={["Control", "v"]}/> }} shortcut={["Control", "u"]}/>
<Divider/> <Divider/>
<ClickAction label="Zoom in" onTrigger={() => { setZoom(zoom + ZOOM_FACTOR); }} shortcut={["Control", "+"]}/> <ClickAction label="Zoom in" onTrigger={() => { setZoom(zoom + ZOOM_FACTOR); }} shortcut={["Control", "+"]}/>
<ClickAction label="Zoom out" onTrigger={() => { setZoom(zoom - ZOOM_FACTOR); }} shortcut={["Control", "-"]}/> <ClickAction label="Zoom out" onTrigger={() => { setZoom(zoom - ZOOM_FACTOR); }} shortcut={["Control", "-"]}/>
@ -198,21 +198,21 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
</DropdownAction> </DropdownAction>
</HeaderMenu> </HeaderMenu>
{currentMode === "view" {currentMode === "view"
? CODE_FORMATS.includes(currentFile?.extension) ? currentFile?.extension != null && CODE_FORMATS.includes(currentFile?.extension)
? <SyntaxHighlighter ? <SyntaxHighlighter
language={EXTENSION_TO_LANGUAGE[currentFile?.extension] ?? currentFile?.extension} language={EXTENSION_TO_LANGUAGE[currentFile?.extension] ?? currentFile?.extension}
className={styles.Code} className={styles.Code}
useInlineStyles={false} useInlineStyles={false}
showLineNumbers={true} showLineNumbers={true}
>{content}</SyntaxHighlighter> >{content}</SyntaxHighlighter>
: <div ref={ref} className={styles.View}> : <div ref={ref as Ref<HTMLDivElement>} className={styles.View}>
{currentFile?.extension === "md" {currentFile?.extension === "md"
? <Markdown options={{ overrides }}>{content}</Markdown> ? <Markdown options={{ overrides } as object}>{content}</Markdown>
: <pre><p>{content}</p></pre> : <pre><p>{content}</p></pre>
} }
</div> </div>
: <textarea : <textarea
ref={ref} ref={ref as Ref<HTMLTextAreaElement>}
className={styles.View} className={styles.View}
value={content} value={content}
onChange={onChange} onChange={onChange}

View file

@ -10,11 +10,10 @@ type Props = Partial<unknown> & Attributes & { children: ReactNode, className?:
export function MarkdownBlockquote({ children, ...props }: MarkdownProps) { export function MarkdownBlockquote({ children, ...props }: MarkdownProps) {
sanitizeProps(props); sanitizeProps(props);
const formatContent = (children: ReactNode): [ReactNode, string] => { const formatContent = (children: ReactNode): [ReactNode, string | null] => {
if (!children) if (!children) return [null, null];
return [null, null];
let alertType: string = null; let alertType: string | null = null;
children = Children.map(children, (child) => { children = Children.map(children, (child) => {
if (isValidElement(child)) { if (isValidElement(child)) {

View file

@ -17,7 +17,7 @@ export function MarkdownImage({ alt, src, ...props }: MarkdownImageProps) {
return src; return src;
}, [src]); }, [src]);
sanitizeProps(props as MarkdownProps); sanitizeProps(props);
return <img return <img
{...props} {...props}

View file

@ -4,16 +4,17 @@ import { useContextMenu } from "../../../../hooks/modals/contextMenu";
import { Actions } from "../../../actions/Actions"; import { Actions } from "../../../actions/Actions";
import { ClickAction } from "../../../actions/actions/ClickAction"; import { ClickAction } from "../../../actions/actions/ClickAction";
import { useWindowedModal } from "../../../../hooks/modals/windowedModal"; import { useWindowedModal } from "../../../../hooks/modals/windowedModal";
import AppsManager from "../../../../features/apps/appsManager"; import { AppsManager } from "../../../../features/apps/appsManager";
import { DialogBox } from "../../../modals/dialog-box/DialogBox"; import { DialogBox } from "../../../modals/dialog-box/DialogBox";
import { DIALOG_CONTENT_TYPES } from "../../../../config/modals.config"; import { DIALOG_CONTENT_TYPES } from "../../../../config/modals.config";
import Vector2 from "../../../../features/math/vector2"; import { Vector2 } from "../../../../features/math/vector2";
import { APPS } from "../../../../config/apps.config"; import { APPS } from "../../../../config/apps.config";
import { MarkdownProps } from "../TextEditor"; import { MarkdownProps } from "../TextEditor";
import { sanitizeProps } from "../../../../features/apps/text-editor/_utils/sanitizeProps"; import { sanitizeProps } from "../../../../features/apps/text-editor/_utils/sanitizeProps";
import { copyToClipboard, removeUrlProtocol } from "../../../../features/_utils/browser.utils"; import { copyToClipboard, removeUrlProtocol } from "../../../../features/_utils/browser.utils";
import { TextDisplay } from "../../../actions/actions/TextDisplay"; import { TextDisplay } from "../../../actions/actions/TextDisplay";
import styles from "../TextEditor.module.css"; import styles from "../TextEditor.module.css";
import { ModalProps } from "../../../modals/ModalView";
interface MarkdownLinkProps extends MarkdownProps { interface MarkdownLinkProps extends MarkdownProps {
href: string; href: string;
@ -27,7 +28,7 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
event.preventDefault(); event.preventDefault();
if (!href.startsWith("http://") && !href.startsWith("https://")) { if (!href.startsWith("http://") && !href.startsWith("https://")) {
const target = currentFile.parent.navigate(href); const target = currentFile.parent?.navigate(href);
if (target != null) { if (target != null) {
if (target.isFile()) { if (target.isFile()) {
setCurrentFile(target); setCurrentFile(target);
@ -39,22 +40,24 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
iconUrl: AppsManager.getAppIconUrl(app.id), iconUrl: AppsManager.getAppIconUrl(app.id),
title: "Failed to open link", title: "Failed to open link",
size: new Vector2(450, 150), size: new Vector2(450, 150),
Modal: (props) => Modal: (props: ModalProps) =>
<DialogBox {...props}> <DialogBox {...props}>
<p>Target not found: "{href}"</p> <p>Target not found: "{href}"</p>
<button data-type={DIALOG_CONTENT_TYPES.CloseButton}>Ok</button> <button data-type={DIALOG_CONTENT_TYPES.closeButton}>Ok</button>
</DialogBox> </DialogBox>
}); });
} }
} else { } else {
window.open(href, "_blank").focus(); window.open(href, "_blank")?.focus();
} }
}; };
const { onContextMenu } = useContextMenu({ Actions: (props) => const { onContextMenu } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<TextDisplay>{removeUrlProtocol(href)}</TextDisplay> <TextDisplay>{removeUrlProtocol(href)}</TextDisplay>
<ClickAction label="Open link" icon={faExternalLink} onTrigger={onClick}/> <ClickAction label="Open link" icon={faExternalLink} onTrigger={(event) => {
onClick(event as MouseEvent);
}}/>
<ClickAction label="Copy link" icon={faClipboard} onTrigger={() => { <ClickAction label="Copy link" icon={faClipboard} onTrigger={() => {
copyToClipboard(href); copyToClipboard(href);
}}/> }}/>
@ -62,16 +65,16 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
}); });
const title = useMemo<string>(() => { const title = useMemo<string>(() => {
let title: string = null; let title: string = "";
try { try {
title = new URL(href).hostname; title = new URL(href).hostname;
} catch (error) { } catch (error) {
title = href.split("/").pop(); title = href.split("/").pop() ?? "";
} }
return title; return title;
}, [href]); }, [href]);
sanitizeProps(props as MarkdownProps); sanitizeProps(props);
return <a return <a
{...props} {...props}
@ -79,7 +82,7 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
href={href} href={href}
onContextMenu={onContextMenu} onContextMenu={onContextMenu as unknown as MouseEventHandler}
onClick={onClick as unknown as MouseEventHandler} onClick={onClick as unknown as MouseEventHandler}
title={title} title={title}
> >

View file

@ -1,4 +1,4 @@
import { memo, useState } from "react"; import { memo, MouseEventHandler, useState } from "react";
import { SettingsManager } from "../../features/settings/settingsManager"; import { SettingsManager } from "../../features/settings/settingsManager";
import { useSettingsManager } from "../../hooks/settings/settingsManagerContext"; import { useSettingsManager } from "../../hooks/settings/settingsManagerContext";
import styles from "./Desktop.module.css"; import styles from "./Desktop.module.css";
@ -8,9 +8,9 @@ import { useContextMenu } from "../../hooks/modals/contextMenu";
import { FALLBACK_ICON_DIRECTION, FALLBACK_ICON_SIZE, FALLBACK_WALLPAPER } from "../../config/desktop.config"; import { FALLBACK_ICON_DIRECTION, FALLBACK_ICON_SIZE, FALLBACK_WALLPAPER } from "../../config/desktop.config";
import { reloadViewport } from "../../features/_utils/browser.utils"; import { reloadViewport } from "../../features/_utils/browser.utils";
import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext";
import { DirectoryList } from "../apps/file-explorer/directory-list/DirectoryList"; import { DirectoryList, FileEventHandler, FolderEventHandler } from "../apps/file-explorer/directory-list/DirectoryList";
import { APPS, APP_ICONS, APP_NAMES } from "../../config/apps.config"; import { APPS, APP_ICONS, APP_NAMES } from "../../config/apps.config";
import Vector2 from "../../features/math/vector2"; import { Vector2 } from "../../features/math/vector2";
import { Actions } from "../actions/Actions"; import { Actions } from "../actions/Actions";
import { ClickAction } from "../actions/actions/ClickAction"; import { ClickAction } from "../actions/actions/ClickAction";
import { faArrowsRotate, faCompress, faExpand, faEye, faFolder, faPaintBrush, faTerminal, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faArrowsRotate, faCompress, faExpand, faEye, faFolder, faPaintBrush, faTerminal, faTrash } from "@fortawesome/free-solid-svg-icons";
@ -21,15 +21,16 @@ import { Divider } from "../actions/actions/Divider";
import { isValidInteger } from "../../features/_utils/number.utils"; import { isValidInteger } from "../../features/_utils/number.utils";
import { useWindowedModal } from "../../hooks/modals/windowedModal"; import { useWindowedModal } from "../../hooks/modals/windowedModal";
import { Share } from "../modals/share/Share"; import { Share } from "../modals/share/Share";
import ModalsManager from "../../features/modals/modalsManager"; import { ModalsManager } from "../../features/modals/modalsManager";
import { VirtualFolder } from "../../features/virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../features/virtual-drive/folder/virtualFolder";
import { VirtualFolderLink } from "../../features/virtual-drive/folder/virtualFolderLink"; import { VirtualFolderLink } from "../../features/virtual-drive/folder/virtualFolderLink";
import { VirtualFile } from "../../features/virtual-drive/file"; import { VirtualFile } from "../../features/virtual-drive/file";
import { TABS } from "../../config/apps/settings.config"; import { TABS } from "../../config/apps/settings.config";
import { ModalProps } from "../modals/ModalView";
export const Desktop = memo(() => { export const Desktop = memo(() => {
const settingsManager = useSettingsManager(); const settingsManager = useSettingsManager();
const [wallpaper, setWallpaper] = useState<string>(null); const [wallpaper, setWallpaper] = useState<string | null>(null);
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot(); const virtualRoot = useVirtualRoot();
const [showIcons, setShowIcons] = useState(false); const [showIcons, setShowIcons] = useState(false);
@ -37,31 +38,31 @@ export const Desktop = memo(() => {
const [iconDirection, setIconDirection] = useState(FALLBACK_ICON_DIRECTION); const [iconDirection, setIconDirection] = useState(FALLBACK_ICON_DIRECTION);
const { openWindowedModal } = useWindowedModal(); const { openWindowedModal } = useWindowedModal();
const directory = virtualRoot.navigate("~/Desktop"); const directory = virtualRoot?.navigate("~/Desktop");
const { onContextMenu, ShortcutsListener } = useContextMenu({ Actions: (props) => const { onContextMenu, ShortcutsListener } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<DropdownAction label="View" icon={faEye}> <DropdownAction label="View" icon={faEye}>
<RadioAction initialIndex={iconSize} onTrigger={(event, params, value: string) => { <RadioAction initialIndex={iconSize} onTrigger={(event, params, value) => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("icon-size", value); void settings?.set("icon-size", value as string);
}} options={[ }} options={[
{ label: "Small icons" }, { label: "Small icons" },
{ label: "Medium icons" }, { label: "Medium icons" },
{ label: "Large icons" }, { label: "Large icons" },
]}/> ]}/>
<Divider/> <Divider/>
<RadioAction initialIndex={iconDirection} onTrigger={(event, params, value: string) => { <RadioAction initialIndex={iconDirection} onTrigger={(event, params, value) => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("icon-direction", value); void settings?.set("icon-direction", value as string);
}} options={[ }} options={[
{ label: "Align vertically" }, { label: "Align vertically" },
{ label: "Align horizontally" }, { label: "Align horizontally" },
]}/> ]}/>
<Divider/> <Divider/>
<ToggleAction label="Show dekstop icons" initialValue={showIcons} onTrigger={() => { <ToggleAction label="Show dekstop icons" initialValue={showIcons} onTrigger={() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("show-icons", (!showIcons).toString()); void settings?.set("show-icons", (!showIcons).toString());
}}/> }}/>
</DropdownAction> </DropdownAction>
<ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => { <ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => {
@ -72,7 +73,7 @@ export const Desktop = memo(() => {
shortcut={["F11"]} shortcut={["F11"]}
icon={!document.fullscreenElement ? faExpand : faCompress} icon={!document.fullscreenElement ? faExpand : faCompress}
onTrigger={() => { onTrigger={() => {
if (windowsManager.isAnyFocused()) if (windowsManager?.isAnyFocused())
return; return;
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
@ -87,78 +88,78 @@ export const Desktop = memo(() => {
}} }}
/> />
<ClickAction label="Change appearance" icon={faPaintBrush} onTrigger={() => { <ClickAction label="Change appearance" icon={faPaintBrush} onTrigger={() => {
windowsManager.open("settings", { tab: TABS.APPEARANCE }); windowsManager?.open("settings", { tab: TABS.APPEARANCE });
}}/> }}/>
<Divider/> <Divider/>
<ClickAction label={`Open in ${APP_NAMES.FILE_EXPLORER}`} icon={APP_ICONS.FILE_EXPLORER} onTrigger={() => { <ClickAction label={`Open in ${APP_NAMES.FILE_EXPLORER}`} icon={APP_ICONS.FILE_EXPLORER} onTrigger={() => {
windowsManager.open(APPS.FILE_EXPLORER, { path: directory.path }); windowsManager?.open(APPS.FILE_EXPLORER, { path: directory?.path });
}}/> }}/>
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={() => { <ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={() => {
windowsManager.open(APPS.TERMINAL, { path: directory.path }); windowsManager?.open(APPS.TERMINAL, { path: directory?.path });
}}/> }}/>
<Divider/> <Divider/>
<ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} onTrigger={() => { <ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} onTrigger={() => {
openWindowedModal({ openWindowedModal({
size: new Vector2(350, 350), size: new Vector2(350, 350),
Modal: (props) => <Share {...props}/> Modal: (props: ModalProps) => <Share {...props}/>
}); });
}}/> }}/>
</Actions> </Actions>
}); });
const { onContextMenu: onContextMenuFile } = useContextMenu({ Actions: (props) => const { onContextMenu: onContextMenuFile } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<ClickAction label="Open" onTrigger={(event, file: VirtualFile) => { <ClickAction label="Open" onTrigger={(event, file) => {
file.open(windowsManager); if (windowsManager != null) (file as VirtualFile).open(windowsManager);
}}/> }}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, file: VirtualFile) => { <ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, file) => {
file.parent.open(windowsManager); if (windowsManager != null) (file as VirtualFile).parent?.open(windowsManager);
}}/> }}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file: VirtualFile) => { <ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
file.delete(); (file as VirtualFile).delete();
}}/> }}/>
</Actions> </Actions>
}); });
const { onContextMenu: onContextMenuFolder } = useContextMenu({ Actions: (props) => const { onContextMenu: onContextMenuFolder } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder: VirtualFolder) => { <ClickAction label="Open" onTrigger={(event, folder) => {
folder.open(windowsManager); if (windowsManager != null) (folder as VirtualFolder).open(windowsManager);
}}/> }}/>
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={faTerminal} onTrigger={(event, folder: VirtualFolder) => { <ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={faTerminal} onTrigger={(event, folder) => {
windowsManager.open(APPS.TERMINAL, { path: folder.path }); windowsManager?.open(APPS.TERMINAL, { path: (folder as VirtualFolder).path });
}}/> }}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, folder: VirtualFolder) => { <ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, folder) => {
folder.parent.open(windowsManager); if (windowsManager != null) (folder as VirtualFolder).parent?.open(windowsManager);
}}/> }}/>
<Divider/> <Divider/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder: VirtualFolder) => { <ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
folder.delete(); (folder as VirtualFolder).delete();
}}/> }}/>
</Actions> </Actions>
}); });
useEffect(() => { useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.get("wallpaper", setWallpaper); void settings?.get("wallpaper", setWallpaper);
void settings.get("show-icons", (value) => { void settings?.get("show-icons", (value) => {
if (value != null) { if (value != null) {
setShowIcons(value === "true"); setShowIcons(value === "true");
} else { } else {
setShowIcons(true); setShowIcons(true);
} }
}); });
void settings.get("icon-size", (value) => { void settings?.get("icon-size", (value) => {
if (isValidInteger(value)) if (isValidInteger(value))
setIconSize(parseInt(value)); setIconSize(parseInt(value));
}); });
void settings.get("icon-direction", (value) => { void settings?.get("icon-direction", (value) => {
if (isValidInteger(value)) if (isValidInteger(value))
setIconDirection(parseInt(value)); setIconDirection(parseInt(value));
}); });
}, [settingsManager]); }, [settingsManager]);
const onError = () => { const onError = () => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("wallpaper", FALLBACK_WALLPAPER); void settings?.set("wallpaper", FALLBACK_WALLPAPER);
}; };
const iconScale = 1 + ((isValidInteger(iconSize) ? iconSize : FALLBACK_ICON_SIZE) - 1) / 5; const iconScale = 1 + ((isValidInteger(iconSize) ? iconSize : FALLBACK_ICON_SIZE) - 1) / 5;
@ -167,7 +168,7 @@ export const Desktop = memo(() => {
<ShortcutsListener/> <ShortcutsListener/>
<div <div
className={styles.Desktop} className={styles.Desktop}
onContextMenu={onContextMenu} onContextMenu={onContextMenu as unknown as MouseEventHandler}
> >
{showIcons && <DirectoryList {showIcons && <DirectoryList
directory={directory as VirtualFolder} directory={directory as VirtualFolder}
@ -179,7 +180,7 @@ export const Desktop = memo(() => {
fileClassName={styles["Item"]} fileClassName={styles["Item"]}
folderClassName={styles["Item"]} folderClassName={styles["Item"]}
onOpenFile={(event, file) => { onOpenFile={(event, file) => {
(event as Event).preventDefault(); (event).preventDefault();
const options: Record<string, unknown> = {}; const options: Record<string, unknown> = {};
if (file.name === "Info.md") if (file.name === "Info.md")
@ -187,13 +188,15 @@ export const Desktop = memo(() => {
if (file.extension === "md") if (file.extension === "md")
options.mode = "view"; options.mode = "view";
windowsManager.openFile(file, options); windowsManager?.openFile(file, options);
}} }}
onOpenFolder={(event, { linkedPath, path }: VirtualFolderLink & VirtualFolder) => { onOpenFolder={(event, folder) => {
windowsManager.open(APPS.FILE_EXPLORER, { path: linkedPath ?? path }); windowsManager?.open(APPS.FILE_EXPLORER, {
path: (folder as VirtualFolderLink).linkedPath ?? folder.path
});
}} }}
onContextMenuFile={onContextMenuFile} onContextMenuFile={onContextMenuFile as unknown as FileEventHandler}
onContextMenuFolder={onContextMenuFolder} onContextMenuFolder={onContextMenuFolder as unknown as FolderEventHandler}
/>} />}
{wallpaper {wallpaper
? <img src={wallpaper} className={styles.Wallpaper} alt="Desktop wallpaper" onError={onError}/> ? <img src={wallpaper} className={styles.Wallpaper} alt="Desktop wallpaper" onError={onError}/>

View file

@ -1,6 +1,6 @@
import { CSSProperties, FC, KeyboardEvent, memo, ReactNode } from "react"; import { CSSProperties, FC, KeyboardEvent, memo, ReactNode } from "react";
import Modal from "../../features/modals/modal"; import { Modal } from "../../features/modals/modal";
import OutsideClickListener from "../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../hooks/_utils/outsideClick";
import styles from "./ModalView.module.css"; import styles from "./ModalView.module.css";
import { useEffect } from "react"; import { useEffect } from "react";
@ -23,7 +23,7 @@ export const ModalView: FC<ModalProps> = memo(({ modal }) => {
useEffect(() => { useEffect(() => {
const onDismiss = (event: Event) => { const onDismiss = (event: Event) => {
if ((event as unknown as KeyboardEvent).key === "Escape") if ((event as unknown as KeyboardEvent).key === "Escape")
modal.close(); modal?.close();
}; };
document.addEventListener("keydown", onDismiss); document.addEventListener("keydown", onDismiss);
@ -33,15 +33,19 @@ export const ModalView: FC<ModalProps> = memo(({ modal }) => {
}; };
}, [modal]); }, [modal]);
if (modal?.element == null) return;
const Modal = modal.element;
const Container = () => (<div const Container = () => (<div
className={styles.ModalView} className={styles.ModalView}
style={{ "--position-x": modal.position.x, "--position-y": modal.position.y } as CSSProperties} style={{ "--position-x": modal?.position.x, "--position-y": modal?.position.y } as CSSProperties}
> >
<modal.element modal={modal} {...modal.props}/> <Modal modal={modal} {...modal?.props}/>
</div>); </div>);
if (modal.dismissible) { if (modal.dismissible) {
return (<OutsideClickListener onOutsideClick={() => { modal.close(); }}> return (<OutsideClickListener onOutsideClick={() => { modal?.close(); }}>
<Container/> <Container/>
</OutsideClickListener>); </OutsideClickListener>);
} else { } else {

View file

@ -1,26 +1,27 @@
import { memo, useEffect, useRef, useState } from "react"; import { memo, MutableRefObject, useEffect, useRef, useState } from "react";
import { ModalView } from "./ModalView"; import { ModalView } from "./ModalView";
import styles from "./ModalsView.module.css"; import styles from "./ModalsView.module.css";
import { useModals } from "../../hooks/modals/modalsContext"; import { useModals } from "../../hooks/modals/modalsContext";
import { useModalsManager } from "../../hooks/modals/modalsManagerContext"; import { useModalsManager } from "../../hooks/modals/modalsManagerContext";
import Modal from "../../features/modals/modal"; import { Modal } from "../../features/modals/modal";
export const ModalsView = memo(() => { export const ModalsView = memo(() => {
const ref = useRef(null); const ref = useRef<HTMLDivElement>(null);
const modals = useModals(); const modals = useModals();
const modalsManager = useModalsManager(); const modalsManager = useModalsManager();
const [sortedModals, setSortedModals] = useState<Modal[]>([]); const [sortedModals, setSortedModals] = useState<Modal[]>([]);
// Sort modals // Sort modals
useEffect(() => { useEffect(() => {
setSortedModals([...modals].sort((modalA, modalB) => if (modals != null)
modalA.lastInteraction - modalB.lastInteraction setSortedModals([...modals].sort((modalA, modalB) =>
)); (modalA.lastInteraction ?? 0) - (modalB.lastInteraction ?? 0)
));
}, [modals]); }, [modals]);
useEffect(() => { useEffect(() => {
if (modalsManager) if (modalsManager != null)
modalsManager.containerRef = ref; modalsManager.containerRef = ref as MutableRefObject<HTMLDivElement>;
}, [modalsManager, ref]); }, [modalsManager, ref]);
return <div ref={ref} className={styles.ModalsView}> return <div ref={ref} className={styles.ModalsView}>

View file

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useScreenDimensions } from "../../../hooks/_utils/screen"; import { useScreenDimensions } from "../../../hooks/_utils/screen";
import Vector2 from "../../../features/math/vector2"; import { Vector2 } from "../../../features/math/vector2";
import styles from "./WindowedModal.module.css"; import styles from "./WindowedModal.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
@ -10,28 +10,22 @@ import utilStyles from "../../../styles/utils.module.css";
import { ModalProps } from "../ModalView"; import { ModalProps } from "../ModalView";
export function WindowedModal({ modal, params, children, ...props }: ModalProps) { export function WindowedModal({ modal, params, children, ...props }: ModalProps) {
const { iconUrl, title } = params;
const nodeRef = useRef(null); const nodeRef = useRef(null);
const [startPosition, setStartPosition] = useState(modal.position); const [startPosition, setStartPosition] = useState(modal?.position);
const [screenWidth, screenHeight] = useScreenDimensions(); const [screenWidth, screenHeight] = useScreenDimensions();
useEffect(() => { useEffect(() => {
if (screenWidth == null || screenHeight == null) if (screenWidth == null || screenHeight == null) return;
return;
if (modal.size.x > screenWidth || modal.size.y > screenHeight) {
setStartPosition(new Vector2(0, 0)); if (modal?.position != null) {
if (modal.position.x > screenWidth) modal.position.x = 0;
if (modal.position.y > screenHeight) modal.position.y = 0;
setStartPosition(modal.position);
} else { } else {
if (modal.position.x > screenWidth) { setStartPosition(new Vector2(0, 0));
modal.position.x = 0;
setStartPosition(modal.position);
}
if (modal.position.y > screenHeight) {
modal.position.y = 0;
setStartPosition(modal.position);
}
} }
}, [modal, screenHeight, screenWidth]); }, [modal, screenHeight, screenWidth]);
@ -39,13 +33,13 @@ export function WindowedModal({ modal, params, children, ...props }: ModalProps)
axis="both" axis="both"
handle={".Window-handle"} handle={".Window-handle"}
defaultPosition={startPosition} defaultPosition={startPosition}
position={null} position={undefined}
scale={1} scale={1}
bounds={{ bounds={{
top: -modal.position.y - 1, top: -(modal?.position.y ?? 0) - 1,
bottom: screenHeight - 55 - modal.position.y, bottom: (screenHeight ?? 0) - 55 - (modal?.position.y ?? 0),
left: -modal.size.x + 85 - modal.position.x, left: -(modal?.size.x ?? 0) + 85 - (modal?.position.x ?? 0),
right: screenWidth - 5 - modal.position.x right: (screenWidth ?? 0) - 5 - (modal?.position.x ?? 0)
}} }}
cancel="button" cancel="button"
nodeRef={nodeRef} nodeRef={nodeRef}
@ -54,18 +48,18 @@ export function WindowedModal({ modal, params, children, ...props }: ModalProps)
className={styles.WindowedModal} className={styles.WindowedModal}
ref={nodeRef} ref={nodeRef}
style={{ style={{
width: modal.size.x, width: modal?.size.x ?? 0,
height: modal.size.y, height: modal?.size.y ?? 0,
}} }}
> >
<div className={`${styles.Header} Window-handle`}> <div className={`${styles.Header} Window-handle`}>
<ReactSVG <ReactSVG
className={styles["Window-icon"]} className={styles["Window-icon"]}
src={iconUrl} src={params?.iconUrl as string}
/> />
<p className={utilStyles.TextSemibold}>{title}</p> <p className={utilStyles.TextSemibold}>{params?.title as string}</p>
<button aria-label="Close" className={`${styles["Header-button"]} ${styles["Exit-button"]}`} tabIndex={0} <button aria-label="Close" className={`${styles["Header-button"]} ${styles["Exit-button"]}`} tabIndex={0}
onClick={() => { modal.close(); }}> onClick={() => { modal?.close(); }}>
<FontAwesomeIcon icon={faXmark}/> <FontAwesomeIcon icon={faXmark}/>
</button> </button>
</div> </div>

View file

@ -6,11 +6,14 @@ import styles from "./DialogBox.module.css";
export function DialogBox({ modal, params, children, ...props }: ModalProps) { export function DialogBox({ modal, params, children, ...props }: ModalProps) {
const onClick = (event: MouseEvent) => { const onClick = (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
const type = parseInt((event.target as HTMLElement).getAttribute("data-type")); const attribute = (event.target as HTMLElement).getAttribute("data-type");
if (attribute == null) return;
const type = parseInt(attribute);
switch (type) { switch (type) {
case DIALOG_CONTENT_TYPES.CloseButton: case DIALOG_CONTENT_TYPES.closeButton:
modal.close(); modal?.close();
break; break;
} }
}; };

View file

@ -3,7 +3,7 @@ import { WindowedModal } from "../_utils/WindowedModal";
import styles from "./FileProperties.module.css"; import styles from "./FileProperties.module.css";
import utilStyles from "../../../styles/utils.module.css"; import utilStyles from "../../../styles/utils.module.css";
import { StorageManager } from "../../../features/storage/storageManager"; import { StorageManager } from "../../../features/storage/storageManager";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import { ModalProps } from "../ModalView.js"; import { ModalProps } from "../ModalView.js";
import { VirtualFile } from "../../../features/virtual-drive/file"; import { VirtualFile } from "../../../features/virtual-drive/file";
@ -12,7 +12,7 @@ interface FilePropetiesProps extends ModalProps {
} }
export function FileProperties({ modal, params, file, ...props }: FilePropetiesProps) { export function FileProperties({ modal, params, file, ...props }: FilePropetiesProps) {
const associatedApp = AppsManager.getAppByFileExtension(file.extension); const associatedApp = file.extension != null ? AppsManager.getAppByFileExtension(file.extension) : null;
return <WindowedModal className={styles.FileProperties} modal={modal} params={params} {...props}> return <WindowedModal className={styles.FileProperties} modal={modal} params={params} {...props}>
<span className={styles.Section}> <span className={styles.Section}>
@ -21,15 +21,17 @@ export function FileProperties({ modal, params, file, ...props }: FilePropetiesP
</span> </span>
<span className={styles.Section}> <span className={styles.Section}>
<p className={styles.Line}>Type: {file.getType()}</p> <p className={styles.Line}>Type: {file.getType()}</p>
<span className={styles.Line}> {associatedApp != null &&
Opens with: <span className={styles.Line}>
<ImagePreview className={styles.AppIcon} source={AppsManager.getAppIconUrl(associatedApp.id)}/> Opens with:
{associatedApp.name} <ImagePreview className={styles.AppIcon} source={AppsManager.getAppIconUrl(associatedApp.id)}/>
</span> {associatedApp.name}
</span>
}
</span> </span>
<span className={styles.Section}> <span className={styles.Section}>
<p className={styles.Line}>Location: {file.path}</p> <p className={styles.Line}>Location: {file.path}</p>
<p className={styles.Line}>Size: {StorageManager.getByteSize(file.source ?? file.content)} bytes</p> <p className={styles.Line}>Size: {StorageManager.getByteSize(file.source ?? file.content as string | null)} bytes</p>
<p className={styles.Line}>Size on drive: {StorageManager.getByteSize(file.toString())} bytes</p> <p className={styles.Line}>Size on drive: {StorageManager.getByteSize(file.toString())} bytes</p>
</span> </span>
<span className={styles.Section}> <span className={styles.Section}>

View file

@ -18,8 +18,8 @@ interface FileSelectorProps extends ModalProps {
export function FileSelector({ modal, params, type, allowedFormats, onFinish, ...props }: FileSelectorProps) { export function FileSelector({ modal, params, type, allowedFormats, onFinish, ...props }: FileSelectorProps) {
const multi = (type === SELECTOR_MODE.MULTIPLE); const multi = (type === SELECTOR_MODE.MULTIPLE);
const [selection, setSelection] = useState<string[]>(multi ? [] : null); const [selection, setSelection] = useState<string[] | null>(multi ? [] : null);
const [directory, setDirectory] = useState<VirtualFolder>(null); const [directory, setDirectory] = useState<VirtualFolder | null>(null);
const finish = (event: Event) => { const finish = (event: Event) => {
event?.preventDefault(); event?.preventDefault();
@ -28,19 +28,19 @@ export function FileSelector({ modal, params, type, allowedFormats, onFinish, ..
return; return;
const files = selection.map((id) => { const files = selection.map((id) => {
const { name, extension } = VirtualFile.convertId(id); const { name, extension } = VirtualFile.splitId(id);
return directory.findFile(name, extension); return directory.findFile(name, extension);
}).filter((file) => { }).filter((file) => {
if (file == null) if (file == null)
return false; return false;
const validFormat = (allowedFormats == null || allowedFormats.includes(file.extension)); const validFormat = (allowedFormats == null || (file.extension != null && allowedFormats.includes(file.extension)));
return validFormat; return validFormat;
}); }) as VirtualFile[];
if (files.length === 0) if (files.length === 0)
return; return;
modal.close(); modal?.close();
onFinish?.(multi ? files : files[0]); onFinish?.(multi ? files : files[0]);
}; };
@ -55,7 +55,7 @@ export function FileSelector({ modal, params, type, allowedFormats, onFinish, ..
<div className={styles.Footer}> <div className={styles.Footer}>
<span className={styles.Selection}> <span className={styles.Selection}>
{multi {multi
? <p>Selected file(s): {selection.join(", ")}</p> ? <p>Selected file(s): {selection != null ? selection.join(", ") : ""}</p>
: <p>Selected file: {selection ?? ""}</p> : <p>Selected file: {selection ?? ""}</p>
} }
</span> </span>
@ -63,15 +63,15 @@ export function FileSelector({ modal, params, type, allowedFormats, onFinish, ..
<Button className={styles.Button} onClick={finish}> <Button className={styles.Button} onClick={finish}>
Confirm Confirm
</Button> </Button>
<Button className={styles.Button} onClick={() => { modal.close(); }}> <Button className={styles.Button} onClick={() => { modal?.close(); }}>
Cancel Cancel
</Button> </Button>
</div> </div>
</div> </div>
} }
onSelectionChange={({ files, directory }) => { onSelectionChange={({ files, directory }) => {
setSelection(files); setSelection(files as string[] | null);
setDirectory(directory); setDirectory(directory as VirtualFolder);
}} }}
onSelectionFinish={finish} onSelectionFinish={finish}
/> />

View file

@ -7,7 +7,7 @@ interface OptionProps {
setOption: Function; setOption: Function;
} }
export default function Option({ name, label, setOption }: OptionProps) { export function Option({ name, label, setOption }: OptionProps) {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const onChange = (event: Event) => { const onChange = (event: Event) => {

View file

@ -1,12 +1,12 @@
import { ChangeEventHandler, ReactEventHandler, UIEventHandler, useEffect, useRef, useState } from "react"; import { ChangeEventHandler, MutableRefObject, ReactEventHandler, UIEventHandler, useEffect, useRef, useState } from "react";
import ModalsManager from "../../../features/modals/modalsManager"; import { ModalsManager } from "../../../features/modals/modalsManager";
import { WindowedModal } from "../_utils/WindowedModal"; import { WindowedModal } from "../_utils/WindowedModal";
import styles from "./Share.module.css"; import styles from "./Share.module.css";
import { copyToClipboard, generateUrl } from "../../../features/_utils/browser.utils"; import { copyToClipboard, generateUrl } from "../../../features/_utils/browser.utils";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import utilStyles from "../../../styles/utils.module.css"; import utilStyles from "../../../styles/utils.module.css";
import { Button } from "../../_utils/button/Button"; import { Button } from "../../_utils/button/Button";
import Option from "./Option"; import { Option } from "./Option";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSquare } from "@fortawesome/free-regular-svg-icons"; import { faSquare } from "@fortawesome/free-regular-svg-icons";
import { faSquareCheck } from "@fortawesome/free-solid-svg-icons"; import { faSquareCheck } from "@fortawesome/free-solid-svg-icons";
@ -46,15 +46,15 @@ const APP_OPTIONS: Record<string, { label: string, name: string }[]> = {
}; };
export function Share({ modal, params, ...props }: ModalProps) { export function Share({ modal, params, ...props }: ModalProps) {
const [appId, setAppId] = useState<string>(params.appId ?? ""); const [appId, setAppId] = useState<string>(params?.appId ?? "");
const [fullscreen, setFullscreen] = useState<boolean>(params.fullscreen ?? false); const [fullscreen, setFullscreen] = useState<boolean>(params?.fullscreen ?? false);
const [standalone, setStandalone] = useState<boolean>(params.standalone ?? false); const [standalone, setStandalone] = useState<boolean>(params?.standalone ?? false);
const [options, setOptions] = useState({}); const [options, setOptions] = useState({});
const [url, setUrl] = useState<string | null>(null); const [url, setUrl] = useState<string | null>(null);
const { alert } = useAlert(); const { alert } = useAlert();
const formRef = useRef(null); const formRef = useRef<HTMLFormElement>(null);
const { boxShadow, onUpdate } = useScrollWithShadow({ const { boxShadow, onUpdate } = useScrollWithShadow({
ref: formRef, ref: formRef as MutableRefObject<HTMLFormElement>,
horizontal: false, horizontal: false,
dynamicOffsetFactor: 1, dynamicOffsetFactor: 1,
shadow: { shadow: {
@ -67,7 +67,7 @@ export function Share({ modal, params, ...props }: ModalProps) {
useEffect(() => { useEffect(() => {
setUrl(generateUrl({ setUrl(generateUrl({
appId: appId !== "" ? appId : null, appId: appId !== "" ? appId : undefined,
fullscreen, fullscreen,
standalone, standalone,
...options ...options
@ -75,7 +75,7 @@ export function Share({ modal, params, ...props }: ModalProps) {
}, [appId, fullscreen, standalone, options]); }, [appId, fullscreen, standalone, options]);
useEffect(() => { useEffect(() => {
onUpdate({ target: formRef.current as HTMLElement }); onUpdate({ target: formRef.current as unknown as HTMLElement });
}, [appId]); }, [appId]);
const onAppIdChange = (event: Event) => { const onAppIdChange = (event: Event) => {
@ -98,7 +98,7 @@ export function Share({ modal, params, ...props }: ModalProps) {
}; };
const setOption = (name: string, value: string) => { const setOption = (name: string, value: string) => {
setOptions((options = {}) => { setOptions((options: Record<string, string> = {}) => {
options = { ...options }; options = { ...options };
options[name] = value; options[name] = value;
return options; return options;
@ -174,7 +174,7 @@ export function Share({ modal, params, ...props }: ModalProps) {
<Button <Button
className={`${styles.Button} ${utilStyles.TextBold}`} className={`${styles.Button} ${utilStyles.TextBold}`}
onClick={() => { onClick={() => {
copyToClipboard(url, () => { copyToClipboard(url as string, () => {
alert({ alert({
title: "Share", title: "Share",
iconUrl: ModalsManager.getModalIconUrl("share"), iconUrl: ModalsManager.getModalIconUrl("share"),

View file

@ -1,7 +1,7 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { DefaultRoute } from "./routes/DefaultRoute"; import { DefaultRoute } from "./routes/DefaultRoute";
import AppsManager from "../../features/apps/appsManager"; import { AppsManager } from "../../features/apps/appsManager";
import App from "../../features/apps/app"; import { App } from "../../features/apps/app";
import { StandaloneRoute } from "./routes/StandaloneRoute"; import { StandaloneRoute } from "./routes/StandaloneRoute";
import { NoRoute } from "./routes/NoRoute"; import { NoRoute } from "./routes/NoRoute";

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import App from "../../../features/apps/app"; import { App } from "../../../features/apps/app";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import { generateUrl, getViewportParams, openUrl, setViewportIcon, setViewportTitle } from "../../../features/_utils/browser.utils"; import { generateUrl, getViewportParams, openUrl, setViewportIcon, setViewportTitle } from "../../../features/_utils/browser.utils";
import { NAME } from "../../../config/branding.config"; import { NAME } from "../../../config/branding.config";
import { StandaloneHeader } from "../../windows/StandaloneHeader"; import { StandaloneHeader } from "../../windows/StandaloneHeader";

View file

@ -1,16 +1,12 @@
import { CSSProperties, memo, ReactEventHandler, UIEventHandler, useEffect, useRef, useState } from "react"; import { CSSProperties, memo, MouseEvent, MutableRefObject, ReactEventHandler, UIEventHandler, useEffect, useRef, useState } from "react";
import styles from "./Taskbar.module.css"; import styles from "./Taskbar.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons"; import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
import AppsManager from "../../features/apps/appsManager"; import { AppsManager } from "../../features/apps/appsManager";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import { HomeMenu } from "./menus/HomeMenu"; import { HomeMenu } from "./menus/HomeMenu";
import OutsideClickListener from "../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../hooks/_utils/outsideClick";
import { Battery } from "./indicators/Battery";
import { Network } from "./indicators/Network";
import { Volume } from "./indicators/Volume";
import { SearchMenu } from "./menus/SearchMenu"; import { SearchMenu } from "./menus/SearchMenu";
import { Calendar } from "./indicators/Calendar";
import { useScrollWithShadow } from "../../hooks/_utils/scrollWithShadows"; import { useScrollWithShadow } from "../../hooks/_utils/scrollWithShadows";
import { AppButton } from "./app-icon/AppIcon"; import { AppButton } from "./app-icon/AppIcon";
import { useContextMenu } from "../../hooks/modals/contextMenu"; import { useContextMenu } from "../../hooks/modals/contextMenu";
@ -24,16 +20,17 @@ import { SettingsManager } from "../../features/settings/settingsManager";
import { useWindows } from "../../hooks/windows/windowsContext"; import { useWindows } from "../../hooks/windows/windowsContext";
import { ZIndexManager } from "../../features/z-index/zIndexManager"; import { ZIndexManager } from "../../features/z-index/zIndexManager";
import { useZIndex } from "../../hooks/z-index/zIndex"; import { useZIndex } from "../../hooks/z-index/zIndex";
import App from "../../features/apps/app"; import { App } from "../../features/apps/app";
import { Battery, Calendar, Network, Volume } from "./indicators";
export const Taskbar = memo(() => { export const Taskbar = memo(() => {
const ref = useRef(null); const ref = useRef<HTMLDivElement>(null);
const settingsManager = useSettingsManager(); const settingsManager = useSettingsManager();
const [showHome, setShowHome] = useState(false); const [showHome, setShowHome] = useState(false);
const [showSearch, setShowSearch] = useState(false); const [showSearch, setShowSearch] = useState(false);
const [hideUtilMenus, setHideUtilMenus] = useState(false); const [hideUtilMenus, setHideUtilMenus] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const { boxShadow, onUpdate } = useScrollWithShadow({ ref, shadow: { const { boxShadow, onUpdate } = useScrollWithShadow({ ref: ref as MutableRefObject<HTMLElement>, shadow: {
offset: 20, offset: 20,
blurRadius: 10, blurRadius: 10,
spreadRadius: -10, spreadRadius: -10,
@ -45,7 +42,7 @@ export const Taskbar = memo(() => {
const { onContextMenu } = useContextMenu({ Actions: (props) => const { onContextMenu } = useContextMenu({ Actions: (props) =>
<Actions avoidTaskbar={false} {...props}> <Actions avoidTaskbar={false} {...props}>
<ClickAction label={`Open ${APP_NAMES.SETTINGS}`} icon={faCog} onTrigger={() => { <ClickAction label={`Open ${APP_NAMES.SETTINGS}`} icon={faCog} onTrigger={() => {
windowsManager.open(APPS.SETTINGS); windowsManager?.open(APPS.SETTINGS);
}}/> }}/>
</Actions> </Actions>
}); });
@ -53,8 +50,8 @@ export const Taskbar = memo(() => {
const zIndex = useZIndex({ groupIndex: ZIndexManager.GROUPS.TASKBAR, index: 0 }); const zIndex = useZIndex({ groupIndex: ZIndexManager.GROUPS.TASKBAR, index: 0 });
useEffect(() => { useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings.get("pins", (pinList: string) => { void settings?.get("pins", (pinList: string) => {
const pins = pinList.split(","); const pins = pinList.split(",");
const newApps = AppsManager.APPS.sort((appA, appB) => { const newApps = AppsManager.APPS.sort((appA, appB) => {
@ -89,7 +86,6 @@ export const Taskbar = memo(() => {
const updateShowSearch = (show: boolean) => { const updateShowSearch = (show: boolean) => {
setShowSearch(show); setShowSearch(show);
if (show) { if (show) {
if (searchQuery !== "") { if (searchQuery !== "") {
setSearchQuery(""); setSearchQuery("");
@ -127,7 +123,7 @@ export const Taskbar = memo(() => {
data-allow-context-menu={true} data-allow-context-menu={true}
onContextMenu={(event) => { onContextMenu={(event) => {
if ((event.target as HTMLElement).getAttribute("data-allow-context-menu")) if ((event.target as HTMLElement).getAttribute("data-allow-context-menu"))
onContextMenu(event); onContextMenu(event as unknown as MouseEvent<HTMLElement, MouseEvent>);
}} }}
> >
<div className={styles.MenuIcons}> <div className={styles.MenuIcons}>
@ -159,7 +155,7 @@ export const Taskbar = memo(() => {
setActive={updateShowSearch} setActive={updateShowSearch}
searchQuery={searchQuery} searchQuery={searchQuery}
setSearchQuery={setSearchQuery} setSearchQuery={setSearchQuery}
inputRef={inputRef} inputRef={inputRef as unknown as MutableRefObject<HTMLInputElement>}
/> />
</OutsideClickListener> </OutsideClickListener>
</div> </div>
@ -173,7 +169,9 @@ export const Taskbar = memo(() => {
ref={ref} ref={ref}
> >
{apps.map((app) => { {apps.map((app) => {
const isActive = windows.map((window) => window.app.id).includes(app.id); if (windows == null) return;
const isActive = windows.map((window) => window.app?.id).includes(app.id);
const shouldBeShown = (app.isPinned || isActive); const shouldBeShown = (app.isPinned || isActive);
return (<AppButton return (<AppButton
windowsManager={windowsManager} windowsManager={windowsManager}
@ -190,7 +188,7 @@ export const Taskbar = memo(() => {
<Network showUtilMenu={showUtilMenu} hideUtilMenus={hideUtilMenus}/> <Network showUtilMenu={showUtilMenu} hideUtilMenus={hideUtilMenus}/>
<Volume showUtilMenu={showUtilMenu} hideUtilMenus={hideUtilMenus}/> <Volume showUtilMenu={showUtilMenu} hideUtilMenus={hideUtilMenus}/>
<Calendar showUtilMenu={showUtilMenu} hideUtilMenus={hideUtilMenus}/> <Calendar showUtilMenu={showUtilMenu} hideUtilMenus={hideUtilMenus}/>
<button title="Show Desktop" id="desktop-button" onClick={() => { windowsManager.minimizeAll(); }}/> <button title="Show Desktop" id="desktop-button" onClick={() => { windowsManager?.minimizeAll(); }}/>
</div> </div>
</div>; </div>;
}); });

View file

@ -1,20 +1,17 @@
import { FC, memo } from "react"; import { FC, memo, MouseEvent } from "react";
import App from "../../../features/apps/app"; import { App } from "../../../features/apps/app";
import styles from "./AppIcon.module.css"; import styles from "./AppIcon.module.css";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext"; import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext";
import { useContextMenu } from "../../../hooks/modals/contextMenu"; import { useContextMenu } from "../../../hooks/modals/contextMenu";
import { Actions } from "../../actions/Actions"; import { Actions } from "../../actions/Actions";
import { ClickAction } from "../../actions/actions/ClickAction"; import { ClickAction } from "../../actions/actions/ClickAction";
import { faThumbTack, faTimes } from "@fortawesome/free-solid-svg-icons"; import { AppsManager } from "../../../features/apps/appsManager";
import { SettingsManager } from "../../../features/settings/settingsManager"; import { WindowsManager } from "../../../features/windows/windowsManager";
import { removeFromArray } from "../../../features/_utils/array.utils";
import AppsManager from "../../../features/apps/appsManager";
import WindowsManager from "../../../features/windows/windowsManager";
interface AppButtonProps { interface AppButtonProps {
app: App; app: App;
windowsManager: WindowsManager; windowsManager?: WindowsManager;
active: boolean; active: boolean;
visible: boolean; visible: boolean;
} }
@ -24,7 +21,7 @@ export const AppButton: FC<AppButtonProps> = memo(({ app, windowsManager, active
const { onContextMenu } = useContextMenu({ Actions: (props) => const { onContextMenu } = useContextMenu({ Actions: (props) =>
<Actions avoidTaskbar={false} {...props}> <Actions avoidTaskbar={false} {...props}>
<ClickAction label={app.name} icon={AppsManager.getAppIconUrl(app.id)} onTrigger={() => { <ClickAction label={app.name} icon={AppsManager.getAppIconUrl(app.id)} onTrigger={() => {
windowsManager.open(app.id); windowsManager?.open(app.id);
}}/> }}/>
{/* <ClickAction label={isPinned ? "Unpin from taskbar" : "Pin to taskbar"} icon={faThumbTack} onTrigger={() => { {/* <ClickAction label={isPinned ? "Unpin from taskbar" : "Pin to taskbar"} icon={faThumbTack} onTrigger={() => {
const newPins = [...pins]; const newPins = [...pins];
@ -60,7 +57,7 @@ export const AppButton: FC<AppButtonProps> = memo(({ app, windowsManager, active
onClick={() => { onClick={() => {
const windowId = windowsManager.getAppWindowId(app.id); const windowId = windowsManager.getAppWindowId(app.id);
if (!active) { if (!active || windowId == null) {
windowsManager.open(app.id); windowsManager.open(app.id);
} else if (!windowsManager.isFocused(windowId)) { } else if (!windowsManager.isFocused(windowId)) {
windowsManager.focus(windowId); windowsManager.focus(windowId);
@ -70,7 +67,7 @@ export const AppButton: FC<AppButtonProps> = memo(({ app, windowsManager, active
}} }}
onContextMenu={(event) => { onContextMenu={(event) => {
if (visible) if (visible)
onContextMenu(event); onContextMenu(event as unknown as MouseEvent<HTMLElement, MouseEvent>);
}} }}
title={app.name} title={app.name}
> >

View file

@ -3,7 +3,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import styles from "./Battery.module.css"; import styles from "./Battery.module.css";
import { UtilMenu } from "../menus/UtilMenu"; import { UtilMenu } from "../menus/UtilMenu";
import OutsideClickListener from "../../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../../hooks/_utils/outsideClick";
type Battery = {
charging: boolean | ((prevState: boolean) => boolean);
level: number;
addEventListener: (arg0: string, arg1: {
(): void;
(): void;
}) => void;
removeEventListener: (arg0: string, arg1: {
(): void;
(): void;
}) => void;
};
interface BatteryProps { interface BatteryProps {
hideUtilMenus: boolean; hideUtilMenus: boolean;
@ -18,7 +31,8 @@ export function Battery({ hideUtilMenus, showUtilMenu }: BatteryProps) {
// const [dischargingTime, setDischargingTime] = useState(0); // const [dischargingTime, setDischargingTime] = useState(0);
useEffect(() => { useEffect(() => {
(navigator as any).getBattery?.()?.then((battery) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(navigator as any).getBattery?.()?.then((battery: Battery) => {
const updateIsCharging = () => { const updateIsCharging = () => {
setIsCharging(battery.charging); setIsCharging(battery.charging);
}; };
@ -60,7 +74,7 @@ export function Battery({ hideUtilMenus, showUtilMenu }: BatteryProps) {
} }
}, [hideUtilMenus, showMenu]); }, [hideUtilMenus, showMenu]);
const updateShowMenu = (show) => { const updateShowMenu = (show: boolean) => {
if (show) if (show)
showUtilMenu(); showUtilMenu();

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import styles from "./Calendar.module.css"; import styles from "./Calendar.module.css";
import OutsideClickListener from "../../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../../hooks/_utils/outsideClick";
import { UtilMenu } from "../menus/UtilMenu"; import { UtilMenu } from "../menus/UtilMenu";
interface CalendarProps { interface CalendarProps {

View file

@ -1,7 +1,7 @@
import { faWifi } from "@fortawesome/free-solid-svg-icons"; import { faWifi } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import OutsideClickListener from "../../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../../hooks/_utils/outsideClick";
import { UtilMenu } from "../menus/UtilMenu"; import { UtilMenu } from "../menus/UtilMenu";
import styles from "./Network.module.css"; import styles from "./Network.module.css";

View file

@ -1,7 +1,7 @@
import { faVolumeHigh } from "@fortawesome/free-solid-svg-icons"; import { faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import OutsideClickListener from "../../../hooks/_utils/outsideClick"; import { OutsideClickListener } from "../../../hooks/_utils/outsideClick";
import { UtilMenu } from "../menus/UtilMenu"; import { UtilMenu } from "../menus/UtilMenu";
import styles from "./Volume.module.css"; import styles from "./Volume.module.css";

View file

@ -0,0 +1,4 @@
export { Battery } from "./Battery";
export { Calendar } from "./Calendar";
export { Network } from "./Network";
export { Volume } from "./Volume";

View file

@ -4,13 +4,13 @@ import appStyles from "./AppList.module.css";
import taskbarStyles from "../Taskbar.module.css"; import taskbarStyles from "../Taskbar.module.css";
import { faCircleInfo, faFileLines, faGear, faImage, faPowerOff } from "@fortawesome/free-solid-svg-icons"; import { faCircleInfo, faFileLines, faGear, faImage, faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext"; import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import { closeViewport } from "../../../features/_utils/browser.utils"; import { closeViewport } from "../../../features/_utils/browser.utils";
import { useKeyboardListener } from "../../../hooks/_utils/keyboard"; import { useKeyboardListener } from "../../../hooks/_utils/keyboard";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext"; import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Vector2 from "../../../features/math/vector2"; import { Vector2 } from "../../../features/math/vector2";
import utilStyles from "../../../styles/utils.module.css"; import utilStyles from "../../../styles/utils.module.css";
import { APPS } from "../../../config/apps.config"; import { APPS } from "../../../config/apps.config";
import { NAME } from "../../../config/branding.config"; import { NAME } from "../../../config/branding.config";
@ -69,15 +69,15 @@ export function HomeMenu({ active, setActive, search }: HomeMenuProps) {
</button> </button>
<button title="Settings" tabIndex={tabIndex} onClick={() => { <button title="Settings" tabIndex={tabIndex} onClick={() => {
setActive(false); setActive(false);
windowsManager.open("settings"); windowsManager?.open("settings");
}}> }}>
<FontAwesomeIcon icon={faGear}/> <FontAwesomeIcon icon={faGear}/>
</button> </button>
<button title="Info" tabIndex={tabIndex} onClick={() => { <button title="Info" tabIndex={tabIndex} onClick={() => {
setActive(false); setActive(false);
windowsManager.open("text-editor", { windowsManager?.open("text-editor", {
mode: "view", mode: "view",
file: virtualRoot.navigate("~/Documents/Info.md"), file: virtualRoot?.navigate("~/Documents/Info.md"),
size: new Vector2(575, 675), size: new Vector2(575, 675),
}); });
}}> }}>
@ -85,13 +85,13 @@ export function HomeMenu({ active, setActive, search }: HomeMenuProps) {
</button> </button>
<button title="Images" tabIndex={tabIndex} onClick={() => { <button title="Images" tabIndex={tabIndex} onClick={() => {
setActive(false); setActive(false);
windowsManager.open(APPS.FILE_EXPLORER, { path: "~/Pictures" }); windowsManager?.open(APPS.FILE_EXPLORER, { path: "~/Pictures" });
}}> }}>
<FontAwesomeIcon icon={faImage}/> <FontAwesomeIcon icon={faImage}/>
</button> </button>
<button title="Documents" tabIndex={tabIndex} onClick={() => { <button title="Documents" tabIndex={tabIndex} onClick={() => {
setActive(false); setActive(false);
windowsManager.open(APPS.FILE_EXPLORER, { path: "~/Documents" }); } windowsManager?.open(APPS.FILE_EXPLORER, { path: "~/Documents" }); }
}> }>
<FontAwesomeIcon icon={faFileLines}/> <FontAwesomeIcon icon={faFileLines}/>
</button> </button>
@ -106,7 +106,7 @@ export function HomeMenu({ active, setActive, search }: HomeMenuProps) {
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={() => { onClick={() => {
setActive(false); setActive(false);
windowsManager.open(id); windowsManager?.open(id);
}} }}
title={name} title={name}
> >

View file

@ -1,10 +1,10 @@
import styles from "./SearchMenu.module.css"; import styles from "./SearchMenu.module.css";
import appStyles from "./AppList.module.css"; import appStyles from "./AppList.module.css";
import AppsManager from "../../../features/apps/appsManager"; import { AppsManager } from "../../../features/apps/appsManager";
import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext"; import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext";
import { ChangeEventHandler, useEffect, useState } from "react"; import { ChangeEventHandler, MutableRefObject, useEffect, useState } from "react";
import { useKeyboardListener } from "../../../hooks/_utils/keyboard"; import { useKeyboardListener } from "../../../hooks/_utils/keyboard";
import App from "../../../features/apps/app"; import { App } from "../../../features/apps/app";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
interface SearchMenuProps { interface SearchMenuProps {
@ -12,12 +12,12 @@ interface SearchMenuProps {
setActive: Function; setActive: Function;
searchQuery: string; searchQuery: string;
setSearchQuery: Function; setSearchQuery: Function;
inputRef: { current: HTMLInputElement | undefined }; inputRef: MutableRefObject<HTMLInputElement>;
} }
export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inputRef }: SearchMenuProps) { export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inputRef }: SearchMenuProps) {
const windowsManager = useWindowsManager(); const windowsManager = useWindowsManager();
const [apps, setApps] = useState<App[]>(null); const [apps, setApps] = useState<App[] | null>(null);
const [tabIndex, setTabIndex] = useState(active ? 0 : -1); const [tabIndex, setTabIndex] = useState(active ? 0 : -1);
useEffect(() => { useEffect(() => {
@ -25,7 +25,7 @@ export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inp
}, [active]); }, [active]);
useEffect(() => { useEffect(() => {
if (inputRef.current) { if (inputRef.current != null) {
inputRef.current.focus(); inputRef.current.focus();
window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, document.body.scrollHeight);
} }
@ -58,7 +58,8 @@ export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inp
setActive(false); setActive(false);
} else if (event.key === "Enter" && active) { } else if (event.key === "Enter" && active) {
event.preventDefault(); event.preventDefault();
windowsManager.open(apps[0].id); if (apps == null) return;
windowsManager?.open(apps[0].id);
setActive(false); setActive(false);
} }
}; };
@ -75,7 +76,7 @@ export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inp
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={() => { onClick={() => {
setActive(false); setActive(false);
windowsManager.open(id); windowsManager?.open(id);
}} }}
> >
<ReactSVG src={AppsManager.getAppIconUrl(id)}/> <ReactSVG src={AppsManager.getAppIconUrl(id)}/>

View file

@ -1,8 +1,8 @@
import { ReactElement, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useAlert } from "../../hooks/modals/alert"; import { useAlert } from "../../hooks/modals/alert";
import AppsManager from "../../features/apps/appsManager"; import { AppsManager } from "../../features/apps/appsManager";
import Vector2 from "../../features/math/vector2"; import { Vector2 } from "../../features/math/vector2";
import App from "../../features/apps/app"; import { App } from "../../features/apps/app";
export interface WindowFallbackViewProps { export interface WindowFallbackViewProps {
error?: Error; error?: Error;
@ -12,7 +12,7 @@ export interface WindowFallbackViewProps {
} }
// I don't know why this component's type needs to be ReactNode instead of FC, it has something to do with the way it's implemented // I don't know why this component's type needs to be ReactNode instead of FC, it has something to do with the way it's implemented
export default function WindowFallbackView({ error, resetErrorBoundary: _resetErrorBoundary, app, closeWindow }: WindowFallbackViewProps): ReactElement { export function WindowFallbackView({ error, resetErrorBoundary: _resetErrorBoundary, app, closeWindow }: WindowFallbackViewProps): undefined {
const { alert } = useAlert(); const { alert } = useAlert();
const [alerted, setAlerted] = useState(false); const [alerted, setAlerted] = useState(false);
@ -21,15 +21,17 @@ export default function WindowFallbackView({ error, resetErrorBoundary: _resetEr
return; return;
setAlerted(true); setAlerted(true);
closeWindow(); closeWindow?.();
alert({
title: `${app.name} has stopped working`, if (app != null && error != null)
text: `${error.name}: ${error.message}`, alert({
iconUrl: AppsManager.getAppIconUrl(app.id), title: `${app.name} has stopped working`,
size: new Vector2(350, 150), text: `${error.name}: ${error.message}`,
single: true iconUrl: AppsManager.getAppIconUrl(app.id),
}); size: new Vector2(350, 150),
}, [alerted, alert, app.id, app.name, error.message, closeWindow, error.name]); single: true
});
}, [alerted, alert, app?.id, app?.name, error?.message, closeWindow, error?.name]);
return; return;
} }

View file

@ -5,11 +5,11 @@ import { ReactSVG } from "react-svg";
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext";
import Draggable from "react-draggable"; import Draggable from "react-draggable";
import { CSSProperties, FC, memo, MouseEventHandler, useEffect, useRef, useState } from "react"; import { CSSProperties, FC, memo, MouseEventHandler, useEffect, useRef, useState } from "react";
import Vector2 from "../../features/math/vector2"; import { Vector2 } from "../../features/math/vector2";
import { faWindowMaximize } from "@fortawesome/free-regular-svg-icons"; import { faWindowMaximize } from "@fortawesome/free-regular-svg-icons";
import utilStyles from "../../styles/utils.module.css"; import utilStyles from "../../styles/utils.module.css";
import { useContextMenu } from "../../hooks/modals/contextMenu"; import { useContextMenu } from "../../hooks/modals/contextMenu";
import AppsManager from "../../features/apps/appsManager"; import { AppsManager } from "../../features/apps/appsManager";
import { ClickAction } from "../actions/actions/ClickAction"; import { ClickAction } from "../actions/actions/ClickAction";
import { Actions } from "../actions/Actions"; import { Actions } from "../actions/Actions";
import { useScreenDimensions } from "../../hooks/_utils/screen"; import { useScreenDimensions } from "../../hooks/_utils/screen";
@ -19,12 +19,13 @@ import { ZIndexManager } from "../../features/z-index/zIndexManager";
import { useZIndex } from "../../hooks/z-index/zIndex"; import { useZIndex } from "../../hooks/z-index/zIndex";
import { useWindowedModal } from "../../hooks/modals/windowedModal"; import { useWindowedModal } from "../../hooks/modals/windowedModal";
import { Divider } from "../actions/actions/Divider"; import { Divider } from "../actions/actions/Divider";
import ModalsManager from "../../features/modals/modalsManager"; import { ModalsManager } from "../../features/modals/modalsManager";
import { Share } from "../modals/share/Share"; import { Share } from "../modals/share/Share";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import WindowFallbackView from "./WindowFallbackView"; import { WindowFallbackView } from "./WindowFallbackView";
import { WindowOptions } from "../../features/windows/windowsManager"; import { WindowOptions } from "../../features/windows/windowsManager";
import { MIN_SCREEN_HEIGHT, MIN_SCREEN_WIDTH } from "../../config/windows.config"; import { MIN_SCREEN_HEIGHT, MIN_SCREEN_WIDTH } from "../../config/windows.config";
import { ModalProps } from "../modals/ModalView";
export interface WindowProps extends WindowOptions { export interface WindowProps extends WindowOptions {
fullscreen?: boolean; fullscreen?: boolean;
@ -48,58 +49,57 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
const [startPosition, setStartPosition] = useState(position); const [startPosition, setStartPosition] = useState(position);
const [maximized, setMaximized] = useState(fullscreen ?? false); const [maximized, setMaximized] = useState(fullscreen ?? false);
const [screenWidth, screenHeight] = useScreenDimensions(); const [screenWidth, screenHeight] = useScreenDimensions();
const [title, setTitle] = useState(app.name); const [title, setTitle] = useState(app?.name ?? "");
const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id)); const [iconUrl, setIconUrl] = useState(app ? AppsManager.getAppIconUrl(app?.id) : "");
const zIndex = useZIndex({ groupIndex: ZIndexManager.GROUPS.WINDOWS, index }); const zIndex = useZIndex({ groupIndex: ZIndexManager.GROUPS.WINDOWS, index: index ?? 0 });
const { onContextMenu, ShortcutsListener } = useContextMenu({ Actions: (props) => const { onContextMenu, ShortcutsListener } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<ClickAction label="Minimize" icon={faMinus} onTrigger={() => { toggleMinimized(); }}/> <ClickAction label="Minimize" icon={faMinus} onTrigger={() => { toggleMinimized?.(); }}/>
<ClickAction label="Maximize" icon={faExpand} shortcut={["F11"]} onTrigger={() => { <ClickAction label="Maximize" icon={faExpand} shortcut={["F11"]} onTrigger={() => {
setMaximized(!maximized); setMaximized(!maximized);
}}/> }}/>
<ClickAction label="Close" icon={faTimes} shortcut={["Control", "q"]} onTrigger={() => { <ClickAction label="Close" icon={faTimes} shortcut={["Control", "q"]} onTrigger={() => {
close(); close?.();
}}/> }}/>
<Divider/> <Divider/>
<ClickAction label="Standalone mode" icon={faCircleRight} onTrigger={() => { <ClickAction label="Standalone mode" icon={faCircleRight} onTrigger={() => {
openUrl(generateUrl({ appId: app.id, standalone: true }), "_self"); if (app != null) openUrl(generateUrl({ appId: app.id, standalone: true }), "_self");
}}/> }}/>
<ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} shortcut={["Alt", "s"]} onTrigger={() => { <ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} shortcut={["Alt", "s"]} onTrigger={() => {
openWindowedModal({ if (app != null)
appId: app.id, openWindowedModal({
fullscreen: maximized, appId: app.id,
size: new Vector2(350, 350), fullscreen: maximized,
Modal: (props) => <Share {...props}/> size: new Vector2(350, 350),
}); Modal: (props: ModalProps) => <Share {...props}/>
});
}}/> }}/>
</Actions> </Actions>
}); });
useEffect(() => { useEffect(() => {
if (screenWidth == null || screenHeight == null) if (screenWidth == null || screenHeight == null) return;
return;
if (screenWidth < MIN_SCREEN_WIDTH || screenHeight < MIN_SCREEN_HEIGHT) { if (screenWidth < MIN_SCREEN_WIDTH || screenHeight < MIN_SCREEN_HEIGHT) {
setMaximized(true); setMaximized(true);
} else { } else if (position != null) {
if (position.x > screenWidth) if (position.x > screenWidth) position.x = 0;
position.x = 0; if (position.y > screenHeight) position.y = 0;
if (position.y > screenHeight)
position.y = 0;
setStartPosition(position); setStartPosition(position);
} else {
setStartPosition(new Vector2(0, 0));
} }
}, [position, size, screenHeight, screenWidth]); }, [position, size, screenHeight, screenWidth]);
useEffect(() => { useEffect(() => {
const setViewportTitleAndIcon = () => { const setViewportTitleAndIcon = () => {
setViewportTitle(`${title} | ${NAME}`); if (title != null) setViewportTitle(`${title} | ${NAME}`);
setViewportIcon(iconUrl); if (iconUrl != null) setViewportIcon(iconUrl);
}; };
if (active && !minimized) if (active && !minimized) setViewportTitleAndIcon();
setViewportTitleAndIcon();
window.addEventListener("focus", setViewportTitleAndIcon); window.addEventListener("focus", setViewportTitleAndIcon);
@ -108,14 +108,17 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
}; };
}, [active, minimized, iconUrl, title]); }, [active, minimized, iconUrl, title]);
if (app == null)
return;
const close: WindowProps["close"] = (event) => { const close: WindowProps["close"] = (event) => {
event?.preventDefault(); event?.preventDefault();
windowsManager.close(id); if (id != null) windowsManager?.close(id);
}; };
const focus: WindowProps["focus"] = (event, force = false) => { const focus: WindowProps["focus"] = (event, force = false) => {
if (force) { if (force) {
onInteract(); onInteract?.();
return; return;
} }
@ -124,7 +127,7 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
const target = event?.target as HTMLElement; const target = event?.target as HTMLElement;
if (event == null || target?.closest?.(".Handle") == null || target?.closest?.("button") == null) if (event == null || target?.closest?.(".Handle") == null || target?.closest?.("button") == null)
onInteract(); onInteract?.();
}; };
const classNames = [styles["Window-container"]]; const classNames = [styles["Window-container"]];
@ -139,14 +142,14 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
key={id} key={id}
axis="both" axis="both"
handle={".Window-handle"} handle={".Window-handle"}
defaultPosition={startPosition.round()} defaultPosition={startPosition?.round()}
position={null} position={undefined}
scale={1} scale={1}
bounds={{ bounds={{
top: 0, top: 0,
bottom: screenHeight - 55, bottom: (screenHeight ?? 0) - 55,
left: -size.x + 85, left: size ? -size.x + 85 : 85,
right: screenWidth - 5 right: (screenWidth ?? 0) - 5
}} }}
cancel="button" cancel="button"
nodeRef={nodeRef} nodeRef={nodeRef}
@ -162,14 +165,18 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
<div <div
className={styles["Window-inner"]} className={styles["Window-inner"]}
style={{ style={{
width: maximized ? null : size.x, width: (maximized || size == null) ? undefined : size.x,
height: maximized ? null : size.y, height: (maximized || size == null) ? undefined : size.y,
}} }}
> >
<div className={`${styles.Header} Window-handle`} onContextMenu={onContextMenu} onDoubleClick={(event) => { <div
setMaximized(!maximized); className={`${styles.Header} Window-handle`}
focus(event as unknown as Event, true); onContextMenu={onContextMenu as unknown as MouseEventHandler}
}}> onDoubleClick={(event) => {
setMaximized(!maximized);
focus(event as unknown as Event, true);
}}
>
<ReactSVG <ReactSVG
className={styles["Window-icon"]} className={styles["Window-icon"]}
src={iconUrl} src={iconUrl}
@ -180,7 +187,7 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
> >
<FontAwesomeIcon icon={faMinus}/> <FontAwesomeIcon icon={faMinus}/>
</button> </button>
{screenWidth > MIN_SCREEN_WIDTH && screenHeight > MIN_SCREEN_HEIGHT {screenWidth != null && screenHeight != null && screenWidth > MIN_SCREEN_WIDTH && screenHeight > MIN_SCREEN_HEIGHT
? <button aria-label="Maximize" className={styles["Header-button"]} tabIndex={0} id="maximize-window" ? <button aria-label="Maximize" className={styles["Header-button"]} tabIndex={0} id="maximize-window"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();

View file

@ -17,9 +17,10 @@ export const WindowsView: FC = memo(() => {
// Sort windows // Sort windows
useEffect(() => { useEffect(() => {
setSortedWindows([...windows].sort((windowA: WindowOptions, windowB: WindowOptions) => if (windows != null)
windowA.lastInteraction - windowB.lastInteraction setSortedWindows([...windows].sort((windowA: WindowOptions, windowB: WindowOptions) =>
)); (windowA.lastInteraction ?? 0) - (windowB.lastInteraction ?? 0)
));
}, [windows]); }, [windows]);
useEffect(() => { useEffect(() => {
@ -40,7 +41,7 @@ export const WindowsView: FC = memo(() => {
// Launch startup apps // Launch startup apps
useEffect(() => { useEffect(() => {
if (windowsManager.startupComplete) if (windowsManager?.startupComplete)
return; return;
let startupAppNames: string[] = []; let startupAppNames: string[] = [];
@ -53,24 +54,24 @@ export const WindowsView: FC = memo(() => {
delete params.app; delete params.app;
// Get list of app names from settings file // Get list of app names from settings file
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.apps); const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.apps);
void settings.get("startup", (value) => { void settings?.get("startup", (value) => {
if (value !== "") { if (value !== "") {
startupAppNames = value?.split(",").concat(startupAppNames); startupAppNames = value?.split(",").concat(startupAppNames);
startupAppNames = removeDuplicatesFromArray(startupAppNames); startupAppNames = removeDuplicatesFromArray(startupAppNames);
} }
windowsManager.startup(startupAppNames, params); windowsManager?.startup(startupAppNames, params);
}); });
}, [settingsManager, windowsManager]); }, [settingsManager, windowsManager]);
return (<div> return (<div>
{windows.map((window: WindowProps) => { {windows?.map((window: WindowProps) => {
const { id, app, size, position, options, minimized, fullscreen } = window; const { id, app, size, position, options, minimized, fullscreen } = window;
const index = sortedWindows.indexOf(window); const index = sortedWindows.indexOf(window);
return <WindowView return <WindowView
key={id} key={id}
onInteract={() => { windowsManager.focus(id); }} onInteract={() => { windowsManager?.focus(id as string); }}
active={index === sortedWindows.length - 1} active={index === sortedWindows.length - 1}
id={id} id={id}
app={app} app={app}
@ -82,7 +83,7 @@ export const WindowsView: FC = memo(() => {
toggleMinimized={(event: Event) => { toggleMinimized={(event: Event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
windowsManager.setMinimized(id, !minimized); windowsManager?.setMinimized(id as string, !minimized);
}} }}
fullscreen={fullscreen} fullscreen={fullscreen}
/>; />;

View file

@ -1,7 +0,0 @@
import styles from "../components/actions/Actions.module.css";
export const STYLES = {
CONTEXT_MENU: styles.ContextMenu,
SHORTCUTS_LISTENER: styles.ShortcutsListener,
HEADER_MENU: styles.HeaderMenu
};

View file

@ -11,7 +11,9 @@ export const APPS = {
LOGIC_SIM: "logic-sim", LOGIC_SIM: "logic-sim",
}; };
export const APP_NAMES = { export type AppKey = keyof typeof APPS;
export const APP_NAMES: Record<AppKey, string> = {
TERMINAL: "Commands", TERMINAL: "Commands",
SETTINGS: "Settings", SETTINGS: "Settings",
MEDIA_VIEWER: "Photos", MEDIA_VIEWER: "Photos",
@ -22,14 +24,15 @@ export const APP_NAMES = {
LOGIC_SIM: "Logic Sim (WIP)" LOGIC_SIM: "Logic Sim (WIP)"
}; };
export const APP_DESCRIPTIONS = { export const APP_DESCRIPTIONS: Record<AppKey, string | null> = {
TERMINAL: "A command line tool inspired by the Unix shell that runs entirely in your browser and uses a virtual file system.", TERMINAL: "A command line tool inspired by the Unix shell that runs entirely in your browser and uses a virtual file system.",
SETTINGS: `Configure ${NAME}'s settings and customize your experience.`, SETTINGS: `Configure ${NAME}'s settings and customize your experience.`,
TEXT_EDITOR: "Simple text editor for reading and writing text documents.", TEXT_EDITOR: "Simple text editor for reading and writing text documents.",
FILE_EXPLORER: "Browse and manage your virtual files.", FILE_EXPLORER: "Browse and manage your virtual files.",
CALCULATOR: "Simple calculator app.", CALCULATOR: "Simple calculator app.",
BROWSER: "Browse the internet.", BROWSER: "Browse the internet.",
LOGIC_SIM: "Create digital logic circuits using the online simulator." LOGIC_SIM: "Create digital logic circuits using the online simulator.",
MEDIA_VIEWER: null,
}; };
export const APP_ICONS = { export const APP_ICONS = {

View file

@ -1,7 +1,7 @@
import Vector2 from "../features/math/vector2"; import { Vector2 } from "../features/math/vector2";
export const DIALOG_CONTENT_TYPES = { export const DIALOG_CONTENT_TYPES = {
CloseButton: 0 closeButton: 0
}; };
export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200); export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200);

View file

@ -1,4 +1,4 @@
export const THEMES = { export const THEMES: Record<number, string | null> = {
0: "Dark", 0: "Dark",
1: "Light", 1: "Light",
2: "Cherry", 2: "Cherry",

View file

@ -37,8 +37,8 @@ export function setViewportTitle(title: string) {
} }
export function setViewportIcon(url: string) { export function setViewportIcon(url: string) {
let link: HTMLLinkElement = document.querySelector("link[rel~='icon']"); let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']");
if (!link) { if (link == null) {
link = document.createElement("link"); link = document.createElement("link");
link.rel = "icon"; link.rel = "icon";
document.head.appendChild(link); document.head.appendChild(link);
@ -49,7 +49,7 @@ export function setViewportIcon(url: string) {
export function getViewportParams(): Record<string, string> { export function getViewportParams(): Record<string, string> {
const query = window.location.search.slice(1); const query = window.location.search.slice(1);
const params = {}; const params: Record<string, string> = {};
query.split("&").forEach((param) => { query.split("&").forEach((param) => {
// For some reason, URI components only decode when decoded twice // For some reason, URI components only decode when decoded twice
// TO DO: Please find a fix, or create a custom function // TO DO: Please find a fix, or create a custom function
@ -61,7 +61,7 @@ export function getViewportParams(): Record<string, string> {
} }
interface generateUrlOptions { interface generateUrlOptions {
appId: string; appId?: string;
fullscreen?: boolean; fullscreen?: boolean;
standalone?: boolean; standalone?: boolean;
} }
@ -107,5 +107,5 @@ export function removeUrlProtocol(url: string) {
} }
export function copyToClipboard(string: string, onSuccess?: (value: void) => void, onFail?: (value: void) => void) { export function copyToClipboard(string: string, onSuccess?: (value: void) => void, onFail?: (value: void) => void) {
navigator.clipboard.writeText(string).then(onSuccess, onFail); void navigator.clipboard.writeText(string).then(onSuccess, onFail);
} }

View file

@ -1,15 +0,0 @@
export function classNames(...args: string[]) {
let className = "";
for (let i = 0; i < args.length; i++) {
const value = args[i];
if (typeof value === "string") {
className += value + " ";
} else if (Array.isArray(value)) {
className += classNames(...value as string[]) + " ";
}
}
return className.trim();
}

View file

@ -0,0 +1,18 @@
export function clamp(value: number, min: number, max: number): number {
if (value < min) {
return min;
} else if (value > max) {
return max;
} else {
return value;
}
}
export function randomRange(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
export function round(value: number, precision: number): number {
const multiplier = Math.pow(10, precision || 0);
return Math.round(value * multiplier) / multiplier;
}

View file

@ -1,6 +1,6 @@
export function formatShortcut(shortcut: string[]): string { export function formatShortcut(shortcut: string[]): string {
const specialKeys = []; const specialKeys: string[] = [];
const singleKeys = []; const singleKeys: string[] = [];
shortcut.forEach((key) => { shortcut.forEach((key) => {
if (key.length > 1) { if (key.length > 1) {

View file

@ -0,0 +1,7 @@
export class ActionsManager {
static MODES = {
contextMenu: "ContextMenu",
shortcutsListener: "ShortcutsListener",
headerMenu: "HeaderMenu",
};
}

View file

@ -1,13 +1,14 @@
import Vector2 from "../math/vector2"; import { Vector2 } from "../math/vector2";
import { WindowProps } from "../../components/windows/WindowView"; import { WindowProps } from "../../components/windows/WindowView";
import { FC } from "react"; import { FC } from "react";
export default class App { export class App {
name: string; name: string = "App";
id: string; id: string = "app";
windowContent: React.FC; windowContent?: FC<JSX.IntrinsicAttributes & WindowProps>;
windowOptions?: { windowOptions?: {
size: Vector2 size: Vector2;
[key: string]: unknown;
}; };
isActive: boolean = false; isActive: boolean = false;
isPinned?: boolean; isPinned?: boolean;
@ -15,11 +16,11 @@ export default class App {
/** /**
* @param windowOptions - Default window options * @param windowOptions - Default window options
*/ */
constructor(name: string, id: string, windowContent: FC, windowOptions?: object | null) { constructor(name: App["name"], id: App["id"], windowContent: App["windowContent"], windowOptions?: App["windowOptions"]) {
Object.assign(this, { name, id, windowContent, windowOptions }); Object.assign(this, { name, id, windowContent, windowOptions });
if (this.windowContent == null) if (this.windowContent == null)
console.warn(`App (${this.id}) is missing the windowContent property.`); console.warn(`${this.name} (${this.id}) is missing the windowContent property.`);
} }
WindowContent = (props: JSX.IntrinsicAttributes & WindowProps) => { WindowContent = (props: JSX.IntrinsicAttributes & WindowProps) => {

View file

@ -1,4 +1,4 @@
import App from "./app"; import { App } from "./app";
import { FileExplorer } from "../../components/apps/file-explorer/FileExplorer"; import { FileExplorer } from "../../components/apps/file-explorer/FileExplorer";
import { MediaViewer } from "../../components/apps/media-viewer/MediaViewer"; import { MediaViewer } from "../../components/apps/media-viewer/MediaViewer";
import { WebView } from "../../components/apps/_utils/web-view/WebView"; import { WebView } from "../../components/apps/_utils/web-view/WebView";
@ -6,21 +6,21 @@ import { Terminal } from "../../components/apps/terminal/Terminal";
import { TextEditor } from "../../components/apps/text-editor/TextEditor"; import { TextEditor } from "../../components/apps/text-editor/TextEditor";
import { Settings } from "../../components/apps/settings/Settings"; import { Settings } from "../../components/apps/settings/Settings";
import { Calculator } from "../../components/apps/calculator/Calculator"; import { Calculator } from "../../components/apps/calculator/Calculator";
import Vector2 from "../math/vector2"; import { Vector2 } from "../math/vector2";
import { APPS, APP_NAMES } from "../../config/apps.config"; import { APPS, APP_NAMES } from "../../config/apps.config";
import { Browser } from "../../components/apps/browser/Browser"; import { Browser } from "../../components/apps/browser/Browser";
import { IMAGE_FORMATS } from "../../config/apps/mediaViewer.config"; import { IMAGE_FORMATS } from "../../config/apps/mediaViewer.config";
import { LogicSim } from "../../components/apps/logic-sim/LogicSim"; import { LogicSim } from "../../components/apps/logic-sim/LogicSim";
export default class AppsManager { export class AppsManager {
static APPS: App[] = [ static APPS: App[] = [
new App(APP_NAMES.TERMINAL, APPS.TERMINAL, Terminal), new App(APP_NAMES.TERMINAL, APPS.TERMINAL, Terminal as App["windowContent"]),
new App(APP_NAMES.SETTINGS, APPS.SETTINGS, Settings), new App(APP_NAMES.SETTINGS, APPS.SETTINGS, Settings),
new App(APP_NAMES.MEDIA_VIEWER, APPS.MEDIA_VIEWER, MediaViewer), new App(APP_NAMES.MEDIA_VIEWER, APPS.MEDIA_VIEWER, MediaViewer),
new App(APP_NAMES.CALCULATOR, APPS.CALCULATOR, Calculator, { size: new Vector2(400, 600) }), new App(APP_NAMES.CALCULATOR, APPS.CALCULATOR, Calculator, { size: new Vector2(400, 600) }),
new App(APP_NAMES.TEXT_EDITOR, APPS.TEXT_EDITOR, TextEditor), new App(APP_NAMES.TEXT_EDITOR, APPS.TEXT_EDITOR, TextEditor),
// new App("Code Editor", "code-editor"), // new App("Code Editor", "code-editor"),
new App(APP_NAMES.FILE_EXPLORER, APPS.FILE_EXPLORER, FileExplorer), new App(APP_NAMES.FILE_EXPLORER, APPS.FILE_EXPLORER, FileExplorer as App["windowContent"]),
new App("Wordle", "wordle", WebView, { new App("Wordle", "wordle", WebView, {
source: "https://prozilla.dev/wordle", source: "https://prozilla.dev/wordle",
size: new Vector2(400, 650) size: new Vector2(400, 650)
@ -55,8 +55,8 @@ export default class AppsManager {
/** /**
* Get the app associated with a file extension * Get the app associated with a file extension
*/ */
static getAppByFileExtension(fileExtension: string): App { static getAppByFileExtension(fileExtension: string): App | null {
let app: App = null; let app: App | null = null;
if (IMAGE_FORMATS.includes(fileExtension)) if (IMAGE_FORMATS.includes(fileExtension))
return this.getAppById(APPS.MEDIA_VIEWER); return this.getAppById(APPS.MEDIA_VIEWER);

View file

@ -1,5 +1,5 @@
import { CHIP, COLORS, PIN } from "../../../../config/apps/logicSim.config"; import { CHIP, COLORS, PIN } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import { Circuit } from "../core/circuit"; import { Circuit } from "../core/circuit";
import { Pin, PinJson } from "../pins/pin"; import { Pin, PinJson } from "../pins/pin";
import { State } from "../_utils/state"; import { State } from "../_utils/state";
@ -16,19 +16,19 @@ export interface ChipJson {
} }
export class Chip { export class Chip {
color: string; color!: string;
name: string; name!: string;
position = Vector2.ZERO; position = Vector2.ZERO;
size: Vector2; size!: Vector2;
circuit: Circuit; circuit!: Circuit;
isCircuit = false; isCircuit = false;
isBlueprint = false; isBlueprint = false;
inputCount = 0; inputCount = 0;
outputCount = 0; outputCount = 0;
inputPins: Pin[]; inputPins!: Pin[];
outputPins: Pin[]; outputPins!: Pin[];
logic: (inputStates: State[]) => State[]; logic!: (inputStates: State[]) => State[];
constructor(circuit: Circuit | null, name: string, color: string, isBlueprint: boolean, inputCount: number, outputCount: number) { constructor(circuit: Circuit | null, name: string, color: string, isBlueprint: boolean, inputCount: number, outputCount: number) {
Object.assign(this, { circuit, name, color, isBlueprint, inputCount, outputCount }); Object.assign(this, { circuit, name, color, isBlueprint, inputCount, outputCount });

View file

@ -3,12 +3,12 @@ import { State } from "../_utils/state";
import { Circuit, CircuitJson } from "../core/circuit"; import { Circuit, CircuitJson } from "../core/circuit";
import { VirtualFolder } from "../../../virtual-drive/folder"; import { VirtualFolder } from "../../../virtual-drive/folder";
import { ControlledPin } from "../pins/controlledPin"; import { ControlledPin } from "../pins/controlledPin";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import { Pin } from "../pins/pin"; import { Pin } from "../pins/pin";
import { Wire } from "../wires/wire"; import { Wire } from "../wires/wire";
export class ChipsManager { export class ChipsManager {
static CHIPS = { static CHIPS: Record<string, Chip> = {
AND: new Chip(null, "AND", "blue", true, 2, 1).setLogic((inputStates: State[]) => { AND: new Chip(null, "AND", "blue", true, 2, 1).setLogic((inputStates: State[]) => {
if (inputStates[0].value === 1 && inputStates[1].value === 1) { if (inputStates[0].value === 1 && inputStates[1].value === 1) {
return [State.HIGH]; return [State.HIGH];
@ -34,8 +34,8 @@ export class ChipsManager {
if (virtualFile == null) if (virtualFile == null)
return; return;
virtualFile.read().then((content) => { virtualFile.read()?.then((content) => {
const data = JSON.parse(content) as CircuitJson; const data = JSON.parse(content as string) as CircuitJson;
circuit.color = data.color; circuit.color = data.color;
circuit.name = data.name; circuit.name = data.name;
@ -84,7 +84,7 @@ export class ChipsManager {
}); });
// Load logic // Load logic
newChip.setLogic((ChipsManager.CHIPS[chipData.name] as Chip).logic); newChip.setLogic((ChipsManager.CHIPS[chipData.name]).logic);
newChip.update(); newChip.update();
circuit.chips.push(newChip); circuit.chips.push(newChip);

View file

@ -1,10 +1,10 @@
import { BACKGROUND, COLORS, CURSORS, FONT, CONTROLLER, ENABLE_COLOR_CACHING } from "../../../../config/apps/logicSim.config"; import { BACKGROUND, COLORS, CURSORS, FONT, CONTROLLER, ENABLE_COLOR_CACHING } from "../../../../config/apps/logicSim.config";
import { clamp } from "../../../math/clamp"; import { Vector2 } from "../../../math/vector2";
import Vector2 from "../../../math/vector2";
import { Chip, ChipJson } from "../chips/chip"; import { Chip, ChipJson } from "../chips/chip";
import { ControlledPin } from "../pins/controlledPin"; import { ControlledPin } from "../pins/controlledPin";
import { InputHandler } from "./inputHandler"; import { InputHandler } from "./inputHandler";
import { Wire, WireJson } from "../wires/wire"; import { Wire, WireJson } from "../wires/wire";
import { clamp } from "../../../_utils/math.utils";
export interface CircuitJson extends ChipJson { export interface CircuitJson extends ChipJson {
wires: WireJson[]; wires: WireJson[];
@ -12,9 +12,9 @@ export interface CircuitJson extends ChipJson {
} }
export class Circuit extends Chip { export class Circuit extends Chip {
canvas: HTMLCanvasElement; canvas!: HTMLCanvasElement;
size = Vector2.ZERO; size = Vector2.ZERO;
context: CanvasRenderingContext2D; context!: CanvasRenderingContext2D;
colors: { [key: string]: string } = {}; colors: { [key: string]: string } = {};
inputHandler: InputHandler; inputHandler: InputHandler;
@ -40,7 +40,7 @@ export class Circuit extends Chip {
init(canvas: HTMLCanvasElement) { init(canvas: HTMLCanvasElement) {
this.canvas = canvas; this.canvas = canvas;
this.context = this.canvas.getContext("2d"); this.context = this.canvas.getContext("2d") as CanvasRenderingContext2D;
this.resize(); this.resize();
// Detect size changes of canvas // Detect size changes of canvas

View file

@ -1,5 +1,5 @@
import { CONTROLLER, PIN, WIRE } from "../../../../config/apps/logicSim.config"; import { CONTROLLER, PIN, WIRE } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import { Chip } from "../chips/chip"; import { Chip } from "../chips/chip";
import { Circuit } from "./circuit"; import { Circuit } from "./circuit";
import { ControlledPin } from "../pins/controlledPin"; import { ControlledPin } from "../pins/controlledPin";
@ -8,18 +8,18 @@ import { State } from "../_utils/state";
import { Wire } from "../wires/wire"; import { Wire } from "../wires/wire";
export class InputHandler { export class InputHandler {
circuit: Circuit; circuit!: Circuit;
canvas: HTMLCanvasElement; canvas!: HTMLCanvasElement;
mousePosition = Vector2.ZERO; mousePosition = Vector2.ZERO;
isPlacing = false; isPlacing = false;
snapping = false; snapping = false;
placingOffset = Vector2.ZERO; placingOffset = Vector2.ZERO;
previousPlacement: Vector2 | undefined = null; previousPlacement!: Vector2 | null;
placingWire: Wire; placingWire!: Wire | null;
placingChip: Chip; placingChip!: Chip | null;
placingPin: ControlledPin; placingPin!: ControlledPin | null;
constructor(circuit: Circuit) { constructor(circuit: Circuit) {
Object.assign(this, { circuit }); Object.assign(this, { circuit });
@ -269,8 +269,8 @@ export class InputHandler {
const isInputPin = pin.isPointingRight; const isInputPin = pin.isPointingRight;
// Start wire placement // Start wire placement
const inputPin = isInputPin ? pin : null; const inputPin = isInputPin ? pin : undefined;
const outputPin = !isInputPin ? pin : null; const outputPin = !isInputPin ? pin : undefined;
const anchorPoint = this.mousePosition.clone; const anchorPoint = this.mousePosition.clone;
this.placingWire = new Wire(this.circuit, "red", inputPin, outputPin, [anchorPoint]); this.placingWire = new Wire(this.circuit, "red", inputPin, outputPin, [anchorPoint]);
@ -305,7 +305,7 @@ export class InputHandler {
} }
}); });
if (closestPositionX != null && closestDistance < WIRE.snappingSensitivity) if (closestDistance != null && closestPositionX != null && closestDistance < WIRE.snappingSensitivity)
lastAnchorPoint.x = closestPositionX; lastAnchorPoint.x = closestPositionX;
} }
@ -316,7 +316,7 @@ export class InputHandler {
// Snapping vertical wire to pins // Snapping vertical wire to pins
let pins: Pin[]; let pins: Pin[];
if (!this.placingWire.placedBackwards) { if (!this.placingWire?.placedBackwards) {
pins = this.circuit.outputPins; pins = this.circuit.outputPins;
this.circuit.chips.forEach((chip) => { this.circuit.chips.forEach((chip) => {
@ -342,28 +342,32 @@ export class InputHandler {
} }
}); });
if (closestPositionY != null && closestDistance < WIRE.snappingSensitivity) if (closestDistance != null && closestPositionY != null && closestDistance < WIRE.snappingSensitivity)
lastAnchorPoint.y = closestPositionY; lastAnchorPoint.y = closestPositionY;
} }
updateWirePlacement() { updateWirePlacement() {
const anchorCount = this.placingWire.anchorPoints.length; const anchorCount = this.placingWire?.anchorPoints.length;
const lastAnchorPoint = this.placingWire.anchorPoints[anchorCount - 1]; if (anchorCount == null) return;
const lastAnchorPoint = this.placingWire?.anchorPoints[anchorCount - 1];
if (lastAnchorPoint == null) return;
if (!this.snapping) { if (!this.snapping) {
lastAnchorPoint.x = this.mousePosition.x; lastAnchorPoint.x = this.mousePosition.x;
lastAnchorPoint.y = this.mousePosition.y; lastAnchorPoint.y = this.mousePosition.y;
} else { } else {
// Wire snapping // Wire snapping
let previousAnchorPoint: Vector2; let previousAnchorPoint: Vector2 | undefined;
if (anchorCount >= 2) { if (anchorCount >= 2) {
previousAnchorPoint = this.placingWire.anchorPoints[anchorCount - 2]; previousAnchorPoint = this.placingWire?.anchorPoints[anchorCount - 2];
} else if (!this.placingWire.placedBackwards) { } else if (!this.placingWire?.placedBackwards) {
previousAnchorPoint = this.placingWire.inputPin.position; previousAnchorPoint = this.placingWire?.inputPin.position;
} else { } else {
previousAnchorPoint = this.placingWire.outputPin.position; previousAnchorPoint = this.placingWire?.outputPin.position;
} }
if (previousAnchorPoint == null) return;
const deltaX = Math.abs(this.mousePosition.x - previousAnchorPoint.x); const deltaX = Math.abs(this.mousePosition.x - previousAnchorPoint.x);
const deltaY = Math.abs(this.mousePosition.y - previousAnchorPoint.y); const deltaY = Math.abs(this.mousePosition.y - previousAnchorPoint.y);
@ -377,7 +381,7 @@ export class InputHandler {
} }
anchorWirePlacement() { anchorWirePlacement() {
this.placingWire.anchorPoints.push(this.mousePosition.clone); this.placingWire?.anchorPoints.push(this.mousePosition.clone);
} }
cancelWirePlacement() { cancelWirePlacement() {
@ -389,6 +393,8 @@ export class InputHandler {
endWirePlacement(pin: Pin) { endWirePlacement(pin: Pin) {
const isInputPin = pin.isPointingRight; const isInputPin = pin.isPointingRight;
if (this.placingWire == null) return;
let correctPlacement = false; let correctPlacement = false;
if (!isInputPin && !this.placingWire.placedBackwards) { if (!isInputPin && !this.placingWire.placedBackwards) {
this.placingWire.outputPin = pin; this.placingWire.outputPin = pin;
@ -434,11 +440,14 @@ export class InputHandler {
} }
updateChipPlacement() { updateChipPlacement() {
if (this.placingChip == null) return;
this.placingChip.position.x = this.mousePosition.x - this.placingChip.size.x / 2 + this.placingOffset.x; this.placingChip.position.x = this.mousePosition.x - this.placingChip.size.x / 2 + this.placingOffset.x;
this.placingChip.position.y = this.mousePosition.y - this.placingChip.size.y / 2 + this.placingOffset.y; this.placingChip.position.y = this.mousePosition.y - this.placingChip.size.y / 2 + this.placingOffset.y;
} }
cancelChipPlacement() { cancelChipPlacement() {
if (this.placingChip == null) return;
if (this.previousPlacement != null) { if (this.previousPlacement != null) {
this.placingChip.position = this.previousPlacement; this.placingChip.position = this.previousPlacement;
this.previousPlacement = null; this.previousPlacement = null;
@ -473,11 +482,12 @@ export class InputHandler {
} }
updatePinPlacement() { updatePinPlacement() {
this.placingPin.position.y = this.mousePosition.y; if (this.placingPin != null)
this.placingPin.position.y = this.mousePosition.y;
} }
cancelPinPlacement() { cancelPinPlacement() {
if (this.placingPin.isInput) { if (this.placingPin?.isInput) {
this.circuit.inputPins.pop(); this.circuit.inputPins.pop();
} else { } else {
this.circuit.outputPins.pop(); this.circuit.outputPins.pop();

View file

@ -1,5 +1,5 @@
import { COLORS, CONTROLLER, CURSORS } from "../../../../config/apps/logicSim.config"; import { COLORS, CONTROLLER, CURSORS } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import { Circuit } from "../core/circuit"; import { Circuit } from "../core/circuit";
import { Pin } from "./pin"; import { Pin } from "./pin";

View file

@ -1,5 +1,5 @@
import { COLORS, CONTROLLER, CURSORS, PIN } from "../../../../config/apps/logicSim.config"; import { COLORS, CONTROLLER, CURSORS, PIN } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import { Chip } from "../chips/chip"; import { Chip } from "../chips/chip";
import { Circuit } from "../core/circuit"; import { Circuit } from "../core/circuit";
import { State } from "../_utils/state"; import { State } from "../_utils/state";
@ -16,13 +16,13 @@ export interface PinJson {
export class Pin { export class Pin {
id: number; id: number;
name: string; name!: string;
position = Vector2.ZERO; position = Vector2.ZERO;
attachedChip: Chip; attachedChip!: Chip;
circuit: Circuit; circuit!: Circuit;
state = State.LOW; state = State.LOW;
isInput: boolean; isInput!: boolean;
isControlled: boolean = false; isControlled: boolean = false;
outputWires: Wire[] = []; outputWires: Wire[] = [];

View file

@ -1,5 +1,5 @@
import { WIRE } from "../../../../config/apps/logicSim.config"; import { WIRE } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import { Circuit } from "../core/circuit"; import { Circuit } from "../core/circuit";
import { Pin } from "../pins/pin"; import { Pin } from "../pins/pin";
import { State } from "../_utils/state"; import { State } from "../_utils/state";
@ -15,12 +15,12 @@ export interface WireJson {
} }
export class Wire { export class Wire {
color: string; color!: string;
state = State.LOW; state = State.LOW;
inputPin: Pin; inputPin!: Pin;
outputPin: Pin; outputPin!: Pin;
anchorPoints: Vector2[]; anchorPoints!: Vector2[];
circuit: Circuit; circuit!: Circuit;
placedBackwards = false; placedBackwards = false;
constructor(circuit: Circuit, color: string, inputPin?: Pin, outputPin?: Pin, anchorPoints?: Vector2[]) { constructor(circuit: Circuit, color: string, inputPin?: Pin, outputPin?: Pin, anchorPoints?: Vector2[]) {

View file

@ -1,7 +1,7 @@
import { SettingsManager } from "../../settings/settingsManager"; import { SettingsManager } from "../../settings/settingsManager";
import { VirtualFolder } from "../../virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../virtual-drive/folder/virtualFolder";
import { VirtualRoot } from "../../virtual-drive/root/virtualRoot"; import { VirtualRoot } from "../../virtual-drive/root/virtualRoot";
import Stream from "./stream"; import { Stream } from "./stream";
type Option = { type Option = {
long: string, long: string,
@ -10,12 +10,11 @@ type Option = {
}; };
export type CommandResponse = string | { blank: boolean } | void | Stream; export type CommandResponse = string | { blank: boolean } | void | Stream;
export type ExecuteParams = {
type Execute = (args?: string[], options?: {
promptOutput?: Function, promptOutput?: Function,
pushHistory?: Function, pushHistory?: Function,
virtualRoot?: VirtualRoot, virtualRoot?: VirtualRoot,
currentDirectory?: VirtualFolder, currentDirectory: VirtualFolder,
setCurrentDirectory?: Function, setCurrentDirectory?: Function,
username?: string, username?: string,
hostname?: string, hostname?: string,
@ -25,7 +24,9 @@ type Execute = (args?: string[], options?: {
inputs?: Record<string, string>, inputs?: Record<string, string>,
timestamp: number, timestamp: number,
settingsManager: SettingsManager, settingsManager: SettingsManager,
}) => CommandResponse | Promise<CommandResponse>; };
type Execute = (args?: string[], params?: ExecuteParams) => CommandResponse | Promise<CommandResponse>;
type Manual = { type Manual = {
purpose?: string, purpose?: string,
@ -34,12 +35,12 @@ type Manual = {
options?: object options?: object
}; };
export default class Command { export class Command {
name: string | undefined; name: string = "command";
options: Option[] = []; options: Option[] = [];
manual: Manual; manual: Manual | undefined;
requireArgs: boolean; requireArgs: boolean | undefined;
requireOptions: boolean; requireOptions: boolean | undefined;
execute: Execute = () => {}; execute: Execute = () => {};
@ -81,8 +82,8 @@ export default class Command {
return this; return this;
} }
getOption(key: string): Option { getOption(key: string): Option | null {
let matchingOption: Option = null; let matchingOption: Option | null = null;
this.options.forEach((option) => { this.options.forEach((option) => {
if (option.short === key || option.long === key) if (option.short === key || option.long === key)

View file

@ -1,4 +1,4 @@
import Command from "./command"; import { Command } from "./command";
let commands: Command[] = []; let commands: Command[] = [];
@ -11,10 +11,10 @@ const loadCommands = () => {
// https://vitejs.dev/guide/features.html#glob-import // https://vitejs.dev/guide/features.html#glob-import
const modules = import.meta.glob("./commands/*.ts"); const modules = import.meta.glob("./commands/*.ts");
for (const path in modules) { for (const path in modules) {
void modules[path]().then((commandModule: Record<string, Command>) => { void modules[path]().then((commandModule) => {
const commandName = Object.keys(commandModule)[0]; const commandName = Object.keys(commandModule as Record<string, Command>)[0];
const command = commandModule[commandName]; const command = (commandModule as Record<string, Command>)[commandName];
command.setName(commandName.toLowerCase()); command.setName(commandName.toLowerCase());
commands.push(command); commands.push(command);
@ -24,11 +24,11 @@ const loadCommands = () => {
loadCommands(); loadCommands();
export default class CommandsManager { export class CommandsManager {
static COMMANDS = commands; static COMMANDS = commands;
static find(name: string): Command { static find(name: string): Command | null {
let matchCommand: Command = null; let matchCommand: Command | null = null;
this.COMMANDS.forEach((command) => { this.COMMANDS.forEach((command) => {
if (command.name === name) { if (command.name === name) {
@ -41,7 +41,7 @@ export default class CommandsManager {
} }
static search(pattern: string): Command[] { static search(pattern: string): Command[] {
const matches = this.COMMANDS.filter((command) => command.name.match(pattern)); const matches = this.COMMANDS.filter((command) => command.name?.match(pattern));
return matches; return matches;
} }

View file

@ -1,6 +1,6 @@
import { VirtualFile } from "../../../virtual-drive/file"; import { VirtualFile } from "../../../virtual-drive/file";
import { formatError } from "../_utils/terminal.utils"; import { formatError } from "../_utils/terminal.utils";
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const cat = new Command() export const cat = new Command()
.setRequireArgs(true) .setRequireArgs(true)
@ -9,15 +9,17 @@ export const cat = new Command()
usage: "cat [options] [files]", usage: "cat [options] [files]",
description: "Concetenate files to standard output." description: "Concetenate files to standard output."
}) })
.setExecute(function(args, { currentDirectory, options }) { .setExecute(function(this: Command, args, params) {
const { name, extension } = VirtualFile.convertId(args[0]); const { currentDirectory, options } = params as ExecuteParams;
const fileId = (args as string[])[0];
const { name, extension } = VirtualFile.splitId(fileId);
const file = currentDirectory.findFile(name, extension); const file = currentDirectory.findFile(name, extension);
if (!file) if (!file)
return formatError((this as Command).name, `${args[0]}: No such file`); return formatError(this.name, `${fileId}: No such file`);
if (file.content) { if (file.content) {
if (!options.includes("e")) { if (!options?.includes("e")) {
return file.content; return file.content;
} else { } else {
// Append "$" at the end of every line // Append "$" at the end of every line

View file

@ -1,5 +1,5 @@
import { formatError } from "../_utils/terminal.utils"; import { formatError } from "../_utils/terminal.utils";
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const cd = new Command() export const cd = new Command()
.setManual({ .setManual({
@ -7,13 +7,14 @@ export const cd = new Command()
usage: "cd [PATH]", usage: "cd [PATH]",
description: "Change working directory to given path (the home directory by default)." description: "Change working directory to given path (the home directory by default)."
}) })
.setExecute(function(args, { currentDirectory, setCurrentDirectory }) { .setExecute(function(this: Command, args, params) {
const path = args[0] ?? "~"; const { currentDirectory, setCurrentDirectory } = params as ExecuteParams;
const path = (args as string[])[0] ?? "~";
const destination = currentDirectory.navigate(path); const destination = currentDirectory.navigate(path);
if (!destination) if (!destination)
return formatError(this.name, `${args[0]}: No such file or directory`); return formatError(this.name, `${(args as string[])[0]}: No such file or directory`);
setCurrentDirectory(destination); setCurrentDirectory?.(destination);
return { blank: true }; return { blank: true };
}); });

View file

@ -1,11 +1,12 @@
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const clear = new Command() export const clear = new Command()
.setManual({ .setManual({
purpose: "Clear terminal screen", purpose: "Clear terminal screen",
}) })
.setExecute(function(args, { pushHistory }) { .setExecute(function(args, params) {
pushHistory({ const { pushHistory } = params as ExecuteParams;
pushHistory?.({
clear: true, clear: true,
isInput: false isInput: false
}); });

View file

@ -1,9 +1,9 @@
import { ANSI } from "../../../../config/apps/terminal.config"; import { ANSI } from "../../../../config/apps/terminal.config";
import { randomFromArray, removeFromArray } from "../../../_utils/array.utils"; import { randomFromArray, removeFromArray } from "../../../_utils/array.utils";
import { randomRange } from "../../../math/random"; import { randomRange } from "../../../_utils/math.utils";
import Vector2 from "../../../math/vector2"; import { Vector2 } from "../../../math/vector2";
import Command from "../command"; import { Command } from "../command";
import Stream from "../stream"; import { Stream } from "../stream";
const ANIMATION_SPEED = 1.25; const ANIMATION_SPEED = 1.25;
const SCREEN_WIDTH = 75; const SCREEN_WIDTH = 75;

View file

@ -1,13 +1,14 @@
import Command from "../command"; import { Command, ExecuteParams } from "../command";
import CommandsManager from "../commands"; import { CommandsManager } from "../commands";
export const compgen = new Command() export const compgen = new Command()
.setManual({ .setManual({
purpose: "Display a list of all commands" purpose: "Display a list of all commands"
}) })
.setRequireOptions(true) .setRequireOptions(true)
.setExecute(function(args, { options }) { .setExecute(function(args, params) {
if (options.includes("c")) { const { options } = params as ExecuteParams;
if (options?.includes("c")) {
return CommandsManager.COMMANDS.map((command) => command.name).sort().join("\n"); return CommandsManager.COMMANDS.map((command) => command.name).sort().join("\n");
} }
}); });

View file

@ -1,5 +1,5 @@
import { MAX_WIDTH } from "../../../../config/apps/terminal.config"; import { MAX_WIDTH } from "../../../../config/apps/terminal.config";
import Command from "../command"; import { Command, ExecuteParams } from "../command";
const COW = ` const COW = `
\\ ^__^ \\ ^__^
@ -15,14 +15,16 @@ export const cowsay = new Command()
usage: "cowsay text", usage: "cowsay text",
description: "Show ASCII art of a cow saying something." description: "Show ASCII art of a cow saying something."
}) })
.setExecute(function(args, { rawInputValue }) { .setExecute(function(args, params) {
const { rawInputValue } = params as ExecuteParams;
// Separate input value into lines // Separate input value into lines
const segments = rawInputValue.split(" "); const segments = rawInputValue?.split(" ");
const lines = []; const lines: string[] = [];
let currentLine = ""; let currentLine = "";
let maxLineWidth = 0; let maxLineWidth = 0;
const addLine = (line) => { const addLine = (line: string) => {
line = line.trimEnd(); line = line.trimEnd();
lines.push(line); lines.push(line);
if (line.length > maxLineWidth) if (line.length > maxLineWidth)
@ -39,7 +41,7 @@ export const cowsay = new Command()
} }
}; };
segments.forEach((segment) => { segments?.forEach((segment) => {
// Add empty spaces preceding lines // Add empty spaces preceding lines
if (segment === "") { if (segment === "") {
currentLine += " "; currentLine += " ";

View file

@ -1,10 +1,11 @@
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const dir = new Command() export const dir = new Command()
.setManual({ .setManual({
purpose: "List all directories in the current directory" purpose: "List all directories in the current directory"
}) })
.setExecute(function(args, { currentDirectory }) { .setExecute(function(args, params) {
const { currentDirectory } = params as ExecuteParams;
const folderNames = currentDirectory.subFolders.map((folder) => folder.id); const folderNames = currentDirectory.subFolders.map((folder) => folder.id);
if (folderNames.length === 0) if (folderNames.length === 0)

View file

@ -1,9 +1,10 @@
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const echo = new Command() export const echo = new Command()
.setManual({ .setManual({
purpose: "Display text on the terminal screen" purpose: "Display text on the terminal screen"
}) })
.setExecute(function(args, { rawInputValue }) { .setExecute(function(args, params) {
const { rawInputValue } = params as ExecuteParams;
return rawInputValue; return rawInputValue;
}); });

View file

@ -1,10 +1,11 @@
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const exit = new Command() export const exit = new Command()
.setManual({ .setManual({
purpose: "Quit terminal interface" purpose: "Quit terminal interface"
}) })
.setExecute(function(args, { exit }) { .setExecute(function(args, params) {
exit(); const { exit } = params as ExecuteParams;
exit?.();
return { blank: true }; return { blank: true };
}); });

View file

@ -1,5 +1,5 @@
import { randomFromArray } from "../../../_utils/array.utils"; import { randomFromArray } from "../../../_utils/array.utils";
import Command from "../command"; import { Command } from "../command";
/** /**
* Source: https://github.com/shlomif/fortune-mod/blob/master/fortune-mod/datfiles/fortunes * Source: https://github.com/shlomif/fortune-mod/blob/master/fortune-mod/datfiles/fortunes
@ -8,7 +8,7 @@ const FORTUNES = [
"Do not be afraid of competition.", "Do not be afraid of competition.",
"An exciting opportunity lies ahead of you.", "An exciting opportunity lies ahead of you.",
"You love peace.", "You love peace.",
"Get your mind setconfidence will lead you on.", "Get your mind set... confidence will lead you on.",
"You will always be surrounded by true friends.", "You will always be surrounded by true friends.",
"Sell your ideas-they have exceptional merit.", "Sell your ideas-they have exceptional merit.",
"You should be able to undertake and complete anything.", "You should be able to undertake and complete anything.",

View file

@ -1,11 +1,11 @@
import { ANSI } from "../../../../config/apps/terminal.config"; import { ANSI } from "../../../../config/apps/terminal.config";
import { formatError } from "../_utils/terminal.utils"; import { formatError } from "../_utils/terminal.utils";
import Command from "../command"; import { Command } from "../command";
import CommandsManager from "../commands"; import { CommandsManager } from "../commands";
export const help = new Command() export const help = new Command()
.setExecute(function(args) { .setExecute(function(this: Command, args) {
if (args.length === 0) { if (args?.length === 0) {
return CommandsManager.COMMANDS.map((command) => { return CommandsManager.COMMANDS.map((command) => {
if (command.manual?.purpose) { if (command.manual?.purpose) {
return `${command.name} - ${ANSI.fg.green}${ANSI.decoration.dim}${command.manual.purpose}${ANSI.reset}`; return `${command.name} - ${ANSI.fg.green}${ANSI.decoration.dim}${command.manual.purpose}${ANSI.reset}`;
@ -15,7 +15,7 @@ export const help = new Command()
}).sort().join("\n"); }).sort().join("\n");
} }
const commandName = args[0].toLowerCase(); const commandName = (args as string[])[0].toLowerCase();
const command = CommandsManager.find(commandName); const command = CommandsManager.find(commandName);
if (!command) if (!command)

View file

@ -1,9 +1,10 @@
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const hostname = new Command() export const hostname = new Command()
.setManual({ .setManual({
purpose: "Display the hostname" purpose: "Display the hostname"
}) })
.setExecute(function(args, { hostname }) { .setExecute(function(args, params) {
const { hostname } = params as ExecuteParams;
return hostname; return hostname;
}); });

View file

@ -1,6 +1,6 @@
import { ANSI } from "../../../../config/apps/terminal.config"; import { ANSI } from "../../../../config/apps/terminal.config";
import { removeAnsi } from "../_utils/terminal.utils"; import { removeAnsi } from "../_utils/terminal.utils";
import Command from "../command"; import { Command, ExecuteParams } from "../command";
const COLUMN_WIDTH = 5; const COLUMN_WIDTH = 5;
const ROW_OFFSET = 2; const ROW_OFFSET = 2;
@ -18,18 +18,20 @@ export const lolcat = new Command()
.setManual({ .setManual({
purpose: "Display text with a rainbow effect" purpose: "Display text with a rainbow effect"
}) })
.setExecute(function(args, { rawInputValue, timestamp }) { .setExecute(function(args, params) {
const { rawInputValue, timestamp } = params as ExecuteParams;
if (rawInputValue == null) return;
let rows = removeAnsi(rawInputValue).split("\n"); let rows = removeAnsi(rawInputValue).split("\n");
const offset = timestamp / 100; const offset = timestamp / 100;
rows = rows.map((row, index) => { rows = rows.map((row, index) => {
const columns = []; const columns: string[] = [];
const rowIndex = index + offset; const rowIndex = index + offset;
const rowOffset = COLUMN_WIDTH - ((ROW_OFFSET * rowIndex) % COLUMN_WIDTH); const rowOffset = COLUMN_WIDTH - ((ROW_OFFSET * rowIndex) % COLUMN_WIDTH);
let rainbowIndex = Math.floor(rowIndex / (COLUMN_WIDTH / ROW_OFFSET)); let rainbowIndex = Math.floor(rowIndex / (COLUMN_WIDTH / ROW_OFFSET));
const addColumn = (start, end) => { const addColumn = (start: number, end: number) => {
const column = row.substring(start, end); const column = row.substring(start, end);
const color = RAINBOW[rainbowIndex++ % RAINBOW.length]; const color = RAINBOW[rainbowIndex++ % RAINBOW.length];
columns.push(color + column); columns.push(color + column);

View file

@ -1,7 +1,7 @@
import { ANSI } from "../../../../config/apps/terminal.config"; import { ANSI } from "../../../../config/apps/terminal.config";
import { VirtualFolder } from "../../../virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../../virtual-drive/folder/virtualFolder";
import { formatError } from "../_utils/terminal.utils"; import { formatError } from "../_utils/terminal.utils";
import Command from "../command"; import { Command, ExecuteParams } from "../command";
export const ls = new Command() export const ls = new Command()
.setManual({ .setManual({
@ -9,15 +9,16 @@ export const ls = new Command()
usage: "ls [options] [files]", usage: "ls [options] [files]",
description: "List information about directories or files (the current directory by default)." description: "List information about directories or files (the current directory by default)."
}) })
.setExecute(function(args, { currentDirectory }) { .setExecute(function(this: Command, args, params) {
const { currentDirectory } = params as ExecuteParams;
let directory = currentDirectory; let directory = currentDirectory;
if (args.length > 0) { if (args != null && args.length > 0) {
directory = currentDirectory.navigate(args[0]) as VirtualFolder; directory = currentDirectory.navigate((args)[0]) as VirtualFolder;
} }
if (!directory) if (!directory)
return formatError(this.name, `Cannot access '${args[0]}': No such file or directory`); return formatError(this.name, `Cannot access '${(args as string[])[0]}': No such file or directory`);
const folderNames = directory.subFolders.map((folder) => `${ANSI.fg.blue}${folder.id}${ANSI.reset}`); const folderNames = directory.subFolders.map((folder) => `${ANSI.fg.blue}${folder.id}${ANSI.reset}`);
const fileNames = directory.files.map((file) => file.id); const fileNames = directory.files.map((file) => file.id);

View file

@ -1,9 +1,9 @@
import { formatError } from "../_utils/terminal.utils"; import { formatError } from "../_utils/terminal.utils";
import Command from "../command"; import { Command } from "../command";
export const make = new Command() export const make = new Command()
.setRequireArgs(true) .setRequireArgs(true)
.setExecute(function(args) { .setExecute(function(this: Command, args) {
if (args[0] === "love") if ((args as string[])[0] === "love")
return formatError(this.name, "*** No rule to make target 'love'. Stop."); return formatError(this.name, "*** No rule to make target 'love'. Stop.");
}); });

Some files were not shown because too many files have changed in this diff Show more