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 ""
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"
else
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="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" 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="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="application-name" content="ProzillaOS">
<meta name="msapplication-TileColor" content="#0d1114">
@ -44,6 +46,14 @@
<meta name="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">
<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 -->
<script type="application/ld+json">

View file

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

View file

@ -145,6 +145,15 @@
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 {
display: flex;
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 { CommandResponse } from "../../../features/apps/terminal/command";
import { APP_NAMES } from "../../../config/apps.config";
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext";
interface TerminalProps extends WindowProps {
startPath: string;
@ -26,7 +27,7 @@ interface HistoryEntry {
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 [inputValue, setInputValue] = useState(input ?? "");
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 [stream, setStream] = useState<Stream>(null);
const [streamOutput, setStreamOutput] = useState<string>(null);
const streamRef = useRef(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]);
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(() => {
if (!inputRef.current || !active)
return;
@ -61,6 +55,25 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
(inputRef.current as unknown as HTMLInputElement).focus();
}, [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}:`
+ `${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;
stream.on(Stream.EVENT_NAMES.new, (text: string) => {
let output: CommandResponse = text;
pipes.forEach((pipe) => {
if (output instanceof Stream)
void (async () => {
let output: CommandResponse = text;
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;
// 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) {
stream.stop();
promptOutput(ANSI.fg.red + "Stream failed");
return;
}
lastOutput = output;
setStreamOutput(output);
lastOutput = output;
setStreamOutput(output as string);
})();
});
stream.on(Stream.EVENT_NAMES.stop, () => {
@ -121,7 +137,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
document.addEventListener("keydown", onKeyDown);
};
const handleInput = (value: string): CommandResponse => {
const handleInput = async (value: string): Promise<CommandResponse> => {
const rawInputValueStart = value.indexOf(" ") + 1;
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
const timestamp = Date.now();
@ -187,7 +203,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
let response: CommandResponse = null;
try {
response = command.execute(args, {
response = await command.execute(args, {
promptOutput,
pushHistory,
virtualRoot,
@ -199,7 +215,8 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
options,
exit,
inputs,
timestamp
timestamp,
settingsManager,
});
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({
text: prefix + value,
isInput: true,
value
});
setInputValue("");
setHistoryIndex(0);
// Piping is used to chain commands
let pipes = value.split(" | ");
const completedPipes = [];
let output = null;
pipes.forEach((pipe) => {
for (const pipe of pipes) {
if (output instanceof Stream)
return;
continue;
// 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);
});
}
resetInput();
pipes = pipes.filter((pipe) => !completedPipes.includes(pipe));
@ -274,7 +295,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
const { key } = event;
if (key === "Enter") {
submitInput(value);
void submitInput(value);
setInputKey((previousKey) => previousKey + 1);
} else if (key === "ArrowUp") {
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) {
event.preventDefault();
@ -324,9 +347,9 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }: Te
return (
<div
ref={streamRef}
ref={ref}
className={styles.Terminal}
onMouseDown={onMouseDown}
onMouseDown={onMouseDown as unknown as MouseEventHandler}
onContextMenu={onContextMenu as unknown as MouseEventHandler}
onClick={(event) => {
if (window.getSelection().toString() === "") {

View file

@ -17,12 +17,23 @@ 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";
const OVERRIDES = {
a: MarkdownLink,
img: MarkdownImage,
};
export interface MarkdownProps {
modalsManager: ModalsManager;
setCurrentFile: Function;
currentFile: VirtualFile;
app: App;
windowsManager: WindowsManager;
}
interface TextEditorProps extends WindowProps {
file?: VirtualFile;
}
@ -121,7 +132,7 @@ export function TextEditor({ file, setTitle, setIconUrl, close, mode, app, modal
currentFile,
app,
windowsManager
}
} as MarkdownProps
};
}

View file

@ -1,6 +1,8 @@
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;
alt: string
}
@ -14,6 +16,8 @@ export function MarkdownImage({ alt, src, ...props }: MarkdownImageProps) {
return src;
}, [src]);
sanitizeProps(props as MarkdownProps);
return <img
alt={alt}
{...props}

View file

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

View file

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

View file

@ -1,2 +1,6 @@
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");
}
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);
}

View file

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

View file

@ -22,9 +22,7 @@ type Particle = {
size: number;
};
let particles: Particle[] = [];
function generateScreen(frame: number): string {
function generateScreen(frame: number, screen: string[][], particles: Particle[]): string {
// Spawn new particles
if (frame % PARTICLES.framesBetweenSpawn) {
const newParticle: Particle = {
@ -34,20 +32,19 @@ function generateScreen(frame: number): string {
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
particles.forEach((particle) => {
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
if (particle.position.y + particle.size <= 0 || particle.position.x >= SCREEN_WIDTH)
return removeFromArray(particle, particles);
@ -62,13 +59,13 @@ function generateScreen(frame: number): string {
const positionX = particle.position.x;
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;
}
}
});
return screen.map((row) => row.join("")).reverse().join("\n");
return [...screen.map((row) => row.join(""))].reverse().join("\n");
}
export const cmatrix = new Command()
@ -78,11 +75,21 @@ export const cmatrix = new Command()
})
.setExecute(function() {
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;
const interval = setInterval(() => {
const text = generateScreen(frame);
const text = generateScreen(frame, screen, particles);
stream.send(text);
frame++;
}, 100 / ANIMATION_SPEED);

View file

@ -1,7 +1,9 @@
import { APPS } from "../../../../config/apps.config";
import { ANSI } from "../../../../config/apps/terminal.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 { SettingsManager } from "../../../settings/settingsManager";
import AppsManager from "../../appsManager";
import Command from "../command";
@ -9,14 +11,17 @@ export const neofetch = new Command()
.setManual({
purpose: "Fetch system information"
})
.setExecute(function(args, { username, hostname }) {
.setExecute(async function(args, { username, hostname, settingsManager }) {
const leftColumn = ANSI_ASCII_LOGO.split("\n");
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;
// Check for the browser name using regular expressions
let browserName;
let browserName: string;
if (userAgent.match(/Firefox\//)) {
browserName = "Mozilla Firefox";
} else if (userAgent.match(/Edg\//)) {
@ -29,7 +34,7 @@ export const neofetch = new Command()
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 = [
`${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("uptime", TimeManager.getUptime(2)),
formatLine("resolution", window.innerWidth + "x" + window.innerHeight),
formatLine("theme", "default"),
formatLine("theme", theme),
formatLine("icons", "Font Awesome"),
formatLine("terminal", AppsManager.getAppById(APPS.TERMINAL)?.name ?? "Unknown"),
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 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;
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 (typeof(options.fullscreen) == "string") {
fullscreen = options.fullscreen.toLowerCase() === "true";