Improved responsiveness + general maintenance

This commit is contained in:
Prozilla 2024-06-13 07:26:16 +02:00
parent 3bcb70ab95
commit 750a758634
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
17 changed files with 220 additions and 104 deletions

View file

@ -15,7 +15,7 @@ echo -e "Commit message: \e[0;36m$COMMIT_MESSAGE\e[0m"
echo -e "Repository: \e[0;36m$REPO_URL\e[0m" echo -e "Repository: \e[0;36m$REPO_URL\e[0m"
echo "" echo ""
if gh-pages -x -d dist -m "$COMMIT_MESSAGE" -r "$REPO_URL" ; then if gh-pages -d dist -m "$COMMIT_MESSAGE" -r "$REPO_URL" ; then
echo -e "\e[0;32m✓ Successfully deployed to \e[0;36mhttps://$domain/\e[0m" echo -e "\e[0;32m✓ Successfully deployed to \e[0;36mhttps://$domain/\e[0m"
else else
echo -e "\e[0;31mFailed to deploy\e[0m" echo -e "\e[0;31mFailed to deploy\e[0m"

View file

@ -12,9 +12,11 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=4"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=4">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4">
<link rel="icon" href="/favicon.ico?v=5" sizes="any">
<link rel="icon" href="/icon.svg?v=4" type="image/svg+xml">
<link rel="icon" href="/icon.png" type="image/png">
<link rel="manifest" href="/site.webmanifest?v=4"> <link rel="manifest" href="/site.webmanifest?v=4">
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#4cdfff"> <link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#4cdfff">
<link rel="shortcut icon" type="image/x-icon" href="https://os.prozilla.dev/favicon.ico?">
<meta name="apple-mobile-web-app-title" content="ProzillaOS"> <meta name="apple-mobile-web-app-title" content="ProzillaOS">
<meta name="application-name" content="ProzillaOS"> <meta name="application-name" content="ProzillaOS">
<meta name="msapplication-TileColor" content="#0d1114"> <meta name="msapplication-TileColor" content="#0d1114">
@ -44,6 +46,14 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="translucent-black"> <meta name="apple-mobile-web-app-status-bar-style" content="translucent-black">
<script>
// Change theme color to black if app is running as PWA
const isPWA = (window.navigator.standalone === true) || (window.matchMedia("(display-mode: standalone)").matches)
if (isPWA) {
const themeColor = document.querySelector("meta[name='theme-color']");
themeColor?.setAttribute("content", "#0D1114");
}
</script>
<!-- FAQ --> <!-- FAQ -->
<script type="application/ld+json"> <script type="application/ld+json">

View file

@ -13,7 +13,7 @@
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#0D1114",
"background_color": "#ffffff", "background_color": "#0D1114",
"display": "standalone" "display": "standalone"
} }

View file

@ -145,6 +145,15 @@
margin: 0.5rem auto; margin: 0.5rem auto;
} }
.ContextMenu .TextDisplay {
margin: 0;
padding: 0.25rem 0.5rem;
color: var(--foreground-color-1);
font-size: 0.875rem;
text-align: start;
white-space: nowrap;
}
.Header-menu { .Header-menu {
display: flex; display: flex;
width: inherit; width: inherit;

View file

@ -0,0 +1,7 @@
import styles from "../Actions.module.css";
export function TextDisplay({ children }) {
return <p className={styles.TextDisplay}>
{children}
</p>;
}

View file

@ -13,6 +13,7 @@ import { WindowProps } from "../../windows/WindowView";
import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../../features/virtual-drive/folder/virtualFolder";
import { CommandResponse } from "../../../features/apps/terminal/command"; import { CommandResponse } from "../../../features/apps/terminal/command";
import { APP_NAMES } from "../../../config/apps.config"; import { APP_NAMES } from "../../../config/apps.config";
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext";
interface TerminalProps extends WindowProps { interface TerminalProps extends WindowProps {
startPath: string; startPath: string;
@ -26,7 +27,7 @@ interface HistoryEntry {
clear?: boolean; clear?: boolean;
} }
export function Terminal({ startPath, input, setTitle, close: exit, active }: TerminalProps) { export function Terminal({ startPath, input, setTitle, close: exit, active, focus }: TerminalProps) {
const [inputKey, setInputKey] = useState(0); const [inputKey, setInputKey] = useState(0);
const [inputValue, setInputValue] = useState(input ?? ""); const [inputValue, setInputValue] = useState(input ?? "");
const [history, setHistory] = useState<HistoryEntry[]>([{ const [history, setHistory] = useState<HistoryEntry[]>([{
@ -39,21 +40,14 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
const [historyIndex, setHistoryIndex] = useState(0); const [historyIndex, setHistoryIndex] = useState(0);
const [stream, setStream] = useState<Stream>(null); const [stream, setStream] = useState<Stream>(null);
const [streamOutput, setStreamOutput] = useState<string>(null); const [streamOutput, setStreamOutput] = useState<string>(null);
const streamRef = useRef(null); const ref = useRef(null);
const [streamFocused, setStreamFocused] = useState(false); const [streamFocused, setStreamFocused] = useState(false);
const settingsManager = useSettingsManager();
useEffect(() => { useEffect(() => {
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`); setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
}, [currentDirectory.path, currentDirectory.root, setTitle]); }, [currentDirectory.path, currentDirectory.root, setTitle]);
useEffect(() => {
if (streamFocused || streamRef.current == null || streamOutput == null)
return;
(streamRef.current as unknown as HTMLDivElement).scrollTop = (streamRef.current as unknown as HTMLDivElement).scrollHeight;
setStreamFocused(true);
}, [streamFocused, streamOutput, streamRef]);
useEffect(() => { useEffect(() => {
if (!inputRef.current || !active) if (!inputRef.current || !active)
return; return;
@ -61,6 +55,25 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
(inputRef.current as unknown as HTMLInputElement).focus(); (inputRef.current as unknown as HTMLInputElement).focus();
}, [inputRef, active]); }, [inputRef, active]);
const scrollDown = () => {
(ref.current as unknown as HTMLDivElement).scrollTop = (ref.current as unknown as HTMLDivElement).scrollHeight;
};
useEffect(() => {
if (streamFocused || ref.current == null || streamOutput == null)
return;
scrollDown();
setStreamFocused(true);
}, [streamFocused, streamOutput, ref]);
useEffect(() => {
if (ref.current == null || stream != null)
return;
scrollDown();
}, [inputValue]);
const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:` const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:`
+ `${ANSI.fg.blue + (currentDirectory.root ? "/" : currentDirectory.path) + ANSI.reset}$ `; + `${ANSI.fg.blue + (currentDirectory.root ? "/" : currentDirectory.path) + ANSI.reset}$ `;
@ -90,23 +103,26 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
let lastOutput: CommandResponse = null; let lastOutput: CommandResponse = null;
stream.on(Stream.EVENT_NAMES.new, (text: string) => { stream.on(Stream.EVENT_NAMES.new, (text: string) => {
let output: CommandResponse = text; void (async () => {
pipes.forEach((pipe) => { let output: CommandResponse = text;
if (output instanceof Stream)
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 as string}` : pipe);
}
if ((output as unknown) instanceof Stream) {
stream.stop();
promptOutput(ANSI.fg.red + "Stream failed");
return; return;
}
// Output from the previous command gets added as an argument for the next command
output = handleInput(output ? `${pipe} ${output as string}` : pipe);
});
if ((output as unknown) instanceof Stream) { lastOutput = output;
stream.stop(); setStreamOutput(output as string);
promptOutput(ANSI.fg.red + "Stream failed"); })();
return;
}
lastOutput = output;
setStreamOutput(output);
}); });
stream.on(Stream.EVENT_NAMES.stop, () => { stream.on(Stream.EVENT_NAMES.stop, () => {
@ -121,7 +137,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
}; };
const handleInput = (value: string): CommandResponse => { const handleInput = async (value: string): Promise<CommandResponse> => {
const rawInputValueStart = value.indexOf(" ") + 1; const rawInputValueStart = value.indexOf(" ") + 1;
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart); const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
const timestamp = Date.now(); const timestamp = Date.now();
@ -187,7 +203,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
let response: CommandResponse = null; let response: CommandResponse = null;
try { try {
response = command.execute(args, { response = await command.execute(args, {
promptOutput, promptOutput,
pushHistory, pushHistory,
virtualRoot, virtualRoot,
@ -199,7 +215,8 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
options, options,
exit, exit,
inputs, inputs,
timestamp timestamp,
settingsManager,
}); });
if (response == null) if (response == null)
@ -213,29 +230,33 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
} }
}; };
const submitInput = (value: string) => { const resetInput = () => {
setInputValue("");
setHistoryIndex(0);
};
const submitInput = async (value: string) => {
pushHistory({ pushHistory({
text: prefix + value, text: prefix + value,
isInput: true, isInput: true,
value value
}); });
setInputValue("");
setHistoryIndex(0);
// Piping is used to chain commands // Piping is used to chain commands
let pipes = value.split(" | "); let pipes = value.split(" | ");
const completedPipes = []; const completedPipes = [];
let output = null; let output = null;
pipes.forEach((pipe) => { for (const pipe of pipes) {
if (output instanceof Stream) if (output instanceof Stream)
return; continue;
// Output from the previous command gets added as an argument for the next command // Output from the previous command gets added as an argument for the next command
output = handleInput(output ? `${pipe} ${output}` : pipe); output = await handleInput(output ? `${pipe} ${output}` : pipe);
completedPipes.push(pipe); completedPipes.push(pipe);
}); }
resetInput();
pipes = pipes.filter((pipe) => !completedPipes.includes(pipe)); pipes = pipes.filter((pipe) => !completedPipes.includes(pipe));
@ -274,7 +295,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
const { key } = event; const { key } = event;
if (key === "Enter") { if (key === "Enter") {
submitInput(value); void submitInput(value);
setInputKey((previousKey) => previousKey + 1); setInputKey((previousKey) => previousKey + 1);
} else if (key === "ArrowUp") { } else if (key === "ArrowUp") {
event.preventDefault(); event.preventDefault();
@ -306,7 +327,9 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
}); });
}; };
const onMouseDown = (event: { button: number; preventDefault: () => void; }) => { const onMouseDown = (event: MouseEvent) => {
focus(event);
if (event.button === 2) { if (event.button === 2) {
event.preventDefault(); event.preventDefault();
@ -324,9 +347,9 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
return ( return (
<div <div
ref={streamRef} ref={ref}
className={styles.Terminal} className={styles.Terminal}
onMouseDown={onMouseDown} onMouseDown={onMouseDown as unknown as MouseEventHandler}
onContextMenu={onContextMenu as unknown as MouseEventHandler} onContextMenu={onContextMenu as unknown as MouseEventHandler}
onClick={(event) => { onClick={(event) => {
if (window.getSelection().toString() === "") { if (window.getSelection().toString() === "") {

View file

@ -17,12 +17,23 @@ import { VirtualFile } from "../../../features/virtual-drive/file";
import { WindowProps } from "../../windows/WindowView"; import { WindowProps } from "../../windows/WindowView";
import { DropdownAction } from "../../actions/actions/DropdownAction"; import { DropdownAction } from "../../actions/actions/DropdownAction";
import { ClickAction } from "../../actions/actions/ClickAction"; import { ClickAction } from "../../actions/actions/ClickAction";
import ModalsManager from "../../../features/modals/modalsManager";
import App from "../../../features/apps/app";
import WindowsManager from "../../../features/windows/windowsManager";
const OVERRIDES = { const OVERRIDES = {
a: MarkdownLink, a: MarkdownLink,
img: MarkdownImage, img: MarkdownImage,
}; };
export interface MarkdownProps {
modalsManager: ModalsManager;
setCurrentFile: Function;
currentFile: VirtualFile;
app: App;
windowsManager: WindowsManager;
}
interface TextEditorProps extends WindowProps { interface TextEditorProps extends WindowProps {
file?: VirtualFile; file?: VirtualFile;
} }
@ -121,7 +132,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
currentFile, currentFile,
app, app,
windowsManager windowsManager
} } as MarkdownProps
}; };
} }

View file

@ -1,6 +1,8 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { MarkdownProps } from "../TextEditor";
import { sanitizeProps } from "../../../../features/apps/text-editor/_utils/sanitizeProps";
interface MarkdownImageProps { interface MarkdownImageProps extends MarkdownProps {
src: string; src: string;
alt: string alt: string
} }
@ -14,6 +16,8 @@ export function MarkdownImage({ alt, src, ...props }: MarkdownImageProps) {
return src; return src;
}, [src]); }, [src]);
sanitizeProps(props as MarkdownProps);
return <img return <img
alt={alt} alt={alt}
{...props} {...props}

View file

@ -1,25 +1,22 @@
import { MouseEventHandler, ReactNode, useMemo } from "react"; import { MouseEventHandler, ReactNode, useMemo } from "react";
import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { faClipboard, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { useContextMenu } from "../../../../hooks/modals/contextMenu"; import { useContextMenu } from "../../../../hooks/modals/contextMenu";
import { Actions } from "../../../actions/Actions"; import { Actions } from "../../../actions/Actions";
import { ClickAction } from "../../../actions/actions/ClickAction"; import { ClickAction } from "../../../actions/actions/ClickAction";
import { VirtualFile } from "../../../../features/virtual-drive/file";
import { useWindowedModal } from "../../../../hooks/modals/windowedModal"; import { useWindowedModal } from "../../../../hooks/modals/windowedModal";
import AppsManager from "../../../../features/apps/appsManager"; import AppsManager from "../../../../features/apps/appsManager";
import App from "../../../../features/apps/app";
import { DialogBox } from "../../../modals/dialog-box/DialogBox"; import { DialogBox } from "../../../modals/dialog-box/DialogBox";
import { DIALOG_CONTENT_TYPES } from "../../../../config/modals.config"; import { DIALOG_CONTENT_TYPES } from "../../../../config/modals.config";
import Vector2 from "../../../../features/math/vector2"; import Vector2 from "../../../../features/math/vector2";
import WindowsManager from "../../../../features/windows/windowsManager";
import { APPS } from "../../../../config/apps.config"; 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";
interface MarkdownLinkProps { interface MarkdownLinkProps extends MarkdownProps {
href: string; href: string;
children: ReactNode; children: ReactNode;
windowsManager: WindowsManager;
setCurrentFile: Function;
currentFile: VirtualFile;
app: App;
} }
export function MarkdownLink({ href, children, windowsManager, currentFile, setCurrentFile, app, ...props }: MarkdownLinkProps) { export function MarkdownLink({ href, children, windowsManager, currentFile, setCurrentFile, app, ...props }: MarkdownLinkProps) {
@ -55,7 +52,11 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
const { onContextMenu } = useContextMenu({ Actions: (props) => const { onContextMenu } = useContextMenu({ Actions: (props) =>
<Actions {...props}> <Actions {...props}>
<TextDisplay>{removeUrlProtocol(href)}</TextDisplay>
<ClickAction label="Open link" icon={faExternalLink} onTrigger={onClick}/> <ClickAction label="Open link" icon={faExternalLink} onTrigger={onClick}/>
<ClickAction label="Copy link" icon={faClipboard} onTrigger={() => {
copyToClipboard(href);
}}/>
</Actions> </Actions>
}); });
@ -69,6 +70,8 @@ export function MarkdownLink({ href, children, windowsManager, currentFile, setC
return title; return title;
}, [href]); }, [href]);
sanitizeProps(props as MarkdownProps);
return <a return <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"

View file

@ -24,14 +24,15 @@ import { Share } from "../modals/share/Share";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import WindowFallbackView from "./WindowFallbackView"; import WindowFallbackView from "./WindowFallbackView";
import { WindowOptions } from "../../features/windows/windowsManager"; import { WindowOptions } from "../../features/windows/windowsManager";
import { MIN_SCREEN_HEIGHT, MIN_SCREEN_WIDTH } from "../../config/windows.config";
export interface WindowProps extends WindowOptions { export interface WindowProps extends WindowOptions {
fullscreen?: boolean; fullscreen?: boolean;
onInteract?: Function onInteract?: Function
setTitle?: Function; setTitle?: React.Dispatch<React.SetStateAction<string>>;
setIconUrl?: Function; setIconUrl?: React.Dispatch<React.SetStateAction<string>>;
close?: Function; close?: (event?: Event) => void;
focus?: Function; focus?: (event: Event, force?: boolean) => void;
active?: boolean; active?: boolean;
minimized?: boolean; minimized?: boolean;
toggleMinimized?: Function; toggleMinimized?: Function;
@ -44,7 +45,6 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
const nodeRef = useRef(null); const nodeRef = useRef(null);
const { openWindowedModal } = useWindowedModal(); const { openWindowedModal } = useWindowedModal();
const [startSize, setStartSize] = useState(size);
const [startPosition, setStartPosition] = useState(position); const [startPosition, setStartPosition] = useState(position);
const [maximized, setMaximized] = useState(fullscreen ?? false); const [maximized, setMaximized] = useState(fullscreen ?? false);
const [screenWidth, screenHeight] = useScreenDimensions(); const [screenWidth, screenHeight] = useScreenDimensions();
@ -80,9 +80,7 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
if (screenWidth == null || screenHeight == null) if (screenWidth == null || screenHeight == null)
return; return;
if (size.x > screenWidth || size.y > screenHeight) { if (screenWidth < MIN_SCREEN_WIDTH || screenHeight < MIN_SCREEN_HEIGHT) {
setStartSize(new Vector2(screenWidth - 32, screenHeight - 32));
setStartPosition(new Vector2(16, 16));
setMaximized(true); setMaximized(true);
} else { } else {
if (position.x > screenWidth) if (position.x > screenWidth)
@ -110,12 +108,12 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
}; };
}, [active, minimized, iconUrl, title]); }, [active, minimized, iconUrl, title]);
const close = (event?: Event) => { const close: WindowProps["close"] = (event) => {
event?.preventDefault(); event?.preventDefault();
windowsManager.close(id); windowsManager.close(id);
}; };
const focus = (event: Event, force = false): void => { const focus: WindowProps["focus"] = (event, force = false) => {
if (force) { if (force) {
onInteract(); onInteract();
return; return;
@ -147,7 +145,7 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
bounds={{ bounds={{
top: 0, top: 0,
bottom: screenHeight - 55, bottom: screenHeight - 55,
left: -startSize.x + 85, left: -size.x + 85,
right: screenWidth - 5 right: screenWidth - 5
}} }}
cancel="button" cancel="button"
@ -164,8 +162,8 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
<div <div
className={styles["Window-inner"]} className={styles["Window-inner"]}
style={{ style={{
width: maximized ? null : startSize.x, width: maximized ? null : size.x,
height: maximized ? null : startSize.y, height: maximized ? null : size.y,
}} }}
> >
<div className={`${styles.Header} Window-handle`} onContextMenu={onContextMenu} onDoubleClick={(event) => { <div className={`${styles.Header} Window-handle`} onContextMenu={onContextMenu} onDoubleClick={(event) => {
@ -182,15 +180,18 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
> >
<FontAwesomeIcon icon={faMinus}/> <FontAwesomeIcon icon={faMinus}/>
</button> </button>
<button aria-label="Maximize" className={styles["Header-button"]} tabIndex={0} id="maximize-window" {screenWidth > MIN_SCREEN_WIDTH && screenHeight > MIN_SCREEN_HEIGHT
onClick={(event) => { ? <button aria-label="Maximize" className={styles["Header-button"]} tabIndex={0} id="maximize-window"
event.preventDefault(); onClick={(event) => {
setMaximized(!maximized); event.preventDefault();
focus(event as unknown as Event, true); setMaximized(!maximized);
}} focus(event as unknown as Event, true);
> }}
<FontAwesomeIcon icon={maximized ? fasWindowMaximize : faWindowMaximize}/> >
</button> <FontAwesomeIcon icon={maximized ? fasWindowMaximize : faWindowMaximize}/>
</button>
: null
}
<button aria-label="Close" className={`${styles["Header-button"]} ${styles["Exit-button"]}`} tabIndex={0} id="close-window" <button aria-label="Close" className={`${styles["Header-button"]} ${styles["Exit-button"]}`} tabIndex={0} id="close-window"
onClick={close as unknown as MouseEventHandler}> onClick={close as unknown as MouseEventHandler}>
<FontAwesomeIcon icon={faXmark}/> <FontAwesomeIcon icon={faXmark}/>

View file

@ -1,2 +1,6 @@
export const SCREEN_MARGIN = 32; export const SCREEN_MARGIN = 32;
export const TITLE_SEPARATOR = "-"; export const TITLE_SEPARATOR = "-";
// If the user's screen is smaller than these values, windows will always be maximized
export const MIN_SCREEN_WIDTH = 350;
export const MIN_SCREEN_HEIGHT = 350;

View file

@ -102,6 +102,10 @@ export function openUrl(url: string, target?: HTMLAttributeAnchorTarget) {
window.open(url, target ?? "_blank"); window.open(url, target ?? "_blank");
} }
export function copyToClipboard(string: string, onSuccess: (value: void) => void, onFail: (value: void) => void) { export function removeUrlProtocol(url: string) {
return url.replace(/^https?:\/\/|\/$/g, "");
}
export function copyToClipboard(string: string, onSuccess?: (value: void) => void, onFail?: (value: void) => void) {
navigator.clipboard.writeText(string).then(onSuccess, onFail); navigator.clipboard.writeText(string).then(onSuccess, onFail);
} }

View file

@ -1,3 +1,4 @@
import { SettingsManager } from "../../settings/settingsManager";
import { VirtualFolder } from "../../virtual-drive/folder/virtualFolder"; import { VirtualFolder } from "../../virtual-drive/folder/virtualFolder";
import { VirtualRoot } from "../../virtual-drive/root/virtualRoot"; import { VirtualRoot } from "../../virtual-drive/root/virtualRoot";
import Stream from "./stream"; import Stream from "./stream";
@ -21,9 +22,10 @@ type Execute = (args?: string[], options?: {
rawInputValue?: string, rawInputValue?: string,
options?: string[], options?: string[],
exit?: Function, exit?: Function,
inputs?: Record<string, string>; inputs?: Record<string, string>,
timestamp: number, timestamp: number,
}) => CommandResponse; settingsManager: SettingsManager,
}) => CommandResponse | Promise<CommandResponse>;
type Manual = { type Manual = {
purpose?: string, purpose?: string,

View file

@ -22,9 +22,7 @@ type Particle = {
size: number; size: number;
}; };
let particles: Particle[] = []; function generateScreen(frame: number, screen: string[][], particles: Particle[]): string {
function generateScreen(frame: number): string {
// Spawn new particles // Spawn new particles
if (frame % PARTICLES.framesBetweenSpawn) { if (frame % PARTICLES.framesBetweenSpawn) {
const newParticle: Particle = { const newParticle: Particle = {
@ -34,20 +32,19 @@ function generateScreen(frame: number): string {
particles.push(newParticle); particles.push(newParticle);
} }
// Create screen
const screen: string[][] = [];
for (let y = 0; y < SCREEN_HEIGHT; y++) {
const row: string[] = [];
for (let x = 0; x < SCREEN_WIDTH; x++) {
row.push(" ");
}
screen.push(row);
}
// Move and render particles // Move and render particles
particles.forEach((particle) => { particles.forEach((particle) => {
particle.position.y -= PARTICLES.fallSpeed; particle.position.y -= PARTICLES.fallSpeed;
// Clean previous render
for (let i = 0; i < PARTICLES.fallSpeed; i++) {
const positionX = particle.position.x;
const positionY = particle.position.y + i + particle.size;
if (positionY < SCREEN_HEIGHT && positionY >= 0)
screen[positionY][positionX] = " ";
}
// Remove offscreen particles // Remove offscreen particles
if (particle.position.y + particle.size <= 0 || particle.position.x >= SCREEN_WIDTH) if (particle.position.y + particle.size <= 0 || particle.position.x >= SCREEN_WIDTH)
return removeFromArray(particle, particles); return removeFromArray(particle, particles);
@ -62,13 +59,13 @@ function generateScreen(frame: number): string {
const positionX = particle.position.x; const positionX = particle.position.x;
const positionY = particle.position.y + i; const positionY = particle.position.y + i;
if (positionX < SCREEN_WIDTH && positionY < SCREEN_HEIGHT && positionY > 0) { if (positionX < SCREEN_WIDTH && positionY < SCREEN_HEIGHT && positionY >= 0) {
screen[positionY][positionX] = color + character + ANSI.reset; screen[positionY][positionX] = color + character + ANSI.reset;
} }
} }
}); });
return screen.map((row) => row.join("")).reverse().join("\n"); return [...screen.map((row) => row.join(""))].reverse().join("\n");
} }
export const cmatrix = new Command() export const cmatrix = new Command()
@ -78,11 +75,21 @@ export const cmatrix = new Command()
}) })
.setExecute(function() { .setExecute(function() {
const stream = new Stream(); const stream = new Stream();
particles = []; const particles: Particle[] = [];
// Create screen
const screen: string[][] = [];
for (let y = 0; y < SCREEN_HEIGHT; y++) {
const row: string[] = [];
for (let x = 0; x < SCREEN_WIDTH; x++) {
row.push(" ");
}
screen.push(row);
}
let frame = 0; let frame = 0;
const interval = setInterval(() => { const interval = setInterval(() => {
const text = generateScreen(frame); const text = generateScreen(frame, screen, particles);
stream.send(text); stream.send(text);
frame++; frame++;
}, 100 / ANIMATION_SPEED); }, 100 / ANIMATION_SPEED);

View file

@ -1,7 +1,9 @@
import { APPS } from "../../../../config/apps.config"; import { APPS } from "../../../../config/apps.config";
import { ANSI } from "../../../../config/apps/terminal.config"; import { ANSI } from "../../../../config/apps/terminal.config";
import { ANSI_ASCII_LOGO, ANSI_LOGO_COLOR, NAME } from "../../../../config/branding.config"; import { ANSI_ASCII_LOGO, ANSI_LOGO_COLOR, NAME } from "../../../../config/branding.config";
import { THEMES } from "../../../../config/themes.config";
import { TimeManager } from "../../../_utils/time.utils"; import { TimeManager } from "../../../_utils/time.utils";
import { SettingsManager } from "../../../settings/settingsManager";
import AppsManager from "../../appsManager"; import AppsManager from "../../appsManager";
import Command from "../command"; import Command from "../command";
@ -9,14 +11,17 @@ export const neofetch = new Command()
.setManual({ .setManual({
purpose: "Fetch system information" purpose: "Fetch system information"
}) })
.setExecute(function(args, { username, hostname }) { .setExecute(async function(args, { username, hostname, settingsManager }) {
const leftColumn = ANSI_ASCII_LOGO.split("\n"); const leftColumn = ANSI_ASCII_LOGO.split("\n");
const rightColumnWidth = username.length + hostname.length + 1; const rightColumnWidth = username.length + hostname.length + 1;
const themeIndex = await settingsManager.get(SettingsManager.VIRTUAL_PATHS.theme).get("theme");
const theme = THEMES[themeIndex ?? 0] as string;
const userAgent = navigator.userAgent; const userAgent = navigator.userAgent;
// Check for the browser name using regular expressions // Check for the browser name using regular expressions
let browserName; let browserName: string;
if (userAgent.match(/Firefox\//)) { if (userAgent.match(/Firefox\//)) {
browserName = "Mozilla Firefox"; browserName = "Mozilla Firefox";
} else if (userAgent.match(/Edg\//)) { } else if (userAgent.match(/Edg\//)) {
@ -29,7 +34,7 @@ export const neofetch = new Command()
browserName = "Unknown"; browserName = "Unknown";
} }
const formatLine = (label, text) => ANSI.fg.cyan + label.toUpperCase() + ANSI.reset + ": " + text; const formatLine = (label: string, text: string) => ANSI.fg.cyan + label.toUpperCase() + ANSI.reset + ": " + text;
const rightColumn = [ const rightColumn = [
`${ANSI.fg.cyan + username + ANSI.reset}@${ANSI.fg.cyan + hostname + ANSI.reset}`, `${ANSI.fg.cyan + username + ANSI.reset}@${ANSI.fg.cyan + hostname + ANSI.reset}`,
@ -37,7 +42,7 @@ export const neofetch = new Command()
formatLine("os", NAME), formatLine("os", NAME),
formatLine("uptime", TimeManager.getUptime(2)), formatLine("uptime", TimeManager.getUptime(2)),
formatLine("resolution", window.innerWidth + "x" + window.innerHeight), formatLine("resolution", window.innerWidth + "x" + window.innerHeight),
formatLine("theme", "default"), formatLine("theme", theme),
formatLine("icons", "Font Awesome"), formatLine("icons", "Font Awesome"),
formatLine("terminal", AppsManager.getAppById(APPS.TERMINAL)?.name ?? "Unknown"), formatLine("terminal", AppsManager.getAppById(APPS.TERMINAL)?.name ?? "Unknown"),
formatLine("browser", browserName), formatLine("browser", browserName),

View file

@ -0,0 +1,9 @@
import { MarkdownProps } from "../../../../components/apps/text-editor/TextEditor";
export function sanitizeProps(props: MarkdownProps) {
delete props.modalsManager;
delete props.setCurrentFile;
delete props.currentFile;
delete props.app;
delete props.windowsManager;
}

View file

@ -40,10 +40,27 @@ export default class WindowsManager {
} }
const size = options?.size ?? app.windowOptions?.size ?? new Vector2(700, 400); const size = options?.size ?? app.windowOptions?.size ?? new Vector2(700, 400);
const position = new Vector2(randomRange(SCREEN_MARGIN, window.innerWidth - size.x - SCREEN_MARGIN),
randomRange(SCREEN_MARGIN, window.innerHeight - size.y - SCREEN_MARGIN - TASKBAR_HEIGHT)); const availableScreenSpace = new Vector2(
window.innerWidth - SCREEN_MARGIN * 2,
window.innerHeight - SCREEN_MARGIN * 2 - TASKBAR_HEIGHT
);
let fullscreen = false; let fullscreen = false;
if (size.x > availableScreenSpace.x) {
size.x = availableScreenSpace.x;
fullscreen = true;
} else if (size.y > availableScreenSpace.y) {
size.y = availableScreenSpace.y;
fullscreen = true;
}
const position = new Vector2(
SCREEN_MARGIN + randomRange(0, availableScreenSpace.x - size.x),
SCREEN_MARGIN + randomRange(0, availableScreenSpace.y - size.y)
);
if (options?.fullscreen) { if (options?.fullscreen) {
if (typeof(options.fullscreen) == "string") { if (typeof(options.fullscreen) == "string") {
fullscreen = options.fullscreen.toLowerCase() === "true"; fullscreen = options.fullscreen.toLowerCase() === "true";