diff --git a/.gitignore b/.gitignore index db61fdc..4ba7535 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* *.ini + +/tmp \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 570bbe1..36a596a 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,7 +1,7 @@ #!/bin/bash # Edit these variables before deploying -DOMAIN=os.prozilla.dev +DOMAIN="os.prozilla.dev" COMMIT_MESSAGE="Deployed build to GitHub Pages" REPO_URL="https://github.com/Prozilla/ProzillaOS" @@ -11,7 +11,29 @@ echo -e "Domain: \e[0;36m$DOMAIN\e[0m" echo -e "Commit message: \e[0;36m$COMMIT_MESSAGE\e[0m" echo -e "Repository: \e[0;36m$REPO_URL\e[0m\n" +echo -e "\e[0;33mConfiguring routing...\e[0m" echo $DOMAIN > dist/CNAME +cp dist/index.html dist/404.html + +# Use template and pages data to generate index.html files +input="tmp/pages.csv" +template="tmp/template.html" +while IFS=";" read -r app_id app_name app_description +do + path="dist/$app_id/index.html" + echo -e "- dist/\e[0;36m$app_id/index.html\e[0m" + + mkdir -p "dist/$app_id" + cp $template $path + + # Fill template + sed -i "s/_APP_NAME/$app_name/g" $path + sed -i "s/_APP_DESCRIPTION/$app_description/g" $path + sed -i "s/_APP_ID/$app_id/g" $path +done < <(tail -n +2 $input) +rm -r tmp +echo "" + echo -e "\e[0;33mDeploying to GitHub Pages...\e[0m" 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" diff --git a/index.html b/index.html index 65cc7a8..3d27366 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ - + diff --git a/package.json b/package.json index 5ce2899..46165b6 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "start": "vite --port 3000 --host", "build": "tsc && vite build", "serve": "vite preview", - "predeploy": "npm run sitemap && npm run build", + "predeploy": "npm run build && npm run prep", "deploy": "sh deploy.sh", - "sitemap": "node --experimental-specifier-resolution=node --loader ts-node/esm ./src/tools/sitemapGenerator.js" + "prep": "node --no-warnings --loader ts-node/esm ./src/tools/prep" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", diff --git a/public/assets/banner-logo-title.png b/public/assets/banner-logo-title.png index ec78795..e58817c 100644 Binary files a/public/assets/banner-logo-title.png and b/public/assets/banner-logo-title.png differ diff --git a/public/assets/icon-mask.svg b/public/assets/icon-mask.svg new file mode 100644 index 0000000..8f3d79c --- /dev/null +++ b/public/assets/icon-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 1aa9836..0000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - https://os.prozilla.dev/ - 2024-6-9 - - - https://os.prozilla.dev/assets/wallpapers/vibrant-wallpaper-blue-purple-red.png - - - https://os.prozilla.dev/assets/wallpapers/abstract-mesh-gradient-orange-red-purple.png - - - https://os.prozilla.dev/assets/wallpapers/vibrant-wallpaper-purple-yellow.png - - - https://os.prozilla.dev/assets/wallpapers/abstract-wallpaper-mesh-gradient-cyan.png - - - https://os.prozilla.dev/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png - - - https://os.prozilla.dev/assets/wallpapers/mesh-gradient-wallpaper-red-purple.png - - - https://os.prozilla.dev/assets/wallpapers/colorful-mesh-gradient-red-green.png - - - https://os.prozilla.dev/assets/wallpapers/flame-abstract-wallpaper-orange.png - - - https://os.prozilla.dev/assets/wallpapers/wave-abstract-wallpaper-teal.png - - - - - https://os.prozilla.dev/terminal - 2024-6-9 - - - https://os.prozilla.dev/settings - 2024-6-9 - - - https://os.prozilla.dev/media-viewer - 2024-6-9 - - - https://os.prozilla.dev/text-editor - 2024-6-9 - - - https://os.prozilla.dev/file-explorer - 2024-6-9 - - - https://os.prozilla.dev/calculator - 2024-6-9 - - - https://os.prozilla.dev/browser - 2024-6-9 - - - https://os.prozilla.dev/logic-sim - 2024-6-9 - - \ No newline at end of file diff --git a/src/components/_utils/button/Button.module.css b/src/components/_utils/button/Button.module.css index 0dc34d2..289a9b0 100644 --- a/src/components/_utils/button/Button.module.css +++ b/src/components/_utils/button/Button.module.css @@ -20,6 +20,6 @@ text-decoration: none; } -.ButtonLink > svg { +.Button > svg { margin-left: 0.5rem; } \ No newline at end of file diff --git a/src/components/_utils/button/Button.tsx b/src/components/_utils/button/Button.tsx index c5bb499..e407eff 100644 --- a/src/components/_utils/button/Button.tsx +++ b/src/components/_utils/button/Button.tsx @@ -40,6 +40,10 @@ export function Button(props: ButtonProps) { className={className} > {children} + {icon != null + ? + : null + } ); } } \ No newline at end of file diff --git a/src/components/desktop/Desktop.tsx b/src/components/desktop/Desktop.tsx index d25585c..0f8ced8 100644 --- a/src/components/desktop/Desktop.tsx +++ b/src/components/desktop/Desktop.tsx @@ -71,7 +71,7 @@ export const Desktop = memo(() => { { openWindowedModal({ - size: new Vector2(350, 350), + size: new Vector2(350, 400), Modal: (props) => }); }}/> diff --git a/src/components/modals/ModalView.tsx b/src/components/modals/ModalView.tsx index 2e2713b..b65796e 100644 --- a/src/components/modals/ModalView.tsx +++ b/src/components/modals/ModalView.tsx @@ -11,6 +11,7 @@ export interface ModalProps { fullscreen?: boolean; iconUrl?: string; title?: string; + standalone?: boolean; [key: string]: unknown; }; children?: ReactNode; diff --git a/src/components/modals/share/Share.module.css b/src/components/modals/share/Share.module.css index f89182f..809dbdb 100644 --- a/src/components/modals/share/Share.module.css +++ b/src/components/modals/share/Share.module.css @@ -73,6 +73,10 @@ select.Input > * { font-size: inherit; } +.Input:disabled + div { + opacity: 0.5; +} + .Input[type=checkbox] { display: none; } diff --git a/src/components/modals/share/Share.tsx b/src/components/modals/share/Share.tsx index 4b83a1d..246e93c 100644 --- a/src/components/modals/share/Share.tsx +++ b/src/components/modals/share/Share.tsx @@ -37,6 +37,7 @@ const APP_OPTIONS: Record[]> = { export function Share({ modal, params, ...props }: ModalProps) { const [appId, setAppId] = useState(params.appId ?? ""); const [fullscreen, setFullscreen] = useState(params.fullscreen ?? false); + const [standalone, setStandalone] = useState(params.standalone ?? false); const [options, setOptions] = useState({}); const [url, setUrl] = useState(null); const { alert } = useAlert(); @@ -45,9 +46,10 @@ export function Share({ modal, params, ...props }: ModalProps) { setUrl(generateUrl({ appId: appId !== "" ? appId : null, fullscreen, + standalone, ...options })); - }, [appId, fullscreen, options]); + }, [appId, fullscreen, standalone, options]); const onAppIdChange = (event: Event) => { const newAppId = (event.target as HTMLInputElement).value; @@ -74,6 +76,11 @@ export function Share({ modal, params, ...props }: ModalProps) { setFullscreen(newFullscreen); }; + const onStandaloneChange = (event: Event) => { + const newStandalone = (event.target as HTMLInputElement).checked; + setStandalone(newStandalone); + }; + const setOption = (name: string, value: string) => { setOptions((options = {}) => { options = { ...options }; @@ -99,6 +106,23 @@ export function Share({ modal, params, ...props }: ModalProps) { )} + {appId !== "" ? + Standalone: + + + {standalone + ? + : + } + + : null} {appId !== "" ? Fullscreen: diff --git a/src/components/router/Router.tsx b/src/components/router/Router.tsx index 2136074..0159eac 100644 --- a/src/components/router/Router.tsx +++ b/src/components/router/Router.tsx @@ -2,7 +2,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import { DefaultRoute } from "./routes/DefaultRoute"; import AppsManager from "../../features/apps/appsManager"; import App from "../../features/apps/app"; -import { AppRoute } from "./routes/AppRoute"; +import { StandaloneRoute } from "./routes/StandaloneRoute"; import { NoRoute } from "./routes/NoRoute"; export function Router() { @@ -11,7 +11,7 @@ export function Router() { }/> {AppsManager.APPS.map((app: App) => - }/> + }/> )} }/> diff --git a/src/components/router/routes/AppRoute.tsx b/src/components/router/routes/AppRoute.tsx deleted file mode 100644 index 8a65d6a..0000000 --- a/src/components/router/routes/AppRoute.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from "react"; -import App from "../../../features/apps/app"; -import AppsManager from "../../../features/apps/appsManager"; -import { setViewportIcon, setViewportTitle } from "../../../features/_utils/browser.utils"; -import { NAME } from "../../../config/branding.config"; - -interface AppRouteProps { - app: App; -} - -export function AppRoute({ app }: AppRouteProps) { - const [title, setTitle] = useState(app.name); - const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id)); - - useEffect(() => { - const setViewportTitleAndIcon = () => { - setViewportTitle(`${title} | ${NAME}`); - setViewportIcon(iconUrl); - }; - - setViewportTitleAndIcon(); - }, [iconUrl, title]); - - return ; -} \ No newline at end of file diff --git a/src/components/router/routes/StandaloneRoute.module.css b/src/components/router/routes/StandaloneRoute.module.css new file mode 100644 index 0000000..2c58491 --- /dev/null +++ b/src/components/router/routes/StandaloneRoute.module.css @@ -0,0 +1,15 @@ +.StandaloneView { + --header-height: 2.5rem; + + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +.StandaloneWindow { + position: relative; + width: 100%; + height: calc(100% - var(--header-height)); + overflow: hidden; +} \ No newline at end of file diff --git a/src/components/router/routes/StandaloneRoute.tsx b/src/components/router/routes/StandaloneRoute.tsx new file mode 100644 index 0000000..6c5d873 --- /dev/null +++ b/src/components/router/routes/StandaloneRoute.tsx @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useState } from "react"; +import App from "../../../features/apps/app"; +import AppsManager from "../../../features/apps/appsManager"; +import { generateUrl, getViewportParams, openUrl, setViewportIcon, setViewportTitle } from "../../../features/_utils/browser.utils"; +import { NAME } from "../../../config/branding.config"; +import { StandaloneHeader } from "../../windows/StandaloneHeader"; +import styles from "./StandaloneRoute.module.css"; +import { ModalsView } from "../../modals/ModalsView"; + +interface StandaloneRouteProps { + app: App; +} + +export function StandaloneRoute({ app }: StandaloneRouteProps) { + const [title, setTitle] = useState(app.name); + const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id)); + + useEffect(() => { + // TO DO: don't overwrite metadata in production + + const setViewportTitleAndIcon = () => { + if (import.meta.env.DEV) + setViewportTitle(`${title} | ${NAME}`); + setViewportIcon(iconUrl); + }; + + setViewportTitleAndIcon(); + }, [iconUrl, title]); + + const params = getViewportParams(); + + const exit = useCallback(() => { + const url = generateUrl({ appId: app.id }); + openUrl(url, "_self"); + }, [app.id]); + + return + + + + { exit(); }} + {...params} + /> + + ; +} \ No newline at end of file diff --git a/src/components/windows/StandaloneHeader.module.css b/src/components/windows/StandaloneHeader.module.css new file mode 100644 index 0000000..34eaa31 --- /dev/null +++ b/src/components/windows/StandaloneHeader.module.css @@ -0,0 +1,69 @@ +.Header { + display: flex; + width: 100%; + justify-content: space-between; + height: var(--header-height); + padding: 0.75rem 1rem; + background-color: var(--background-color-1); +} + +.Logo { + display: flex; + gap: 0.5rem; + width: auto; + justify-content: flex-start; + align-items: center; + color: var(--foreground-color-1); + text-decoration: none; + transition: color 300ms ease-in-out; +} + +.Logo:hover { + color: var(--foreground-color-0); +} + +.Logo div, +.Logo svg { + width: auto; + height: 1.5rem; + color: inherit; + aspect-ratio: 1; +} + +.Logo svg { + object-fit: contain; +} + +.Logo svg * { + color: inherit; + fill: currentColor; +} + +.Logo > h1 { + margin: 0; + color: inherit; + font-size: 1.25rem; +} + +.ExitButton { + display: flex; + justify-content: center; + align-items: center; + color: var(--foreground-color-1); + background: none; + font-size: 1rem; + transition: color 300ms ease-in-out; +} + +.ExitButton:hover { + color: var(--foreground-color-0); +} + +.ExitButton > svg { + height: 1.25rem; + color: inherit; +} + +.ExitButton > svg * { + color: inherit; +} \ No newline at end of file diff --git a/src/components/windows/StandaloneHeader.tsx b/src/components/windows/StandaloneHeader.tsx new file mode 100644 index 0000000..e3c21dc --- /dev/null +++ b/src/components/windows/StandaloneHeader.tsx @@ -0,0 +1,21 @@ +import { ReactSVG } from "react-svg"; +import { NAME } from "../../config/branding.config"; +import styles from "./StandaloneHeader.module.css"; +import { Button } from "../_utils/button/Button"; +import { faTimes } from "@fortawesome/free-solid-svg-icons"; + +interface StandaloneHeaderProps { + exit: Function; +} + +export function StandaloneHeader({ exit }: StandaloneHeaderProps) { + return + + + {NAME} + + + Exit + + ; +} \ No newline at end of file diff --git a/src/components/windows/WindowView.tsx b/src/components/windows/WindowView.tsx index e2a8546..12fc16a 100644 --- a/src/components/windows/WindowView.tsx +++ b/src/components/windows/WindowView.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import styles from "./WindowView.module.css"; -import { faExpand, faMinus, faWindowMaximize as fasWindowMaximize, faTimes, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faCircleRight, faExpand, faMinus, faWindowMaximize as fasWindowMaximize, faTimes, faXmark } from "@fortawesome/free-solid-svg-icons"; import { ReactSVG } from "react-svg"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext"; import Draggable from "react-draggable"; @@ -14,7 +14,7 @@ import { ClickAction } from "../actions/actions/ClickAction"; import { Actions } from "../actions/Actions"; import { useScreenDimensions } from "../../hooks/_utils/screen"; import { NAME } from "../../config/branding.config"; -import { setViewportIcon, setViewportTitle } from "../../features/_utils/browser.utils"; +import { generateUrl, openUrl, setViewportIcon, setViewportTitle } from "../../features/_utils/browser.utils"; import { ZIndexManager } from "../../features/z-index/zIndexManager"; import { useZIndex } from "../../hooks/z-index/zIndex"; import { useWindowedModal } from "../../hooks/modals/windowedModal"; @@ -36,6 +36,7 @@ export interface WindowProps extends WindowOptions { minimized?: boolean; toggleMinimized?: Function; index?: number; + standalone?: boolean; } export const WindowView: FC = memo(({ id, app, size, position, onInteract, options, active, fullscreen, minimized, toggleMinimized, index }) => { @@ -61,11 +62,14 @@ export const WindowView: FC = memo(({ id, app, size, position, onIn close(); }}/> + { + openUrl(generateUrl({ appId: app.id, standalone: true }), "_self"); + }}/> { openWindowedModal({ appId: app.id, fullscreen: maximized, - size: new Vector2(350, 350), + size: new Vector2(350, 400), Modal: (props) => }); }}/> @@ -210,6 +214,7 @@ export const WindowView: FC = memo(({ id, app, size, position, onIn close={close} focus={focus} active={active} + standalone={false} /> diff --git a/src/config/apps.config.ts b/src/config/apps.config.ts index 33368c2..ce1d458 100644 --- a/src/config/apps.config.ts +++ b/src/config/apps.config.ts @@ -1,3 +1,5 @@ +import { NAME } from "./branding.config"; + export const APPS = { TERMINAL: "terminal", SETTINGS: "settings", @@ -17,6 +19,17 @@ export const APP_NAMES = { FILE_EXPLORER: "Files", CALCULATOR: "Maths", BROWSER: "Browser", + LOGIC_SIM: "Logic Sim (WIP)" +}; + +export const APP_DESCRIPTIONS = { + TERMINAL: "A command line tool inspired by the Unix shell that runs entirely in your browser and uses a virtual file system.", + SETTINGS: `Configure ${NAME}'s settings and customize your experience.`, + TEXT_EDITOR: "Simple text editor for reading and writing text documents.", + FILE_EXPLORER: "Browse and manage your virtual files.", + CALCULATOR: "Simple calculator app.", + BROWSER: "Browse the internet.", + LOGIC_SIM: "Create digital logic circuits using the online simulator." }; export const APP_ICONS = { diff --git a/src/features/_utils/browser.utils.ts b/src/features/_utils/browser.utils.ts index 4b99aa4..a402e3e 100644 --- a/src/features/_utils/browser.utils.ts +++ b/src/features/_utils/browser.utils.ts @@ -1,3 +1,4 @@ +import { HTMLAttributeAnchorTarget } from "react"; import { NAME } from "../../config/branding.config"; /** @@ -59,19 +60,29 @@ export function getViewportParams(): Record { return params; } -export function generateUrl(options: { appId: string; fullscreen: boolean; }) { - const baseUrl = window.location.origin + "/"; +interface generateUrlOptions { + appId: string; + fullscreen?: boolean; + standalone?: boolean; +} + +export function generateUrl(options: generateUrlOptions) { + let baseUrl = window.location.origin + "/"; if (!options || Object.keys(options).length === 0) return baseUrl; - const { appId, fullscreen, ...extraOptions } = options; + const { appId, fullscreen, standalone, ...extraOptions } = options; const params: URLSearchParams & { size?: number } = new URLSearchParams(); - if (appId) - params.set("app", appId); - if (fullscreen) - params.set("fullscreen", fullscreen.toString()); + if (standalone && appId) { + baseUrl += appId; + } else { + if (appId) + params.set("app", appId); + if (fullscreen) + params.set("fullscreen", fullscreen.toString()); + } if (extraOptions && Object.keys(extraOptions).length > 0) { Object.entries(extraOptions).forEach(([key, value]) => { @@ -87,8 +98,8 @@ export function generateUrl(options: { appId: string; fullscreen: boolean; }) { return url; } -export function openUrl(url: string) { - window.open(url, "_blank"); +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) { diff --git a/src/features/apps/appsManager.ts b/src/features/apps/appsManager.ts index a1c341b..8d520db 100644 --- a/src/features/apps/appsManager.ts +++ b/src/features/apps/appsManager.ts @@ -36,7 +36,7 @@ export default class AppsManager { new App(APP_NAMES.BROWSER, APPS.BROWSER, Browser, { size: new Vector2(700, 500) }), - new App("Logic Sim (WIP)", APPS.LOGIC_SIM, LogicSim), + new App(APP_NAMES.LOGIC_SIM, APPS.LOGIC_SIM, LogicSim), ]; static getAppById(id: string): App | null { diff --git a/src/tools/prep.ts b/src/tools/prep.ts new file mode 100644 index 0000000..a3ea698 --- /dev/null +++ b/src/tools/prep.ts @@ -0,0 +1,95 @@ +import fs from "node:fs"; +import { APP_DESCRIPTIONS, APP_NAMES, APPS } from "../config/apps.config"; +import { ANSI } from "../config/apps/terminal.config"; +import { BASE_URL, NAME, TAG_LINE } from "../config/branding.config"; +import { WALLPAPERS } from "../config/desktop.config"; + +const PATH_TO_SITEMAP = "dist/sitemap.xml"; +const PATH_TO_PAGES = "tmp/pages.csv"; +const PATH_TO_INDEX = "dist/index.html"; +const PATH_TO_TEMPLATE = "tmp/template.html"; + +function generateSitemap() { + const date = new Date(); + const lastModified = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + + const images = WALLPAPERS.map((path) => ` + + ${BASE_URL.slice(0, -1) + path} + ` + ); + + const pages = Object.values(APPS).map((appId) => ` + + ${BASE_URL + appId} + ${lastModified} + ` + ); + + const sitemap = ` + + + + + ${BASE_URL} + ${lastModified} + ${images.join("")} + + ${pages.join("")} +`; + + return sitemap.trim(); +} + +function generatePageRecords() { + const records = ["id;name;description"]; + + for (const [key, value] of Object.entries(APPS)) { + const id = value; + const name = (Object.keys(APP_NAMES).includes(key) ? APP_NAMES[key] : id) as string; + const description = (Object.keys(APP_DESCRIPTIONS).includes(key) ? APP_DESCRIPTIONS[key] : TAG_LINE) as string; + + records.push(`${id};${name};${description}`); + } + + return records.join("\n") + "\n"; +} + +function generatePageTemplate() { + let html = fs.readFileSync(PATH_TO_INDEX, "utf-8"); + + const titleRegex = /(?<=(|))/g; + html = html.replaceAll(titleRegex, `_APP_NAME | ${NAME}`); + + const descriptionRegex = /(?<=())/g; + html = html.replaceAll(descriptionRegex, "_APP_DESCRIPTION"); + + const canonicalRegex = /(?<=())/g; + html = html.replaceAll(canonicalRegex, `${BASE_URL}_APP_ID`); + + const faqRegex = /.*?
Standalone:
Fullscreen: