Improved responsiveness + general maintenance
This commit is contained in:
parent
3bcb70ab95
commit
750a758634
17 changed files with 220 additions and 104 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
7
src/components/actions/actions/TextDisplay.tsx
Normal file
7
src/components/actions/actions/TextDisplay.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import styles from "../Actions.module.css";
|
||||||
|
|
||||||
|
export function TextDisplay({ children }) {
|
||||||
|
return <p className={styles.TextDisplay}>
|
||||||
|
{children}
|
||||||
|
</p>;
|
||||||
|
}
|
||||||
|
|
@ -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() === "") {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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}/>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
9
src/features/apps/text-editor/_utils/sanitizeProps.ts
Normal file
9
src/features/apps/text-editor/_utils/sanitizeProps.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue