Added standalone mode
This commit is contained in:
parent
14015ce28e
commit
ba166fc7f0
25 changed files with 364 additions and 168 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -23,3 +23,5 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.ini
|
||||
|
||||
/tmp
|
||||
24
deploy.sh
24
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"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<meta name="apple-mobile-web-app-title" content="ProzillaOS">
|
||||
<meta name="application-name" content="ProzillaOS">
|
||||
<meta name="msapplication-TileColor" content="#0d1114">
|
||||
<meta name="theme-color" content="#0d1114">
|
||||
<meta name="theme-color" content="#4DFFE1">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="ProzillaOS | Web-based Operating System"/>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 274 KiB |
3
public/assets/icon-mask.svg
Normal file
3
public/assets/icon-mask.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M116.184 9C113.875 5 108.101 5 105.792 9L82.5996 49.1704C81.6764 50.7695 81.5836 52.5283 82.0998 54.0636C81.3989 54.3808 80.704 54.7089 80.0154 55.0478C79.6528 53.3043 78.3783 51.7573 76.4129 51.2307L38.1765 40.9853C34.4587 39.9891 31.0566 43.3912 32.0528 47.109L42.2982 85.3454C42.8826 87.5263 44.7233 88.8564 46.6948 89.034C46.4057 89.6522 46.1252 90.2753 45.8534 90.903C44.7404 89.6857 42.9026 89.1731 41.1996 89.9672L11.6187 103.761C8.82797 105.062 8.49251 108.897 11.0148 110.663L37.7511 129.384C38.7006 130.049 39.787 130.233 40.787 130.04C40.9883 131.382 41.2269 132.711 41.5016 134.027C40.1776 133.252 38.3459 133.569 37.4348 135.027L23.1418 157.901C21.918 159.859 23.2732 162.408 25.5812 162.489L52.5368 163.43C53.355 163.458 54.0754 163.169 54.6162 162.693C67.6957 179.848 88.3474 190.918 111.585 190.918C151.125 190.918 183.179 158.864 183.179 119.324C183.179 89.8848 165.411 64.5956 140.013 53.5958C140.366 52.1775 140.207 50.6093 139.377 49.1704L116.184 9ZM81.1121 144.848C84.3761 146.192 87.9281 146.864 91.7681 146.864C95.5121 146.864 98.9921 146.192 102.208 144.848C105.472 143.504 108.304 141.656 110.704 139.304C113.104 136.904 114.976 134.12 116.32 130.952C117.664 127.736 118.336 124.28 118.336 120.584C118.336 116.84 117.664 113.384 116.32 110.216C114.976 107 113.08 104.216 110.632 101.864C108.232 99.464 105.4 97.616 102.136 96.32C98.9201 94.976 95.4161 94.304 91.6241 94.304C87.7841 94.304 84.2321 94.976 80.9681 96.32C77.7521 97.616 74.9441 99.464 72.5441 101.864C70.1441 104.216 68.2721 106.976 66.9281 110.144C65.5841 113.312 64.9121 116.768 64.9121 120.512C64.9121 124.208 65.5841 127.664 66.9281 130.88C68.2721 134.048 70.1681 136.832 72.6161 139.232C75.0641 141.632 77.8961 143.504 81.1121 144.848ZM99.5441 134.552C97.2881 135.896 94.6481 136.568 91.6241 136.568C89.3681 136.568 87.3041 136.184 85.4321 135.416C83.5601 134.648 81.9521 133.568 80.6081 132.176C79.3121 130.736 78.3041 129.032 77.5841 127.064C76.8641 125.096 76.5041 122.912 76.5041 120.512C76.5041 117.296 77.1281 114.512 78.3761 112.16C79.6241 109.76 81.3761 107.912 83.6321 106.616C85.9361 105.272 88.6001 104.6 91.6241 104.6C93.8801 104.6 95.9201 104.984 97.7441 105.752C99.6161 106.472 101.224 107.552 102.568 108.992C103.912 110.384 104.944 112.064 105.664 114.032C106.384 115.952 106.744 118.112 106.744 120.512C106.744 123.68 106.12 126.488 104.872 128.936C103.624 131.336 101.848 133.208 99.5441 134.552ZM128.781 144.776C131.805 146.12 135.405 146.792 139.581 146.792C145.677 146.792 150.453 145.424 153.909 142.688C157.365 139.904 159.093 136.088 159.093 131.24C159.093 128.6 158.661 126.392 157.797 124.616C156.933 122.792 155.781 121.304 154.341 120.152C152.949 118.952 151.413 117.968 149.733 117.2C148.053 116.432 146.349 115.76 144.621 115.184C142.941 114.608 141.381 114.032 139.941 113.456C138.549 112.88 137.421 112.208 136.557 111.44C135.693 110.624 135.261 109.568 135.261 108.272C135.261 106.88 135.837 105.824 136.989 105.104C138.141 104.336 139.701 103.952 141.669 103.952C143.637 103.952 145.389 104.336 146.925 105.104C148.509 105.872 150.069 107.048 151.605 108.632L158.949 101.36C157.029 99.2 154.581 97.496 151.605 96.248C148.677 95 145.437 94.376 141.885 94.376C138.381 94.376 135.285 94.976 132.597 96.176C129.909 97.328 127.797 99.032 126.261 101.288C124.773 103.544 124.029 106.232 124.029 109.352C124.029 111.944 124.461 114.104 125.325 115.832C126.189 117.512 127.317 118.928 128.709 120.08C130.149 121.184 131.709 122.12 133.389 122.888C135.069 123.608 136.749 124.256 138.429 124.832C140.157 125.408 141.717 126.008 143.109 126.632C144.549 127.256 145.701 128 146.565 128.864C147.429 129.728 147.861 130.856 147.861 132.248C147.861 133.784 147.189 134.984 145.845 135.848C144.549 136.712 142.701 137.144 140.301 137.144C137.661 137.144 135.333 136.64 133.317 135.632C131.349 134.576 129.549 133.112 127.917 131.24L120.573 138.512C123.069 141.296 125.805 143.384 128.781 144.776Z" fill="#E6F2FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
|
|
@ -1,70 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xml>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/vibrant-wallpaper-blue-purple-red.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/abstract-mesh-gradient-orange-red-purple.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/vibrant-wallpaper-purple-yellow.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/abstract-wallpaper-mesh-gradient-cyan.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/mesh-gradient-wallpaper-red-purple.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-mesh-gradient-red-green.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/flame-abstract-wallpaper-orange.png</image:loc>
|
||||
</image:image>
|
||||
<image:image>
|
||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/wave-abstract-wallpaper-teal.png</image:loc>
|
||||
</image:image>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/terminal</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/settings</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/media-viewer</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/text-editor</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/file-explorer</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/calculator</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/browser</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://os.prozilla.dev/logic-sim</loc>
|
||||
<lastmod>2024-6-9</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
|
@ -20,6 +20,6 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ButtonLink > svg {
|
||||
.Button > svg {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
|
@ -40,6 +40,10 @@ export function Button(props: ButtonProps) {
|
|||
className={className}
|
||||
>
|
||||
{children}
|
||||
{icon != null
|
||||
? <FontAwesomeIcon icon={icon}/>
|
||||
: null
|
||||
}
|
||||
</button>);
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export const Desktop = memo(() => {
|
|||
<Divider/>
|
||||
<ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} onTrigger={() => {
|
||||
openWindowedModal({
|
||||
size: new Vector2(350, 350),
|
||||
size: new Vector2(350, 400),
|
||||
Modal: (props) => <Share {...props}/>
|
||||
});
|
||||
}}/>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface ModalProps {
|
|||
fullscreen?: boolean;
|
||||
iconUrl?: string;
|
||||
title?: string;
|
||||
standalone?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
children?: ReactNode;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ select.Input > * {
|
|||
font-size: inherit;
|
||||
}
|
||||
|
||||
.Input:disabled + div {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.Input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const APP_OPTIONS: Record<string, Record<string, string>[]> = {
|
|||
export function Share({ modal, params, ...props }: ModalProps) {
|
||||
const [appId, setAppId] = useState<string>(params.appId ?? "");
|
||||
const [fullscreen, setFullscreen] = useState<boolean>(params.fullscreen ?? false);
|
||||
const [standalone, setStandalone] = useState<boolean>(params.standalone ?? false);
|
||||
const [options, setOptions] = useState({});
|
||||
const [url, setUrl] = useState<string | null>(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) {
|
|||
)}
|
||||
</select>
|
||||
</label>
|
||||
{appId !== "" ? <label className={styles.Label}>
|
||||
<p>Standalone:</p>
|
||||
<input
|
||||
className={styles.Input}
|
||||
name="standalone"
|
||||
type="checkbox"
|
||||
checked={standalone}
|
||||
value={standalone.toString()}
|
||||
onChange={onStandaloneChange as unknown as ChangeEventHandler}
|
||||
/>
|
||||
<div className={styles.Checkbox}>
|
||||
{standalone
|
||||
? <FontAwesomeIcon icon={faSquareCheck}/>
|
||||
: <FontAwesomeIcon icon={faSquare}/>
|
||||
}
|
||||
</div>
|
||||
</label> : null}
|
||||
{appId !== "" ? <label className={styles.Label}>
|
||||
<p>Fullscreen:</p>
|
||||
<input
|
||||
|
|
@ -106,6 +130,7 @@ export function Share({ modal, params, ...props }: ModalProps) {
|
|||
name="fullscreen"
|
||||
type="checkbox"
|
||||
checked={fullscreen}
|
||||
disabled={standalone}
|
||||
value={fullscreen.toString()}
|
||||
onChange={onFullscreenChange as unknown as ChangeEventHandler}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Route path="/">
|
||||
<Route index element={<DefaultRoute/>}/>
|
||||
{AppsManager.APPS.map((app: App) =>
|
||||
<Route path={app.id} key={app.id} element={<AppRoute app={app}/>}/>
|
||||
<Route path={app.id} key={app.id} element={<StandaloneRoute app={app}/>}/>
|
||||
)}
|
||||
<Route path="*" element={<NoRoute/>}/>
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -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 <app.WindowContent
|
||||
app={app}
|
||||
setTitle={setTitle}
|
||||
setIconUrl={setIconUrl}
|
||||
active={true}
|
||||
/>;
|
||||
}
|
||||
15
src/components/router/routes/StandaloneRoute.module.css
Normal file
15
src/components/router/routes/StandaloneRoute.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
52
src/components/router/routes/StandaloneRoute.tsx
Normal file
52
src/components/router/routes/StandaloneRoute.tsx
Normal file
|
|
@ -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 <div className={styles.StandaloneView}>
|
||||
<ModalsView/>
|
||||
<StandaloneHeader exit={exit}/>
|
||||
<div className={styles.StandaloneWindow}>
|
||||
<app.WindowContent
|
||||
app={app}
|
||||
setTitle={setTitle}
|
||||
setIconUrl={setIconUrl}
|
||||
active={true}
|
||||
standalone={true}
|
||||
close={() => { exit(); }}
|
||||
{...params}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
69
src/components/windows/StandaloneHeader.module.css
Normal file
69
src/components/windows/StandaloneHeader.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
21
src/components/windows/StandaloneHeader.tsx
Normal file
21
src/components/windows/StandaloneHeader.tsx
Normal file
|
|
@ -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 <header className={styles.Header}>
|
||||
<a className={styles.Logo} href="/" tabIndex={0}>
|
||||
<ReactSVG src={"/assets/icon-mask.svg"}/>
|
||||
<h1>{NAME}</h1>
|
||||
</a>
|
||||
<Button className={styles.ExitButton} onClick={exit} icon={faTimes}>
|
||||
Exit
|
||||
</Button>
|
||||
</header>;
|
||||
}
|
||||
|
|
@ -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<WindowProps> = memo(({ id, app, size, position, onInteract, options, active, fullscreen, minimized, toggleMinimized, index }) => {
|
||||
|
|
@ -61,11 +62,14 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
|
|||
close();
|
||||
}}/>
|
||||
<Divider/>
|
||||
<ClickAction label="Standalone mode" icon={faCircleRight} onTrigger={() => {
|
||||
openUrl(generateUrl({ appId: app.id, standalone: true }), "_self");
|
||||
}}/>
|
||||
<ClickAction label={"Share"} icon={ModalsManager.getModalIconUrl("share")} shortcut={["Alt", "s"]} onTrigger={() => {
|
||||
openWindowedModal({
|
||||
appId: app.id,
|
||||
fullscreen: maximized,
|
||||
size: new Vector2(350, 350),
|
||||
size: new Vector2(350, 400),
|
||||
Modal: (props) => <Share {...props}/>
|
||||
});
|
||||
}}/>
|
||||
|
|
@ -210,6 +214,7 @@ export const WindowView: FC<WindowProps> = memo(({ id, app, size, position, onIn
|
|||
close={close}
|
||||
focus={focus}
|
||||
active={active}
|
||||
standalone={false}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { HTMLAttributeAnchorTarget } from "react";
|
||||
import { NAME } from "../../config/branding.config";
|
||||
|
||||
/**
|
||||
|
|
@ -59,19 +60,29 @@ export function getViewportParams(): Record<string, string> {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
95
src/tools/prep.ts
Normal file
95
src/tools/prep.ts
Normal file
|
|
@ -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) => `
|
||||
<image:image>
|
||||
<image:loc>${BASE_URL.slice(0, -1) + path}</image:loc>
|
||||
</image:image>`
|
||||
);
|
||||
|
||||
const pages = Object.values(APPS).map((appId) => `
|
||||
<url>
|
||||
<loc>${BASE_URL + appId}</loc>
|
||||
<lastmod>${lastModified}</lastmod>
|
||||
</url>`
|
||||
);
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xml>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
<url>
|
||||
<loc>${BASE_URL}</loc>
|
||||
<lastmod>${lastModified}</lastmod>
|
||||
${images.join("")}
|
||||
</url>
|
||||
${pages.join("")}
|
||||
</urlset>`;
|
||||
|
||||
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 = /(?<=(<title>|<meta property="og:title" content="|<meta name="twitter:title" content="))(([a-zA-Z|-]|\s)+)(?=(<\/title>|"\/?>))/g;
|
||||
html = html.replaceAll(titleRegex, `_APP_NAME | ${NAME}`);
|
||||
|
||||
const descriptionRegex = /(?<=(<meta name="description" content="|<meta property="og:description" content="|<meta name="twitter:description" content="))(([a-zA-Z-.]|\s)+)(?=("\/?>))/g;
|
||||
html = html.replaceAll(descriptionRegex, "_APP_DESCRIPTION");
|
||||
|
||||
const canonicalRegex = /(?<=(<link rel="canonical" href="|<meta name="twitter:url" content="|<meta property="og:url" content="))(http(s)?:\/\/[a-zA-Z-.]+\/)(?=("\/?>))/g;
|
||||
html = html.replaceAll(canonicalRegex, `${BASE_URL}_APP_ID`);
|
||||
|
||||
const faqRegex = /<!-- FAQ -->.*?<script type="application\/ld\+json">.*?<\/script>/gs;
|
||||
html = html.replaceAll(faqRegex, "");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
try {
|
||||
const steps: [string, () => string][] = [
|
||||
[PATH_TO_SITEMAP, generateSitemap],
|
||||
[PATH_TO_TEMPLATE, generatePageTemplate],
|
||||
[PATH_TO_PAGES, generatePageRecords],
|
||||
];
|
||||
|
||||
steps.forEach(([path, generateContent]) => {
|
||||
const directory = path.substring(0, path.lastIndexOf("/"));
|
||||
if (directory != "" && !fs.existsSync(directory)){
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(path, generateContent(), { flag: "w+" });
|
||||
console.log(`${ANSI.fg.green}✓ Generated ${path}${ANSI.reset}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { APPS } from "../config/apps.config";
|
||||
import { BASE_URL } from "../config/branding.config";
|
||||
import { WALLPAPERS } from "../config/desktop.config";
|
||||
import fs from "node:fs";
|
||||
|
||||
const PATH_TO_SITEMAP = "public/sitemap.xml";
|
||||
|
||||
function generateSitemap() {
|
||||
const date = new Date();
|
||||
const lastModified = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
||||
|
||||
const images = WALLPAPERS.map((path) => `
|
||||
<image:image>
|
||||
<image:loc>${BASE_URL.slice(0, -1) + path}</image:loc>
|
||||
</image:image>`
|
||||
);
|
||||
|
||||
const pages = Object.values(APPS).map((appId) => `
|
||||
<url>
|
||||
<loc>${BASE_URL + appId}</loc>
|
||||
<lastmod>${lastModified}</lastmod>
|
||||
</url>`
|
||||
);
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xml>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
<url>
|
||||
<loc>${BASE_URL}</loc>
|
||||
<lastmod>${lastModified}</lastmod>
|
||||
${images.join("")}
|
||||
</url>
|
||||
${pages.join("")}
|
||||
</urlset>`;
|
||||
|
||||
return sitemap.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(PATH_TO_SITEMAP, generateSitemap());
|
||||
console.log("Succesfully updated sitemap.xml");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
Loading…
Reference in a new issue