Enabled strict type checking
This commit is contained in:
parent
ea4e274798
commit
88af6bf7ad
157 changed files with 2811 additions and 1085 deletions
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
|
@ -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
1725
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[]; }; }) {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.HeaderMenu {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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"]}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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 ?? ""}/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function MarkdownImage({ alt, src, ...props }: MarkdownImageProps) {
|
|||
return src;
|
||||
}, [src]);
|
||||
|
||||
sanitizeProps(props as MarkdownProps);
|
||||
sanitizeProps(props);
|
||||
|
||||
return <img
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
});
|
||||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
4
src/components/taskbar/indicators/index.ts
Normal file
4
src/components/taskbar/indicators/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { Battery } from "./Battery";
|
||||
export { Calendar } from "./Calendar";
|
||||
export { Network } from "./Network";
|
||||
export { Volume } from "./Volume";
|
||||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)}/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const THEMES = {
|
||||
export const THEMES: Record<number, string | null> = {
|
||||
0: "Dark",
|
||||
1: "Light",
|
||||
2: "Cherry",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
18
src/features/_utils/math.utils.ts
Normal file
18
src/features/_utils/math.utils.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
7
src/features/actions/actionsManager.ts
Normal file
7
src/features/actions/actionsManager.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export class ActionsManager {
|
||||
static MODES = {
|
||||
contextMenu: "ContextMenu",
|
||||
shortcutsListener: "ShortcutsListener",
|
||||
headerMenu: "HeaderMenu",
|
||||
};
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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[]) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
|
|
@ -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 += " ";
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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 };
|
||||
});
|
||||
|
|
@ -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 set…confidence 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.",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue