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": [
"bierner.github-markdown-preview",
"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": {
"@eslint/js": "^9.2.0",
"@types/eslint": "^8.56.10",
"@types/gh-pages": "^6.1.0",
"@types/node": "^20.12.8",
"@types/react": "^18.3.1",
@ -51,9 +52,11 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-refresh": "^0.4.7",
"gh-pages": "^5.0.0",
"stylelint": "^16.6.1",
"tsx": "^4.15.4",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0"
"typescript-eslint": "^7.8.0",
"vite-plugin-checker": "^0.6.4"
},
"browserslist": {
"production": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View file

@ -1,3 +1,3 @@
<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>

View file

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

View file

@ -1,5 +1,5 @@
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 { NAME, TAG_LINE } from "../src/config/branding.config";
import { WALLPAPERS } from "../src/config/desktop.config";
@ -91,8 +91,8 @@ function generate404Page(template: string) {
function generateAppPages(template: string) {
for (const [key, value] of Object.entries(APPS)) {
const appId = value;
const appName = Object.keys(APP_NAMES).includes(key) ? APP_NAMES[key] as string : appId;
const appDescription = Object.keys(APP_DESCRIPTIONS).includes(key) ? APP_DESCRIPTIONS[key] as string : TAG_LINE;
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 AppKey] as string : TAG_LINE;
if (appId === "index") {
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 { Router } from "./components/router/Router";
export default function App(): ReactElement {
export function App(): ReactElement {
useEffect(() => {
const onContextMenu = (event: Event) => {
event.preventDefault();

View file

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

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
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";
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";
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 onButtonClick = (event) => {
const onButtonClick = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
@ -28,7 +35,7 @@ export function Interactable({ onClick, onDoubleClick, children, ...props }) {
}, INTERACTIBLE_DOUBLE_CLICK_DELAY);
};
return <button {...props} onClick={onButtonClick}>
return <button {...props} onClick={onButtonClick as unknown as MouseEventHandler}>
{children}
</button>;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ export function ToggleAction({ actionId, label, shortcut, initialValue, onTrigge
const [active, setActive] = useState(initialValue ?? false);
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);
}}>
<span className={styles.Label}>

View file

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

View file

@ -1,15 +1,15 @@
import styles from "./HeaderMenu.module.css";
import { ActionsProps, Actions } from "../../../actions/Actions";
import { STYLES } from "../../../../config/actions.config";
import { useZIndex } from "../../../../hooks/z-index/zIndex";
import { ZIndexManager } from "../../../../features/z-index/zIndexManager";
import { ActionsManager } from "../../../../features/actions/actionsManager";
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 }}>
<Actions className={STYLES.HEADER_MENU} {...props}>
<Actions mode={ActionsManager.MODES.headerMenu} {...props}>
{children}
</Actions>
</div>;

View file

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

View file

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

View file

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

View file

@ -7,14 +7,14 @@ import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext"
import { useContextMenu } from "../../../hooks/modals/contextMenu";
import { QuickAccessButton } from "./QuickAccessButton";
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 { DirectoryList, OnSelectionChangeParams } from "./directory-list/DirectoryList";
import { DirectoryList, FileEventHandler, FolderEventHandler, OnSelectionChangeParams } from "./directory-list/DirectoryList";
import { Actions } from "../../actions/Actions";
import { ClickAction } from "../../actions/actions/ClickAction";
import utilStyles from "../../../styles/utils.module.css";
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 { TITLE_SEPARATOR } from "../../../config/windows.config";
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 { VirtualFile } from "../../../features/virtual-drive/file";
import { VirtualFolderLink } from "../../../features/virtual-drive/folder/virtualFolderLink";
import ImportButton from "./ImportButton";
import { ImportButton } from "./ImportButton";
import { useAlert } from "../../../hooks/modals/alert";
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 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 windowsManager = useWindowsManager();
const [showHidden] = useState(true);
@ -52,38 +52,38 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
const { openWindowedModal } = useWindowedModal();
const { onContextMenu: onContextMenuFile } = useContextMenu({ Actions: (props) =>
<Actions {...props}>
<ClickAction label={!isSelector ? "Open" : "Select"} onTrigger={(event, file: VirtualFile) => {
<ClickAction label={!isSelector ? "Open" : "Select"} onTrigger={(event, file) => {
if (isSelector) {
onSelectionChange?.({ files: [file.id], directory: currentDirectory });
onSelectionChange?.({ files: [(file as VirtualFile).id], directory: currentDirectory });
onSelectionFinish?.();
return;
}
file.open(windowsManager);
if (windowsManager != null) (file as VirtualFile).open(windowsManager);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file: VirtualFile) => {
file.delete();
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
(file as VirtualFile).delete();
}}/>
<ClickAction label="Properties" icon={faCircleInfo} onTrigger={(event, file: VirtualFile) => {
<ClickAction label="Properties" icon={faCircleInfo} onTrigger={(event, file) => {
openWindowedModal({
title: `${file.id} ${TITLE_SEPARATOR} Properties`,
iconUrl: file.getIconUrl(),
title: `${(file as VirtualFile).id} ${TITLE_SEPARATOR} Properties`,
iconUrl: (file as VirtualFile).getIconUrl(),
size: new Vector2(400, 500),
Modal: (props: object) => <FileProperties file={file} {...props}/>
Modal: (props: object) => <FileProperties file={file as VirtualFile} {...props}/>
});
}}/>
</Actions>
});
const { onContextMenu: onContextMenuFolder } = useContextMenu({ Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder: VirtualFolder & VirtualFolderLink) => {
changeDirectory(folder.linkedPath ?? folder.name);
<ClickAction label="Open" onTrigger={(event, folder) => {
changeDirectory((folder as VirtualFolderLink).linkedPath ?? (folder as VirtualFolder).name);
}}/>
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder: VirtualFolder) => {
windowsManager.open(APPS.TERMINAL, { startPath: folder.path });
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={APP_ICONS.TERMINAL} onTrigger={(event, folder) => {
windowsManager?.open(APPS.TERMINAL, { startPath: (folder as VirtualFolder).path });
}}/>
<Divider/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder: VirtualFolder) => {
folder.delete();
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
(folder as VirtualFolder).delete();
}}/>
</Actions>
});
@ -102,7 +102,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
if (currentDirectory == null)
absolute = true;
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path);
const directory = absolute ? virtualRoot?.navigate(path) : currentDirectory.navigate(path);
if (directory != null) {
setCurrentDirectory(directory as VirtualFolder);
@ -116,7 +116,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
return;
const path = history[stateIndex];
const directory = virtualRoot.navigate(path);
const directory = virtualRoot?.navigate(path);
if (directory != null) {
setCurrentDirectory(directory as VirtualFolder);
setPath(directory.root ? "/" : directory.path);
@ -124,9 +124,10 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
}, [history, stateIndex, virtualRoot]);
useEffect(() => {
const onError = (error: { message: string }) => {
type Error = { message: string };
const onError = (error: unknown) => {
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.",
iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER),
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 () => {
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 === "")
value = "~";
const directory = virtualRoot.navigate(value);
const directory = virtualRoot?.navigate(value);
if (directory == null) {
openWindowedModal({
title: "Error",
iconUrl: AppsManager.getAppIconUrl(APPS.FILE_EXPLORER),
size: new Vector2(300, 150),
Modal: (props) =>
Modal: (props: {}) =>
<DialogBox {...props}>
<p>Invalid path: "{value}"</p>
<button data-type={DIALOG_CONTENT_TYPES.CloseButton}>Ok</button>
<button data-type={DIALOG_CONTENT_TYPES.closeButton}>Ok</button>
</DialogBox>
});
return;
@ -201,7 +202,7 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
tabIndex={0}
className={styles.IconButton}
onClick={() => { changeDirectory(".."); }}
disabled={currentDirectory.isRoot}
disabled={currentDirectory.isRoot != null && currentDirectory.isRoot}
>
<FontAwesomeIcon icon={faArrowUp}/>
</button>
@ -262,19 +263,19 @@ export function FileExplorer({ path: startPath, selectorMode, Footer, onSelectio
className={styles.Main}
showHidden={showHidden}
onOpenFile={(event, file) => {
(event as Event).preventDefault();
event.preventDefault();
if (isSelector)
return void onSelectionFinish?.();
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";
windowsManager.openFile(file, options);
windowsManager?.openFile(file, options);
}}
onOpenFolder={(event, folder) => {
changeDirectory((folder as VirtualFolderLink).linkedPath ?? folder.name);
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
onContextMenuFile={onContextMenuFile as unknown as FileEventHandler}
onContextMenuFolder={onContextMenuFolder as unknown as FolderEventHandler}
allowMultiSelect={selectorMode !== SELECTOR_MODE.SINGLE}
onSelectionChange={onSelectionChange}
/>

View file

@ -9,19 +9,22 @@ interface ImportButtonProps {
directory: VirtualFolder;
}
export default function ImportButton({ directory }: ImportButtonProps): ReactElement {
export function ImportButton({ directory }: ImportButtonProps): ReactElement {
const onChange = (event: InputEvent) => {
const files = (event.target as HTMLInputElement).files;
if (files == null)
return;
Array.from(files).forEach((file: File) => {
const { name, extension } = VirtualFile.convertId(file.name);
const { name, extension } = VirtualFile.splitId(file.name);
const reader = new FileReader();
reader.onload = (event: Event) => {
const { result } = event.target as FileReader;
// 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);
});
};

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 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 (
<button
tabIndex={0}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { Button } from "../../../_utils/button/Button";
import styles from "../Settings.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 { useVirtualRoot } from "../../../../hooks/virtual-drive/virtualRootContext";
import { NAME } from "../../../../config/branding.config";
@ -10,31 +10,29 @@ export function AboutSettings() {
const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot();
return (<>
<div className={styles.Option}>
<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>
<div className={styles.ButtonGroup}>
<Button
className={`${styles.Button} ${utilStyles.TextBold}`}
onClick={(event: Event) => {
event.preventDefault();
windowsManager.open("text-editor", {
mode: "view",
file: virtualRoot.navigate("~/Documents/Info.md"),
size: new Vector2(575, 675),
});
}}
>
Open Info.md
</Button>
<Button
className={`${styles.Button} ${utilStyles.TextBold}`}
href="https://github.com/Prozilla/ProzillaOS"
>
View source
</Button>
</div>
return <div className={styles.Option}>
<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>
<div className={styles.ButtonGroup}>
<Button
className={`${styles.Button} ${utilStyles.TextBold}`}
onClick={(event: Event) => {
event.preventDefault();
windowsManager?.open("text-editor", {
mode: "view",
file: virtualRoot?.navigate("~/Documents/Info.md"),
size: new Vector2(575, 675),
});
}}
>
Open Info.md
</Button>
<Button
className={`${styles.Button} ${utilStyles.TextBold}`}
href="https://github.com/Prozilla/ProzillaOS"
>
View source
</Button>
</div>
</>);
</div>;
}

View file

@ -1,5 +1,5 @@
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 styles from "../Settings.module.css";
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 { useSettingsManager } from "../../../../hooks/settings/settingsManagerContext";
import { SettingsManager } from "../../../../features/settings/settingsManager";
import App from "../../../../features/apps/app";
import { App } from "../../../../features/apps/app";
import { MouseEventHandler } from "react";
interface AppOptionProps {
app: App;
@ -26,7 +27,7 @@ export function AppOption({ app, pins, setPins: _setPins }: AppOptionProps) {
const { onContextMenu } = useContextMenu({ 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={() => {
const newPins = [...pins];
if (isPinned) {
@ -35,8 +36,8 @@ export function AppOption({ app, pins, setPins: _setPins }: AppOptionProps) {
newPins.push(app.id);
}
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings.set("pins", newPins.join(","));
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings?.set("pins", newPins.join(","));
}}/>
</Actions>
});
@ -46,7 +47,7 @@ export function AppOption({ app, pins, setPins: _setPins }: AppOptionProps) {
<ImagePreview className={styles.Icon} source={AppsManager.getAppIconUrl(app.id)}/>
{app.name}
</span>
<button className={styles.IconButton} onClick={onContextMenu}>
<button className={styles.IconButton} onClick={onContextMenu as unknown as MouseEventHandler}>
<FontAwesomeIcon icon={faEllipsisVertical}/>
</button>
{/* <div className={styles["Button-group"]}>

View file

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

View file

@ -1,18 +1,18 @@
import { useEffect, useState } from "react";
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 { SettingsManager } from "../../../../features/settings/settingsManager";
import { AppOption } from "./AppOption";
export function AppsSettings() {
const settingsManager = useSettingsManager();
const [pins, setPins] = useState([]);
const [pins, setPins] = useState<string[]>([]);
useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings.get("pins", (pins) => {
setPins(pins.split(","));
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.taskbar);
void settings?.get("pins", (newPins) => {
setPins(newPins.split(","));
});
}, [settingsManager]);

View file

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

View file

@ -28,7 +28,7 @@ function ansiToJSON(input: string, use_classes: boolean | undefined): AnserJsonE
* Create a class string.
* @returns class name(s)
*/
function createClass(bundle: AnserJsonEntry): string {
function createClass(bundle: AnserJsonEntry): string | null {
const classNames = [];
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;
let index = 0;
let match: RegExpExecArray;
let match: RegExpExecArray | null;
while ((match = linkRegex.exec(bundle.content)) !== null) {
const [, pre, url] = match;
@ -145,7 +145,7 @@ function convertBundleIntoReact(linkify: boolean, useClasses: boolean, bundle: A
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;
return React.createElement(
"code",

View file

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

View file

@ -1,16 +1,16 @@
import { FC, forwardRef } from "react";
import Ansi from "./Ansi";
import { Ansi } from "./Ansi";
import styles from "./Terminal.module.css";
interface OutputLineProps {
text: string;
text?: string;
}
export const OutputLine: FC<OutputLineProps> = forwardRef<HTMLDivElement>(({ text }: OutputLineProps, ref) => {
const lines = text?.split("\n");
return <div ref={ref}>
{lines.map((line, index) =>
{lines?.map((line, index) =>
<Ansi key={index} className={styles.Output} useClasses>{line === "" ? " " : line}</Ansi>
)}
</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 { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext";
import { clamp } from "../../../features/math/clamp";
import { OutputLine } from "./OutputLine";
import { InputLine } from "./InputLine";
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 Stream from "../../../features/apps/terminal/stream";
import { Stream } from "../../../features/apps/terminal/stream";
import { formatError } from "../../../features/apps/terminal/_utils/terminal.utils";
import { WindowProps } from "../../windows/WindowView";
import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder";
import { CommandResponse } from "../../../features/apps/terminal/command";
import { APP_NAMES } from "../../../config/apps.config";
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext";
import { SettingsManager } from "../../../features/settings/settingsManager";
import { clamp } from "../../../features/_utils/math.utils";
interface TerminalProps extends WindowProps {
path: string;
@ -35,18 +36,19 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
isInput: false,
}]);
const virtualRoot = useVirtualRoot();
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot?.navigate(startPath ?? "~"));
const inputRef = useRef(null);
const [historyIndex, setHistoryIndex] = useState(0);
const [stream, setStream] = useState<Stream>(null);
const [streamOutput, setStreamOutput] = useState<string>(null);
const [stream, setStream] = useState<Stream | null>(null);
const [streamOutput, setStreamOutput] = useState<string | null>(null);
const ref = useRef(null);
const [streamFocused, setStreamFocused] = useState(false);
const settingsManager = useSettingsManager();
useEffect(() => {
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
}, [currentDirectory.path, currentDirectory.root, setTitle]);
if (currentDirectory != null)
setTitle?.(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
}, [currentDirectory?.path, currentDirectory?.root, setTitle]);
useEffect(() => {
if (!inputRef.current || !active)
@ -75,7 +77,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
}, [inputValue]);
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 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 () => {
let output: CommandResponse = text;
let output: CommandResponse = text as CommandResponse;
for (const pipe of pipes) {
if (output instanceof Stream)
@ -141,35 +143,31 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
const rawInputValueStart = value.indexOf(" ") + 1;
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
const timestamp = Date.now();
value = value.trim();
if (value === "") return;
if (value === "")
return;
// Get arguments
// Parse arguments
const args = value.match(/(?:[^\s"]+|"[^"]*")+/g);
if (args[0].toLowerCase() === "sudo" && args.length > 1) {
args.shift();
}
if (args == null) return;
if (args[0].toLowerCase() === "sudo" && args.length >= 2) args.shift();
// Get command
const commandName = args.shift().toLowerCase();
const commandName = args.shift()?.toLowerCase();
if (commandName == null) return;
const command = CommandsManager.find(commandName);
if (!command)
return formatError(commandName, "Command not found");
if (!command) return formatError(commandName, "Command not found");
// Get options
const options: string[] = [];
const inputs = {};
const inputs: Record<string, string> = {};
args.filter((arg: string) => arg.startsWith("-")).forEach((option: string) => {
const addOption = (key: string) => {
if (options.includes(key))
return;
options.push(key);
const commandOption = command.getOption(options[options.length - 1]);
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`);
// Execute command
let response: CommandResponse = null;
let response: CommandResponse | null = null;
try {
response = await command.execute(args, {
@ -216,7 +214,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
exit,
inputs,
timestamp,
settingsManager,
settingsManager: settingsManager as SettingsManager,
});
if (response == null)
@ -244,15 +242,15 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
// Piping is used to chain commands
let pipes = value.split(" | ");
const completedPipes = [];
const completedPipes: string[] = [];
let output = null;
let output: CommandResponse | null = null;
for (const pipe of pipes) {
if (output instanceof Stream)
continue;
// 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);
}
@ -264,7 +262,7 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
if (output instanceof Stream) {
connectStream(output, pipes);
} 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) {
setInputValue("");
} else {
setInputValue(inputHistory[inputHistory.length - index].value);
setInputValue(inputHistory[inputHistory.length - index].value ?? "");
}
setHistoryIndex(index);
@ -352,9 +350,9 @@ export function Terminal({ path: startPath, input, setTitle, close: exit, active
onMouseDown={onMouseDown as unknown as MouseEventHandler}
onContextMenu={onContextMenu as unknown as MouseEventHandler}
onClick={(event) => {
if (window.getSelection().toString() === "") {
if (window.getSelection()?.toString() === "") {
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}
onKeyDown={onKeyDown}
onChange={onChange}
inputRef={inputRef}
inputRef={inputRef as unknown as MutableRefObject<HTMLInputElement>}
/>
: <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 { HeaderMenu } from "../_utils/header-menu/HeaderMenu";
import Markdown from "markdown-to-jsx";
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 { MarkdownLink } from "./overrides/MarkdownLink";
import { MarkdownImage } from "./overrides/MarkdownImage";
@ -17,9 +17,9 @@ import { VirtualFile } from "../../../features/virtual-drive/file";
import { WindowProps } from "../../windows/WindowView";
import { DropdownAction } from "../../actions/actions/DropdownAction";
import { ClickAction } from "../../actions/actions/ClickAction";
import ModalsManager from "../../../features/modals/modalsManager";
import App from "../../../features/apps/app";
import WindowsManager from "../../../features/windows/windowsManager";
import { ModalsManager } from "../../../features/modals/modalsManager";
import { App } from "../../../features/apps/app";
import { WindowsManager } from "../../../features/windows/windowsManager";
import { MarkdownBlockquote } from "./overrides/MarkdownBlockquote";
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext";
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) {
const ref = useRef();
const ref = useRef<HTMLDivElement | HTMLTextAreaElement>();
const windowsManager = useWindowsManager();
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 [content, setContent] = useState(file?.content ?? "");
const [unsavedChanges, setUnsavedChanges] = useState(file == null);
@ -76,12 +76,12 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
const iconUrl = currentFile.getIconUrl();
if (iconUrl)
setIconUrl(iconUrl);
setIconUrl?.(iconUrl);
if (newContent?.trim() === "")
setCurrentMode("edit");
} else {
setIconUrl(AppsManager.getAppIconUrl(app.id));
} else if (app != null) {
setIconUrl?.(AppsManager.getAppIconUrl(app.id));
}
setContent(newContent);
@ -90,7 +90,7 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
(ref.current as HTMLElement).scrollTo(0, 0);
}
})();
}, [app.id, currentFile, setIconUrl]);
}, [app?.id, currentFile, setIconUrl]);
useEffect(() => {
// Update title
@ -102,12 +102,12 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
if (currentMode === "view")
label += " (preview)";
setTitle(`${label} ${TITLE_SEPARATOR} ${app.name}`);
}, [currentFile, setTitle, unsavedChanges, currentMode, app.name]);
setTitle?.(app != null ? `${label} ${TITLE_SEPARATOR} ${app.name}` : label);
}, [currentFile, setTitle, unsavedChanges, currentMode, app?.name]);
useEffect(() => {
if (!initialised && currentFile == null && path != null) {
const newFile = virtualRoot.navigate(path);
const newFile = virtualRoot?.navigate(path);
if (newFile == null || !newFile.isFile())
return;
@ -147,7 +147,7 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
return setContent(value);
};
const overrides = {};
const overrides: Record<string, object> = {};
for (const [key, value] of Object.entries(OVERRIDES)) {
overrides[key] = {
component: value,
@ -169,10 +169,10 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
<ClickAction label="Open" onTrigger={() => {
openWindowedModal({
size: DEFAULT_FILE_SELECTOR_SIZE,
Modal: (props) => <FileSelector
Modal: (props: object) => <FileSelector
type={SELECTOR_MODE.SINGLE}
onFinish={(file: VirtualFile) => {
setCurrentFile(file);
onFinish={(file) => {
setCurrentFile(file as VirtualFile);
setUnsavedChanges(false);
}}
{...props}
@ -182,15 +182,15 @@ export function TextEditor({ file, path, setTitle, setIconUrl, close, mode, app,
<Divider/>
<ClickAction label="Save" onTrigger={() => { saveText(); }} shortcut={["Control", "s"]}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} disabled={currentFile == null} onTrigger={() => {
currentFile.parent.open(windowsManager);
if (windowsManager != null) currentFile?.parent?.open(windowsManager);
}}/>
<Divider/>
<ClickAction label="Quit" onTrigger={() => { close(); }} shortcut={["Control", "q"]}/>
<ClickAction label="Quit" onTrigger={() => { close?.(); }} shortcut={["Control", "q"]}/>
</DropdownAction>
<DropdownAction label="View" showOnHover={false}>
<ClickAction label={currentMode === "view" ? "Edit mode" : "Preview mode"} onTrigger={() => {
setCurrentMode(currentMode === "view" ? "edit" : "view");
}} shortcut={["Control", "v"]}/>
}} shortcut={["Control", "u"]}/>
<Divider/>
<ClickAction label="Zoom in" 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>
</HeaderMenu>
{currentMode === "view"
? CODE_FORMATS.includes(currentFile?.extension)
? currentFile?.extension != null && CODE_FORMATS.includes(currentFile?.extension)
? <SyntaxHighlighter
language={EXTENSION_TO_LANGUAGE[currentFile?.extension] ?? currentFile?.extension}
className={styles.Code}
useInlineStyles={false}
showLineNumbers={true}
>{content}</SyntaxHighlighter>
: <div ref={ref} className={styles.View}>
: <div ref={ref as Ref<HTMLDivElement>} className={styles.View}>
{currentFile?.extension === "md"
? <Markdown options={{ overrides }}>{content}</Markdown>
? <Markdown options={{ overrides } as object}>{content}</Markdown>
: <pre><p>{content}</p></pre>
}
</div>
: <textarea
ref={ref}
ref={ref as Ref<HTMLTextAreaElement>}
className={styles.View}
value={content}
onChange={onChange}

View file

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

View file

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

View file

@ -4,16 +4,17 @@ import { useContextMenu } from "../../../../hooks/modals/contextMenu";
import { Actions } from "../../../actions/Actions";
import { ClickAction } from "../../../actions/actions/ClickAction";
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 { 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 { MarkdownProps } from "../TextEditor";
import { sanitizeProps } from "../../../../features/apps/text-editor/_utils/sanitizeProps";
import { copyToClipboard, removeUrlProtocol } from "../../../../features/_utils/browser.utils";
import { TextDisplay } from "../../../actions/actions/TextDisplay";
import styles from "../TextEditor.module.css";
import { ModalProps } from "../../../modals/ModalView";
interface MarkdownLinkProps extends MarkdownProps {
href: string;
@ -27,7 +28,7 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
event.preventDefault();
if (!href.startsWith("http://") && !href.startsWith("https://")) {
const target = currentFile.parent.navigate(href);
const target = currentFile.parent?.navigate(href);
if (target != null) {
if (target.isFile()) {
setCurrentFile(target);
@ -39,22 +40,24 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
iconUrl: AppsManager.getAppIconUrl(app.id),
title: "Failed to open link",
size: new Vector2(450, 150),
Modal: (props) =>
Modal: (props: ModalProps) =>
<DialogBox {...props}>
<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>
});
}
} else {
window.open(href, "_blank").focus();
window.open(href, "_blank")?.focus();
}
};
const { onContextMenu } = useContextMenu({ Actions: (props) =>
<Actions {...props}>
<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={() => {
copyToClipboard(href);
}}/>
@ -62,16 +65,16 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
});
const title = useMemo<string>(() => {
let title: string = null;
let title: string = "";
try {
title = new URL(href).hostname;
} catch (error) {
title = href.split("/").pop();
title = href.split("/").pop() ?? "";
}
return title;
}, [href]);
sanitizeProps(props as MarkdownProps);
sanitizeProps(props);
return <a
{...props}
@ -79,7 +82,7 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
target="_blank"
rel="noreferrer"
href={href}
onContextMenu={onContextMenu}
onContextMenu={onContextMenu as unknown as MouseEventHandler}
onClick={onClick as unknown as MouseEventHandler}
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 { useSettingsManager } from "../../hooks/settings/settingsManagerContext";
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 { reloadViewport } from "../../features/_utils/browser.utils";
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 Vector2 from "../../features/math/vector2";
import { Vector2 } from "../../features/math/vector2";
import { Actions } from "../actions/Actions";
import { ClickAction } from "../actions/actions/ClickAction";
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 { useWindowedModal } from "../../hooks/modals/windowedModal";
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 { VirtualFolderLink } from "../../features/virtual-drive/folder/virtualFolderLink";
import { VirtualFile } from "../../features/virtual-drive/file";
import { TABS } from "../../config/apps/settings.config";
import { ModalProps } from "../modals/ModalView";
export const Desktop = memo(() => {
const settingsManager = useSettingsManager();
const [wallpaper, setWallpaper] = useState<string>(null);
const [wallpaper, setWallpaper] = useState<string | null>(null);
const windowsManager = useWindowsManager();
const virtualRoot = useVirtualRoot();
const [showIcons, setShowIcons] = useState(false);
@ -37,31 +38,31 @@ export const Desktop = memo(() => {
const [iconDirection, setIconDirection] = useState(FALLBACK_ICON_DIRECTION);
const { openWindowedModal } = useWindowedModal();
const directory = virtualRoot.navigate("~/Desktop");
const directory = virtualRoot?.navigate("~/Desktop");
const { onContextMenu, ShortcutsListener } = useContextMenu({ Actions: (props) =>
<Actions {...props}>
<DropdownAction label="View" icon={faEye}>
<RadioAction initialIndex={iconSize} onTrigger={(event, params, value: string) => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("icon-size", value);
<RadioAction initialIndex={iconSize} onTrigger={(event, params, value) => {
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings?.set("icon-size", value as string);
}} options={[
{ label: "Small icons" },
{ label: "Medium icons" },
{ label: "Large icons" },
]}/>
<Divider/>
<RadioAction initialIndex={iconDirection} onTrigger={(event, params, value: string) => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("icon-direction", value);
<RadioAction initialIndex={iconDirection} onTrigger={(event, params, value) => {
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings?.set("icon-direction", value as string);
}} options={[
{ label: "Align vertically" },
{ label: "Align horizontally" },
]}/>
<Divider/>
<ToggleAction label="Show dekstop icons" initialValue={showIcons} onTrigger={() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("show-icons", (!showIcons).toString());
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings?.set("show-icons", (!showIcons).toString());
}}/>
</DropdownAction>
<ClickAction label="Reload" shortcut={["Control", "r"]} icon={faArrowsRotate} onTrigger={() => {
@ -72,7 +73,7 @@ export const Desktop = memo(() => {
shortcut={["F11"]}
icon={!document.fullscreenElement ? faExpand : faCompress}
onTrigger={() => {
if (windowsManager.isAnyFocused())
if (windowsManager?.isAnyFocused())
return;
if (!document.fullscreenElement) {
@ -87,78 +88,78 @@ export const Desktop = memo(() => {
}}
/>
<ClickAction label="Change appearance" icon={faPaintBrush} onTrigger={() => {
windowsManager.open("settings", { tab: TABS.APPEARANCE });
windowsManager?.open("settings", { tab: TABS.APPEARANCE });
}}/>
<Divider/>
<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={() => {
windowsManager.open(APPS.TERMINAL, { path: directory.path });
windowsManager?.open(APPS.TERMINAL, { path: directory?.path });
}}/>
<Divider/>
<ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} onTrigger={() => {
openWindowedModal({
size: new Vector2(350, 350),
Modal: (props) => <Share {...props}/>
Modal: (props: ModalProps) => <Share {...props}/>
});
}}/>
</Actions>
});
const { onContextMenu: onContextMenuFile } = useContextMenu({ Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, file: VirtualFile) => {
file.open(windowsManager);
<ClickAction label="Open" onTrigger={(event, file) => {
if (windowsManager != null) (file as VirtualFile).open(windowsManager);
}}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, file: VirtualFile) => {
file.parent.open(windowsManager);
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, file) => {
if (windowsManager != null) (file as VirtualFile).parent?.open(windowsManager);
}}/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file: VirtualFile) => {
file.delete();
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, file) => {
(file as VirtualFile).delete();
}}/>
</Actions>
});
const { onContextMenu: onContextMenuFolder } = useContextMenu({ Actions: (props) =>
<Actions {...props}>
<ClickAction label="Open" onTrigger={(event, folder: VirtualFolder) => {
folder.open(windowsManager);
<ClickAction label="Open" onTrigger={(event, folder) => {
if (windowsManager != null) (folder as VirtualFolder).open(windowsManager);
}}/>
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={faTerminal} onTrigger={(event, folder: VirtualFolder) => {
windowsManager.open(APPS.TERMINAL, { path: folder.path });
<ClickAction label={`Open in ${APP_NAMES.TERMINAL}`} icon={faTerminal} onTrigger={(event, folder) => {
windowsManager?.open(APPS.TERMINAL, { path: (folder as VirtualFolder).path });
}}/>
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, folder: VirtualFolder) => {
folder.parent.open(windowsManager);
<ClickAction label={`Reveal in ${APP_NAMES.FILE_EXPLORER}`} icon={faFolder} onTrigger={(event, folder) => {
if (windowsManager != null) (folder as VirtualFolder).parent?.open(windowsManager);
}}/>
<Divider/>
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder: VirtualFolder) => {
folder.delete();
<ClickAction label="Delete" icon={faTrash} onTrigger={(event, folder) => {
(folder as VirtualFolder).delete();
}}/>
</Actions>
});
useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.get("wallpaper", setWallpaper);
void settings.get("show-icons", (value) => {
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings?.get("wallpaper", setWallpaper);
void settings?.get("show-icons", (value) => {
if (value != null) {
setShowIcons(value === "true");
} else {
setShowIcons(true);
}
});
void settings.get("icon-size", (value) => {
void settings?.get("icon-size", (value) => {
if (isValidInteger(value))
setIconSize(parseInt(value));
});
void settings.get("icon-direction", (value) => {
void settings?.get("icon-direction", (value) => {
if (isValidInteger(value))
setIconDirection(parseInt(value));
});
}, [settingsManager]);
const onError = () => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.desktop);
void settings.set("wallpaper", FALLBACK_WALLPAPER);
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.desktop);
void settings?.set("wallpaper", FALLBACK_WALLPAPER);
};
const iconScale = 1 + ((isValidInteger(iconSize) ? iconSize : FALLBACK_ICON_SIZE) - 1) / 5;
@ -167,7 +168,7 @@ export const Desktop = memo(() => {
<ShortcutsListener/>
<div
className={styles.Desktop}
onContextMenu={onContextMenu}
onContextMenu={onContextMenu as unknown as MouseEventHandler}
>
{showIcons && <DirectoryList
directory={directory as VirtualFolder}
@ -179,7 +180,7 @@ export const Desktop = memo(() => {
fileClassName={styles["Item"]}
folderClassName={styles["Item"]}
onOpenFile={(event, file) => {
(event as Event).preventDefault();
(event).preventDefault();
const options: Record<string, unknown> = {};
if (file.name === "Info.md")
@ -187,13 +188,15 @@ export const Desktop = memo(() => {
if (file.extension === "md")
options.mode = "view";
windowsManager.openFile(file, options);
windowsManager?.openFile(file, options);
}}
onOpenFolder={(event, { linkedPath, path }: VirtualFolderLink & VirtualFolder) => {
windowsManager.open(APPS.FILE_EXPLORER, { path: linkedPath ?? path });
onOpenFolder={(event, folder) => {
windowsManager?.open(APPS.FILE_EXPLORER, {
path: (folder as VirtualFolderLink).linkedPath ?? folder.path
});
}}
onContextMenuFile={onContextMenuFile}
onContextMenuFolder={onContextMenuFolder}
onContextMenuFile={onContextMenuFile as unknown as FileEventHandler}
onContextMenuFolder={onContextMenuFolder as unknown as FolderEventHandler}
/>}
{wallpaper
? <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 Modal from "../../features/modals/modal";
import OutsideClickListener from "../../hooks/_utils/outsideClick";
import { Modal } from "../../features/modals/modal";
import { OutsideClickListener } from "../../hooks/_utils/outsideClick";
import styles from "./ModalView.module.css";
import { useEffect } from "react";
@ -23,7 +23,7 @@ export const ModalView: FC<ModalProps> = memo(({ modal }) => {
useEffect(() => {
const onDismiss = (event: Event) => {
if ((event as unknown as KeyboardEvent).key === "Escape")
modal.close();
modal?.close();
};
document.addEventListener("keydown", onDismiss);
@ -33,15 +33,19 @@ export const ModalView: FC<ModalProps> = memo(({ modal }) => {
};
}, [modal]);
if (modal?.element == null) return;
const Modal = modal.element;
const Container = () => (<div
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>);
if (modal.dismissible) {
return (<OutsideClickListener onOutsideClick={() => { modal.close(); }}>
return (<OutsideClickListener onOutsideClick={() => { modal?.close(); }}>
<Container/>
</OutsideClickListener>);
} 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 styles from "./ModalsView.module.css";
import { useModals } from "../../hooks/modals/modalsContext";
import { useModalsManager } from "../../hooks/modals/modalsManagerContext";
import Modal from "../../features/modals/modal";
import { Modal } from "../../features/modals/modal";
export const ModalsView = memo(() => {
const ref = useRef(null);
const ref = useRef<HTMLDivElement>(null);
const modals = useModals();
const modalsManager = useModalsManager();
const [sortedModals, setSortedModals] = useState<Modal[]>([]);
// Sort modals
useEffect(() => {
setSortedModals([...modals].sort((modalA, modalB) =>
modalA.lastInteraction - modalB.lastInteraction
));
if (modals != null)
setSortedModals([...modals].sort((modalA, modalB) =>
(modalA.lastInteraction ?? 0) - (modalB.lastInteraction ?? 0)
));
}, [modals]);
useEffect(() => {
if (modalsManager)
modalsManager.containerRef = ref;
if (modalsManager != null)
modalsManager.containerRef = ref as MutableRefObject<HTMLDivElement>;
}, [modalsManager, ref]);
return <div ref={ref} className={styles.ModalsView}>

View file

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

View file

@ -6,11 +6,14 @@ import styles from "./DialogBox.module.css";
export function DialogBox({ modal, params, children, ...props }: ModalProps) {
const onClick = (event: MouseEvent) => {
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) {
case DIALOG_CONTENT_TYPES.CloseButton:
modal.close();
case DIALOG_CONTENT_TYPES.closeButton:
modal?.close();
break;
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
import styles from "./Battery.module.css";
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 {
hideUtilMenus: boolean;
@ -18,7 +31,8 @@ export function Battery({ hideUtilMenus, showUtilMenu }: BatteryProps) {
// const [dischargingTime, setDischargingTime] = useState(0);
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 = () => {
setIsCharging(battery.charging);
};
@ -60,7 +74,7 @@ export function Battery({ hideUtilMenus, showUtilMenu }: BatteryProps) {
}
}, [hideUtilMenus, showMenu]);
const updateShowMenu = (show) => {
const updateShowMenu = (show: boolean) => {
if (show)
showUtilMenu();

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
import styles from "./SearchMenu.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 { ChangeEventHandler, useEffect, useState } from "react";
import { ChangeEventHandler, MutableRefObject, useEffect, useState } from "react";
import { useKeyboardListener } from "../../../hooks/_utils/keyboard";
import App from "../../../features/apps/app";
import { App } from "../../../features/apps/app";
import { ReactSVG } from "react-svg";
interface SearchMenuProps {
@ -12,12 +12,12 @@ interface SearchMenuProps {
setActive: Function;
searchQuery: string;
setSearchQuery: Function;
inputRef: { current: HTMLInputElement | undefined };
inputRef: MutableRefObject<HTMLInputElement>;
}
export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inputRef }: SearchMenuProps) {
const windowsManager = useWindowsManager();
const [apps, setApps] = useState<App[]>(null);
const [apps, setApps] = useState<App[] | null>(null);
const [tabIndex, setTabIndex] = useState(active ? 0 : -1);
useEffect(() => {
@ -25,7 +25,7 @@ export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inp
}, [active]);
useEffect(() => {
if (inputRef.current) {
if (inputRef.current != null) {
inputRef.current.focus();
window.scrollTo(0, document.body.scrollHeight);
}
@ -58,7 +58,8 @@ export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inp
setActive(false);
} else if (event.key === "Enter" && active) {
event.preventDefault();
windowsManager.open(apps[0].id);
if (apps == null) return;
windowsManager?.open(apps[0].id);
setActive(false);
}
};
@ -75,7 +76,7 @@ export function SearchMenu({ active, setActive, searchQuery, setSearchQuery, inp
tabIndex={tabIndex}
onClick={() => {
setActive(false);
windowsManager.open(id);
windowsManager?.open(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 AppsManager from "../../features/apps/appsManager";
import Vector2 from "../../features/math/vector2";
import App from "../../features/apps/app";
import { AppsManager } from "../../features/apps/appsManager";
import { Vector2 } from "../../features/math/vector2";
import { App } from "../../features/apps/app";
export interface WindowFallbackViewProps {
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
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 [alerted, setAlerted] = useState(false);
@ -21,15 +21,17 @@ export default function WindowFallbackView({ error, resetErrorBoundary: _resetEr
return;
setAlerted(true);
closeWindow();
alert({
title: `${app.name} has stopped working`,
text: `${error.name}: ${error.message}`,
iconUrl: AppsManager.getAppIconUrl(app.id),
size: new Vector2(350, 150),
single: true
});
}, [alerted, alert, app.id, app.name, error.message, closeWindow, error.name]);
closeWindow?.();
if (app != null && error != null)
alert({
title: `${app.name} has stopped working`,
text: `${error.name}: ${error.message}`,
iconUrl: AppsManager.getAppIconUrl(app.id),
size: new Vector2(350, 150),
single: true
});
}, [alerted, alert, app?.id, app?.name, error?.message, closeWindow, error?.name]);
return;
}

View file

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

View file

@ -17,9 +17,10 @@ export const WindowsView: FC = memo(() => {
// Sort windows
useEffect(() => {
setSortedWindows([...windows].sort((windowA: WindowOptions, windowB: WindowOptions) =>
windowA.lastInteraction - windowB.lastInteraction
));
if (windows != null)
setSortedWindows([...windows].sort((windowA: WindowOptions, windowB: WindowOptions) =>
(windowA.lastInteraction ?? 0) - (windowB.lastInteraction ?? 0)
));
}, [windows]);
useEffect(() => {
@ -40,7 +41,7 @@ export const WindowsView: FC = memo(() => {
// Launch startup apps
useEffect(() => {
if (windowsManager.startupComplete)
if (windowsManager?.startupComplete)
return;
let startupAppNames: string[] = [];
@ -53,24 +54,24 @@ export const WindowsView: FC = memo(() => {
delete params.app;
// Get list of app names from settings file
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.apps);
void settings.get("startup", (value) => {
const settings = settingsManager?.getSettings(SettingsManager.VIRTUAL_PATHS.apps);
void settings?.get("startup", (value) => {
if (value !== "") {
startupAppNames = value?.split(",").concat(startupAppNames);
startupAppNames = removeDuplicatesFromArray(startupAppNames);
}
windowsManager.startup(startupAppNames, params);
windowsManager?.startup(startupAppNames, params);
});
}, [settingsManager, windowsManager]);
return (<div>
{windows.map((window: WindowProps) => {
{windows?.map((window: WindowProps) => {
const { id, app, size, position, options, minimized, fullscreen } = window;
const index = sortedWindows.indexOf(window);
return <WindowView
key={id}
onInteract={() => { windowsManager.focus(id); }}
onInteract={() => { windowsManager?.focus(id as string); }}
active={index === sortedWindows.length - 1}
id={id}
app={app}
@ -82,7 +83,7 @@ export const WindowsView: FC = memo(() => {
toggleMinimized={(event: Event) => {
event.preventDefault();
event.stopPropagation();
windowsManager.setMinimized(id, !minimized);
windowsManager?.setMinimized(id as string, !minimized);
}}
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",
};
export const APP_NAMES = {
export type AppKey = keyof typeof APPS;
export const APP_NAMES: Record<AppKey, string> = {
TERMINAL: "Commands",
SETTINGS: "Settings",
MEDIA_VIEWER: "Photos",
@ -22,14 +24,15 @@ export const APP_NAMES = {
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.",
SETTINGS: `Configure ${NAME}'s settings and customize your experience.`,
TEXT_EDITOR: "Simple text editor for reading and writing text documents.",
FILE_EXPLORER: "Browse and manage your virtual files.",
CALCULATOR: "Simple calculator app.",
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 = {

View file

@ -1,7 +1,7 @@
import Vector2 from "../features/math/vector2";
import { Vector2 } from "../features/math/vector2";
export const DIALOG_CONTENT_TYPES = {
CloseButton: 0
closeButton: 0
};
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",
1: "Light",
2: "Cherry",

View file

@ -37,8 +37,8 @@ export function setViewportTitle(title: string) {
}
export function setViewportIcon(url: string) {
let link: HTMLLinkElement = document.querySelector("link[rel~='icon']");
if (!link) {
let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']");
if (link == null) {
link = document.createElement("link");
link.rel = "icon";
document.head.appendChild(link);
@ -49,7 +49,7 @@ export function setViewportIcon(url: string) {
export function getViewportParams(): Record<string, string> {
const query = window.location.search.slice(1);
const params = {};
const params: Record<string, string> = {};
query.split("&").forEach((param) => {
// For some reason, URI components only decode when decoded twice
// TO DO: Please find a fix, or create a custom function
@ -61,7 +61,7 @@ export function getViewportParams(): Record<string, string> {
}
interface generateUrlOptions {
appId: string;
appId?: string;
fullscreen?: 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) {
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 {
const specialKeys = [];
const singleKeys = [];
const specialKeys: string[] = [];
const singleKeys: string[] = [];
shortcut.forEach((key) => {
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 { FC } from "react";
export default class App {
name: string;
id: string;
windowContent: React.FC;
export class App {
name: string = "App";
id: string = "app";
windowContent?: FC<JSX.IntrinsicAttributes & WindowProps>;
windowOptions?: {
size: Vector2
size: Vector2;
[key: string]: unknown;
};
isActive: boolean = false;
isPinned?: boolean;
@ -15,11 +16,11 @@ export default class App {
/**
* @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 });
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) => {

View file

@ -1,4 +1,4 @@
import App from "./app";
import { App } from "./app";
import { FileExplorer } from "../../components/apps/file-explorer/FileExplorer";
import { MediaViewer } from "../../components/apps/media-viewer/MediaViewer";
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 { Settings } from "../../components/apps/settings/Settings";
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 { Browser } from "../../components/apps/browser/Browser";
import { IMAGE_FORMATS } from "../../config/apps/mediaViewer.config";
import { LogicSim } from "../../components/apps/logic-sim/LogicSim";
export default class AppsManager {
export class AppsManager {
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.MEDIA_VIEWER, APPS.MEDIA_VIEWER, MediaViewer),
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("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, {
source: "https://prozilla.dev/wordle",
size: new Vector2(400, 650)
@ -55,8 +55,8 @@ export default class AppsManager {
/**
* Get the app associated with a file extension
*/
static getAppByFileExtension(fileExtension: string): App {
let app: App = null;
static getAppByFileExtension(fileExtension: string): App | null {
let app: App | null = null;
if (IMAGE_FORMATS.includes(fileExtension))
return this.getAppById(APPS.MEDIA_VIEWER);

View file

@ -1,5 +1,5 @@
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 { Pin, PinJson } from "../pins/pin";
import { State } from "../_utils/state";
@ -16,19 +16,19 @@ export interface ChipJson {
}
export class Chip {
color: string;
name: string;
color!: string;
name!: string;
position = Vector2.ZERO;
size: Vector2;
circuit: Circuit;
size!: Vector2;
circuit!: Circuit;
isCircuit = false;
isBlueprint = false;
inputCount = 0;
outputCount = 0;
inputPins: Pin[];
outputPins: Pin[];
logic: (inputStates: State[]) => State[];
inputPins!: Pin[];
outputPins!: Pin[];
logic!: (inputStates: State[]) => State[];
constructor(circuit: Circuit | null, name: string, color: string, isBlueprint: boolean, inputCount: number, outputCount: number) {
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 { VirtualFolder } from "../../../virtual-drive/folder";
import { ControlledPin } from "../pins/controlledPin";
import Vector2 from "../../../math/vector2";
import { Vector2 } from "../../../math/vector2";
import { Pin } from "../pins/pin";
import { Wire } from "../wires/wire";
export class ChipsManager {
static CHIPS = {
static CHIPS: Record<string, Chip> = {
AND: new Chip(null, "AND", "blue", true, 2, 1).setLogic((inputStates: State[]) => {
if (inputStates[0].value === 1 && inputStates[1].value === 1) {
return [State.HIGH];
@ -34,8 +34,8 @@ export class ChipsManager {
if (virtualFile == null)
return;
virtualFile.read().then((content) => {
const data = JSON.parse(content) as CircuitJson;
virtualFile.read()?.then((content) => {
const data = JSON.parse(content as string) as CircuitJson;
circuit.color = data.color;
circuit.name = data.name;
@ -84,7 +84,7 @@ export class ChipsManager {
});
// Load logic
newChip.setLogic((ChipsManager.CHIPS[chipData.name] as Chip).logic);
newChip.setLogic((ChipsManager.CHIPS[chipData.name]).logic);
newChip.update();
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 { clamp } from "../../../math/clamp";
import Vector2 from "../../../math/vector2";
import { Vector2 } from "../../../math/vector2";
import { Chip, ChipJson } from "../chips/chip";
import { ControlledPin } from "../pins/controlledPin";
import { InputHandler } from "./inputHandler";
import { Wire, WireJson } from "../wires/wire";
import { clamp } from "../../../_utils/math.utils";
export interface CircuitJson extends ChipJson {
wires: WireJson[];
@ -12,9 +12,9 @@ export interface CircuitJson extends ChipJson {
}
export class Circuit extends Chip {
canvas: HTMLCanvasElement;
canvas!: HTMLCanvasElement;
size = Vector2.ZERO;
context: CanvasRenderingContext2D;
context!: CanvasRenderingContext2D;
colors: { [key: string]: string } = {};
inputHandler: InputHandler;
@ -40,7 +40,7 @@ export class Circuit extends Chip {
init(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.context = this.canvas.getContext("2d");
this.context = this.canvas.getContext("2d") as CanvasRenderingContext2D;
this.resize();
// Detect size changes of canvas

View file

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

View file

@ -1,5 +1,5 @@
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 { Pin } from "./pin";

View file

@ -1,5 +1,5 @@
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 { Circuit } from "../core/circuit";
import { State } from "../_utils/state";
@ -16,13 +16,13 @@ export interface PinJson {
export class Pin {
id: number;
name: string;
name!: string;
position = Vector2.ZERO;
attachedChip: Chip;
circuit: Circuit;
attachedChip!: Chip;
circuit!: Circuit;
state = State.LOW;
isInput: boolean;
isInput!: boolean;
isControlled: boolean = false;
outputWires: Wire[] = [];

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { formatError } from "../_utils/terminal.utils";
import Command from "../command";
import { Command, ExecuteParams } from "../command";
export const cd = new Command()
.setManual({
@ -7,13 +7,14 @@ export const cd = new Command()
usage: "cd [PATH]",
description: "Change working directory to given path (the home directory by default)."
})
.setExecute(function(args, { currentDirectory, setCurrentDirectory }) {
const path = args[0] ?? "~";
.setExecute(function(this: Command, args, params) {
const { currentDirectory, setCurrentDirectory } = params as ExecuteParams;
const path = (args as string[])[0] ?? "~";
const destination = currentDirectory.navigate(path);
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 };
});

View file

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

View file

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

View file

@ -1,13 +1,14 @@
import Command from "../command";
import CommandsManager from "../commands";
import { Command, ExecuteParams } from "../command";
import { CommandsManager } from "../commands";
export const compgen = new Command()
.setManual({
purpose: "Display a list of all commands"
})
.setRequireOptions(true)
.setExecute(function(args, { options }) {
if (options.includes("c")) {
.setExecute(function(args, params) {
const { options } = params as ExecuteParams;
if (options?.includes("c")) {
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 Command from "../command";
import { Command, ExecuteParams } from "../command";
const COW = `
\\ ^__^
@ -15,14 +15,16 @@ export const cowsay = new Command()
usage: "cowsay text",
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
const segments = rawInputValue.split(" ");
const lines = [];
const segments = rawInputValue?.split(" ");
const lines: string[] = [];
let currentLine = "";
let maxLineWidth = 0;
const addLine = (line) => {
const addLine = (line: string) => {
line = line.trimEnd();
lines.push(line);
if (line.length > maxLineWidth)
@ -39,7 +41,7 @@ export const cowsay = new Command()
}
};
segments.forEach((segment) => {
segments?.forEach((segment) => {
// Add empty spaces preceding lines
if (segment === "") {
currentLine += " ";

View file

@ -1,10 +1,11 @@
import Command from "../command";
import { Command, ExecuteParams } from "../command";
export const dir = new Command()
.setManual({
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);
if (folderNames.length === 0)

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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
@ -8,7 +8,7 @@ const FORTUNES = [
"Do not be afraid of competition.",
"An exciting opportunity lies ahead of you.",
"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.",
"Sell your ideas-they have exceptional merit.",
"You should be able to undertake and complete anything.",

View file

@ -1,11 +1,11 @@
import { ANSI } from "../../../../config/apps/terminal.config";
import { formatError } from "../_utils/terminal.utils";
import Command from "../command";
import CommandsManager from "../commands";
import { Command } from "../command";
import { CommandsManager } from "../commands";
export const help = new Command()
.setExecute(function(args) {
if (args.length === 0) {
.setExecute(function(this: Command, args) {
if (args?.length === 0) {
return CommandsManager.COMMANDS.map((command) => {
if (command.manual?.purpose) {
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");
}
const commandName = args[0].toLowerCase();
const commandName = (args as string[])[0].toLowerCase();
const command = CommandsManager.find(commandName);
if (!command)

View file

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

View file

@ -1,6 +1,6 @@
import { ANSI } from "../../../../config/apps/terminal.config";
import { removeAnsi } from "../_utils/terminal.utils";
import Command from "../command";
import { Command, ExecuteParams } from "../command";
const COLUMN_WIDTH = 5;
const ROW_OFFSET = 2;
@ -18,18 +18,20 @@ export const lolcat = new Command()
.setManual({
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");
const offset = timestamp / 100;
rows = rows.map((row, index) => {
const columns = [];
const columns: string[] = [];
const rowIndex = index + offset;
const rowOffset = COLUMN_WIDTH - ((ROW_OFFSET * rowIndex) % COLUMN_WIDTH);
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 color = RAINBOW[rainbowIndex++ % RAINBOW.length];
columns.push(color + column);

View file

@ -1,7 +1,7 @@
import { ANSI } from "../../../../config/apps/terminal.config";
import { VirtualFolder } from "../../../virtual-drive/folder/virtualFolder";
import { formatError } from "../_utils/terminal.utils";
import Command from "../command";
import { Command, ExecuteParams } from "../command";
export const ls = new Command()
.setManual({
@ -9,15 +9,16 @@ export const ls = new Command()
usage: "ls [options] [files]",
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;
if (args.length > 0) {
directory = currentDirectory.navigate(args[0]) as VirtualFolder;
if (args != null && args.length > 0) {
directory = currentDirectory.navigate((args)[0]) as VirtualFolder;
}
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 fileNames = directory.files.map((file) => file.id);

View file

@ -1,9 +1,9 @@
import { formatError } from "../_utils/terminal.utils";
import Command from "../command";
import { Command } from "../command";
export const make = new Command()
.setRequireArgs(true)
.setExecute(function(args) {
if (args[0] === "love")
.setExecute(function(this: Command, args) {
if ((args as string[])[0] === "love")
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