Added browser app

This commit is contained in:
Prozilla 2023-12-18 11:03:51 +01:00
parent 9eb6368b41
commit d20c1f6b80
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
19 changed files with 295 additions and 20 deletions

View file

@ -0,0 +1,3 @@
<options>
<startup></startup>
</options>

View file

@ -1,3 +1,3 @@
<options>
<pins>file-explorer,terminal,settings,media-viewer,text-editor,wordle,balls,minesweeper</pins>
<pins>file-explorer,terminal,settings,media-viewer,browser,text-editor,wordle,balls,minesweeper</pins>
</options>

View file

@ -1,21 +1,19 @@
import { useCallback, useEffect, useState } from "react";
import { forwardRef, useEffect, useState } from "react";
import styles from "./WebView.module.css";
/**
* @param {Object} props
* @param {String} props.source
*/
export function WebView(props) {
export const WebView = forwardRef(({ source, focus, ...props }, ref) => {
const [hovered, setHovered] = useState(false);
const { source, focus } = props;
useEffect(() => {
window.focus();
const onBlur = (event) => {
if (hovered) {
focus(event);
focus?.(event);
}
};
@ -38,10 +36,15 @@ export function WebView(props) {
return (
<div className={styles.Container} onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
<iframe
ref={ref}
src={source}
title={props.title ?? "Web view"}
className={styles["Web-view"]}
referrerPolicy="no-referrer"
sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts"
{...props}
/>
</div>
);
}
});

View file

@ -11,4 +11,5 @@
height: 100%;
border: none;
background: none;
pointer-events: initial;
}

View file

@ -0,0 +1,113 @@
import { useEffect, useRef, useState } from "react";
import styles from "./Browser.module.css";
import { WebView } from "../.templates/WebView.jsx";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretLeft, faCaretRight, faHome, faRotateRight } from "@fortawesome/free-solid-svg-icons";
import { HOME_URL, SEARCH_URL } from "../../../constants/applications/browser.js";
import { isValidUrl } from "../../../features/utils/browser.js";
import { useHistory } from "../../../hooks/utils/history.js";
import { TITLE_SEPARATOR } from "../../../constants/windows.js";
/** @type {import("../../windows/WindowView.jsx").windowProps} */
export function Browser({ startUrl, focus }) {
const initialUrl = startUrl ?? HOME_URL;
const [url, setUrl] = useState(initialUrl);
const [input, setInput] = useState(initialUrl);
const { history, pushState, stateIndex, undo, redo, undoAvailable, redoAvailable } = useHistory(initialUrl);
const ref = useRef(null);
useEffect(() => {
if (history.length === 0)
return;
setUrl(history[stateIndex]);
}, [history, stateIndex]);
const reload = () => {
if (ref.current == null)
return;
ref.current.contentWindow.location.href = url;
};
const updateUrl = (newUrl) => {
if (url === newUrl) {
return reload();
}
setUrl(newUrl);
setInput(newUrl);
pushState(newUrl);
};
const onInputChange = (event) => {
setInput(event.target.value);
};
const onKeyDown = (event) => {
const value = event.target.value;
if (event.key === "Enter" && value !== "") {
if (isValidUrl(value)) {
updateUrl(value);
} else {
updateUrl(`${SEARCH_URL}&q=${value}`);
}
}
};
return (<div className={styles.Container}>
<div className={styles.Header}>
<div className={styles["Nav-bar"]}>
<button
title="Back"
tabIndex={0}
className={styles["Icon-button"]}
onClick={undo}
disabled={!undoAvailable}
>
<FontAwesomeIcon icon={faCaretLeft}/>
</button>
<button
title="Forward"
tabIndex={0}
className={styles["Icon-button"]}
onClick={redo}
disabled={!redoAvailable}
>
<FontAwesomeIcon icon={faCaretRight}/>
</button>
<button
title="Reload"
tabIndex={0}
className={styles["Icon-button"]}
onClick={reload}
>
<FontAwesomeIcon icon={faRotateRight}/>
</button>
<button
title="Home"
tabIndex={0}
className={styles["Icon-button"]}
onClick={() => { updateUrl(HOME_URL); }}
>
<FontAwesomeIcon icon={faHome}/>
</button>
<input
value={input}
type="text"
aria-label="Search bar"
className={styles["Search-bar"]}
tabIndex={0}
onChange={onInputChange}
onKeyDown={onKeyDown}
/>
</div>
<div className={styles["Bookmarks"]}>
</div>
</div>
<WebView ref={ref} source={url} title="Browser" focus={focus}/>
</div>);
}

View file

@ -0,0 +1,92 @@
.Container {
--header-height: 3.5rem;
--nav-bar-height: 2.25rem;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.Header {
display: flex;
flex-direction: column;
width: 100%;
height: var(--header-height);
background-color: var(--background-color-a);
}
.Nav-bar {
display: flex;
gap: 1rem;
justify-content: flex-start;
align-items: center;
width: 100%;
height: var(--nav-bar-height);
padding: 1.25rem;
}
.Icon-button {
--color: var(--foreground-color-a);
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 1rem;
width: auto;
padding: 0;
background: none;
border: none;
outline: none;
aspect-ratio: 1;
cursor: pointer;
}
.Icon-button::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0%);
border-radius: 9999px;
transform: scale(100%);
transform-origin: center;
transition: all 200ms ease-in-out;
}
.Icon-button:hover::after,
.Icon-button:focus-visible::after {
background-color: rgba(255, 255, 255, 10%);
transform: scale(175%);
}
.Icon-button:disabled {
--color: var(--foreground-color-c);
}
.Icon-button svg {
height: 100%;
}
.Icon-button svg path {
fill: var(--color);
transition: fill 100ms ease-in-out;
}
.Search-bar {
flex: 1;
padding: 0.25rem 0.5rem;
background-color: var(--background-color-c);
border: none;
border-radius: 0.5rem;
outline: none;
font-family: inherit;
font-size: 0.875em;
}
.Bookmarks {
height: calc(var(--header-height) - var(--nav-bar-height));
}

View file

@ -154,10 +154,18 @@ export function FileExplorer({ startPath, app, modalsManager }) {
>
<FontAwesomeIcon icon={faCaretRight}/>
</button>
<button title="Up" tabIndex={0} className={styles["Icon-button"]} onClick={() => { changeDirectory(".."); }}>
<button
title="Up"
tabIndex={0}
className={styles["Icon-button"]}
onClick={() => { changeDirectory(".."); }}
>
<FontAwesomeIcon icon={faArrowUp}/>
</button>
<button title="New" tabIndex={0} className={styles["Icon-button"]}
<button
title="New"
tabIndex={0}
className={styles["Icon-button"]}
onClick={() => {
openWindowedModal({
title: "Error",

View file

@ -31,6 +31,7 @@
width: 100%;
height: 100%;
padding: 0.5rem;
padding-bottom: calc(var(--taskbar-height) + 0.5rem);
z-index: 1;
}

View file

@ -74,14 +74,12 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
setStartPosition(new Vector2(16, 16));
setMaximized(true);
} else {
if (position.x > screenWidth) {
if (position.x > screenWidth)
position.x = 0;
setStartPosition(position);
}
if (position.y > screenHeight) {
if (position.y > screenHeight)
position.y = 0;
setStartPosition(position);
}
setStartPosition(position);
}
}, [position, size, screenHeight, screenWidth]);
@ -114,7 +112,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
key={id}
axis="both"
handle={".Window-handle"}
defaultPosition={startPosition}
defaultPosition={startPosition.round()}
position={null}
scale={1}
bounds={{
@ -127,6 +125,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
nodeRef={nodeRef}
disabled={maximized}
onStart={focus}
grid={[1, 1]}
>
<div
className={classNames.join(" ")}

View file

@ -2,18 +2,31 @@ import { useWindows } from "../../hooks/windows/windowsContext.js";
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
import { memo, useEffect, useState } from "react";
import { WindowView } from "./WindowView.jsx";
import { useSettingsManager } from "../../hooks/settings/settingsManagerContext.js";
import { SettingsManager } from "../../features/settings/settingsManager.js";
export const WindowsView = memo(() => {
const settingsManager = useSettingsManager();
const windows = useWindows();
const windowsManager = useWindowsManager();
const [sortedWindows, setSortedWindows] = useState([]);
// Sort windows
useEffect(() => {
setSortedWindows([...windows].sort((windowA, windowB) =>
windowA.lastInteraction - windowB.lastInteraction
));
}, [windows]);
// Launch startup apps
useEffect(() => {
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.applications);
settings.get("startup", (value) => {
if (value !== "")
windowsManager.startup(value?.split(","));
});
}, [settingsManager, windowsManager]);
// TO DO: prevent windows from being rerendered when order is changed
return (<div>

View file

@ -0,0 +1,2 @@
export const HOME_URL = "https://os.prozilla.dev/";
export const SEARCH_URL = "https://www.google.com/search?igu=1";

View file

@ -8,6 +8,7 @@ import { Settings } from "../../components/applications/settings/Settings.jsx";
// import { Calculator } from "../../components/applications/calculator/Calculator.jsx";
import Vector2 from "../math/vector2.js";
import { APPS } from "../../constants/applications.js";
import { Browser } from "../../components/applications/browser/Browser.jsx";
export default class AppsManager {
static APPLICATIONS = [
@ -31,6 +32,9 @@ export default class AppsManager {
source: "https://prozilla.dev/minesweeper",
size: new Vector2(500, 580)
}),
new Application("Browser", "browser", Browser, {
size: new Vector2(700, 500)
}),
];
/**

View file

@ -5,4 +5,13 @@ export default class Vector2 {
this.x = x;
this.y = y ?? x;
}
/**
* @returns {Vector2}
*/
round() {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
}
}

View file

@ -7,7 +7,8 @@ export class SettingsManager {
*/
static VIRTUAL_PATHS = {
desktop: "~/.config/desktop.xml",
taskbar: "~/.config/taskbar.xml"
taskbar: "~/.config/taskbar.xml",
applications: "~/.config/applications.xml",
};
/**

View file

@ -16,4 +16,17 @@ export function closeViewport(requireConfirmation = false) {
export function reloadViewport(bypassCache = false) {
console.info("Reloading viewport");
window.location.reload(bypassCache);
}
/**
* @param {string} string
* @returns {boolean}
*/
export function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (err) {
return false;
}
}

View file

@ -41,6 +41,8 @@ export function loadDefaultData(virtualRoot) {
file.setSource("/config/desktop.xml");
}).createFile("taskbar", "xml", (file) => {
file.setSource("/config/taskbar.xml");
}).createFile("applications", "xml", (file) => {
file.setSource("/config/applications.xml");
});
})
.createFolder("Pictures", (folder) => {

View file

@ -9,6 +9,7 @@ export default class WindowsManager {
constructor() {
this.windows = {};
this.updateWindows = () => {};
this.startupComplete = false;
}
/**
@ -184,6 +185,17 @@ export default class WindowsManager {
this.updateWindows = updateWindows;
}
startup(appIds) {
if (appIds == null || this.startupComplete)
return;
appIds.forEach((appId) => {
this.open(appId);
});
this.startupComplete = true;
}
/**
* @returns {string[]}
*/

View file

@ -31,8 +31,6 @@ export function useHistory(initialState) {
return state !== newHistory[index + 1];
});
console.log(newHistory);
setHistory(newHistory);
setStateIndex(0);
};

View file

@ -21,10 +21,11 @@ body {
-moz-osx-font-smoothing: grayscale;
}
* {
*, :after, :before {
color: var(--foreground-color-a);
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
text-rendering: optimizelegibility;
}
p, a, button, input, h1, h2, h3, h4, h5, h6 {