Added browser app
This commit is contained in:
parent
9eb6368b41
commit
d20c1f6b80
19 changed files with 295 additions and 20 deletions
3
public/config/applications.xml
Normal file
3
public/config/applications.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<options>
|
||||
<startup></startup>
|
||||
</options>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -11,4 +11,5 @@
|
|||
height: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
pointer-events: initial;
|
||||
}
|
||||
113
src/components/applications/browser/Browser.jsx
Normal file
113
src/components/applications/browser/Browser.jsx
Normal 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>);
|
||||
}
|
||||
92
src/components/applications/browser/Browser.module.css
Normal file
92
src/components/applications/browser/Browser.module.css
Normal 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));
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: calc(var(--taskbar-height) + 0.5rem);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(" ")}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
2
src/constants/applications/browser.js
Normal file
2
src/constants/applications/browser.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const HOME_URL = "https://os.prozilla.dev/";
|
||||
export const SEARCH_URL = "https://www.google.com/search?igu=1";
|
||||
|
|
@ -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)
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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[]}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ export function useHistory(initialState) {
|
|||
return state !== newHistory[index + 1];
|
||||
});
|
||||
|
||||
console.log(newHistory);
|
||||
|
||||
setHistory(newHistory);
|
||||
setStateIndex(0);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue