embed live service sites (Llama.cpp, Jellyfin) inside desktop windows
This commit is contained in:
parent
eecf8cf339
commit
d5206d6607
5 changed files with 56 additions and 5 deletions
|
|
@ -33,6 +33,9 @@ heatmap**, a gym SBD panel and links to friends' sites.
|
||||||
hidden `ssh` easter egg.
|
hidden `ssh` easter egg.
|
||||||
- **Live service status** — each self-hosted service is health-checked through the site's
|
- **Live service status** — each self-hosted service is health-checked through the site's
|
||||||
own API; Minecraft is checked through mcstatus.io.
|
own API; Minecraft is checked through mcstatus.io.
|
||||||
|
- **Embedded live services** — opening Llama.cpp or Jellyfin from the desktop shows the
|
||||||
|
real site inside the window (an iframe). Services that forbid framing
|
||||||
|
(`X-Frame-Options` / CSP) keep a link card instead.
|
||||||
- **Live GitDXD stats** — contribution heatmap + latest commit activity proxied server-side
|
- **Live GitDXD stats** — contribution heatmap + latest commit activity proxied server-side
|
||||||
from the self-hosted Forgejo instance.
|
from the self-hosted Forgejo instance.
|
||||||
- **Extras** — a playable `Revival Inc.` game embedded at `/revival-inc`, `llms.txt`,
|
- **Extras** — a playable `Revival Inc.` game embedded at `/revival-inc`, `llms.txt`,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ src/app/
|
||||||
MobileHome.tsx → "skeuo" theme, phone: app grid + dock
|
MobileHome.tsx → "skeuo" theme, phone: app grid + dock
|
||||||
WinWindow.tsx → a single draggable/resizable window
|
WinWindow.tsx → a single draggable/resizable window
|
||||||
apps.tsx → APPS registry + the content of each app + in-app terminal
|
apps.tsx → APPS registry + the content of each app + in-app terminal
|
||||||
|
+ embedded-site windows
|
||||||
useDesktopIcons.ts → desktop icon grid + drag/drop (THE KEY FILE, see below)
|
useDesktopIcons.ts → desktop icon grid + drag/drop (THE KEY FILE, see below)
|
||||||
useDragOrder.ts → simple list reordering (mobile home screen only)
|
useDragOrder.ts → simple list reordering (mobile home screen only)
|
||||||
useTheme.ts → theme state via useSyncExternalStore
|
useTheme.ts → theme state via useSyncExternalStore
|
||||||
|
|
@ -210,6 +211,12 @@ list reordering, not free placement. Don't confuse the two.
|
||||||
`eslint-disable-next-line` with a comment. Don't add more without a reason.
|
`eslint-disable-next-line` with a comment. Don't add more without a reason.
|
||||||
- Watch for `pointercancel`: any custom pointer gesture over an `<img>`, link,
|
- Watch for `pointercancel`: any custom pointer gesture over an `<img>`, link,
|
||||||
or text selection needs `draggable=false` / `user-select: none`.
|
or text selection needs `draggable=false` / `user-select: none`.
|
||||||
|
- Service shortcuts can show the real site in an iframe via
|
||||||
|
`createWebsiteApp({ embed: true })` (set on the shortcut's `embed` flag), but
|
||||||
|
**only for sites that don't send `X-Frame-Options` / a `frame-ancestors` CSP**.
|
||||||
|
As of writing only **Llama.cpp** and **Jellyfin** qualify; Navidrome
|
||||||
|
(`DENY`), GitDXD (`SAMEORIGIN`) and SMP/Discord (`DENY`) block framing and
|
||||||
|
keep the link card. Verify headers before enabling `embed` on a new service.
|
||||||
- Reduced motion is handled with a near-zero `animation-duration` (not
|
- Reduced motion is handled with a near-zero `animation-duration` (not
|
||||||
`animation: none`) so `animationend` still fires and windows unmount.
|
`animation: none`) so `animationend` still fires and windows unmount.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ interface Shortcut {
|
||||||
appId?: string;
|
appId?: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
/** Show the real site in an iframe. Only for sites that allow framing. */
|
||||||
|
embed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IconEntry {
|
interface IconEntry {
|
||||||
|
|
@ -36,8 +38,8 @@ interface IconEntry {
|
||||||
|
|
||||||
const SERVICE_SHORTCUTS: Shortcut[] = [
|
const SERVICE_SHORTCUTS: Shortcut[] = [
|
||||||
{ id: "svc-proxmox", icon: "🖥️", label: "Proxmox", appId: "alacritty", color: "#1257a0", desc: "Server Management" },
|
{ id: "svc-proxmox", icon: "🖥️", label: "Proxmox", appId: "alacritty", color: "#1257a0", desc: "Server Management" },
|
||||||
{ id: "svc-ai", icon: "🦾", label: "Llama.cpp", url: "https://ai.dvorakmortonxd.dev/", color: "#157347", desc: "Local AI Server" },
|
{ id: "svc-ai", icon: "🦾", label: "Llama.cpp", url: "https://ai.dvorakmortonxd.dev/", color: "#157347", desc: "Local AI Server", embed: true },
|
||||||
{ id: "svc-jellyfin", icon: "🎬", label: "Jellyfin", url: "https://jelly.dvorakmortonxd.dev/", color: "#6a4fb3", desc: "Local Media Server" },
|
{ id: "svc-jellyfin", icon: "🎬", label: "Jellyfin", url: "https://jelly.dvorakmortonxd.dev/", color: "#6a4fb3", desc: "Local Media Server", embed: true },
|
||||||
{ id: "svc-smp", icon: "🎮", label: "SMP", url: "https://discord.gg/vyvhk7d8bX", color: "#c0392b", desc: "My Survival Minecraft Server" },
|
{ id: "svc-smp", icon: "🎮", label: "SMP", url: "https://discord.gg/vyvhk7d8bX", color: "#c0392b", desc: "My Survival Minecraft Server" },
|
||||||
{ id: "svc-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/", color: "#b3005f", desc: "Music Streaming" },
|
{ id: "svc-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/", color: "#b3005f", desc: "Music Streaming" },
|
||||||
{ id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT" },
|
{ id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT" },
|
||||||
|
|
@ -194,6 +196,7 @@ export default function WindowsDesktop() {
|
||||||
desc: s.desc,
|
desc: s.desc,
|
||||||
icon: s.icon,
|
icon: s.icon,
|
||||||
image: s.image,
|
image: s.image,
|
||||||
|
embed: s.embed,
|
||||||
});
|
});
|
||||||
setDynamicApps((prev) => (prev[id] ? prev : { ...prev, [id]: app }));
|
setDynamicApps((prev) => (prev[id] ? prev : { ...prev, [id]: app }));
|
||||||
openAppDef(app);
|
openAppDef(app);
|
||||||
|
|
|
||||||
|
|
@ -657,6 +657,21 @@ function WebsiteApp({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Renders a site in an iframe. Only usable for sites that allow framing. */
|
||||||
|
function EmbeddedSite({ title, url }: { title: string; url: string }) {
|
||||||
|
return (
|
||||||
|
<div className="website-embed">
|
||||||
|
<iframe
|
||||||
|
className="website-embed-frame"
|
||||||
|
src={url}
|
||||||
|
title={title}
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createWebsiteApp(opts: {
|
export function createWebsiteApp(opts: {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -664,16 +679,23 @@ export function createWebsiteApp(opts: {
|
||||||
desc?: string;
|
desc?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
/**
|
||||||
|
* Render the real site in an iframe instead of the link card. Only works for
|
||||||
|
* sites that do not send `X-Frame-Options` / a `frame-ancestors` CSP.
|
||||||
|
*/
|
||||||
|
embed?: boolean;
|
||||||
}): AppDef {
|
}): AppDef {
|
||||||
return {
|
return {
|
||||||
id: opts.id,
|
id: opts.id,
|
||||||
title: opts.title,
|
title: opts.title,
|
||||||
icon: opts.icon ?? "🌐",
|
icon: opts.icon ?? "🌐",
|
||||||
color: "#0a7cc1",
|
color: "#0a7cc1",
|
||||||
width: 620,
|
width: opts.embed ? 1000 : 620,
|
||||||
height: 440,
|
height: opts.embed ? 640 : 440,
|
||||||
url: opts.url,
|
url: opts.url,
|
||||||
content: (
|
content: opts.embed ? (
|
||||||
|
<EmbeddedSite title={opts.title} url={opts.url} />
|
||||||
|
) : (
|
||||||
<WebsiteApp
|
<WebsiteApp
|
||||||
title={opts.title}
|
title={opts.title}
|
||||||
url={opts.url}
|
url={opts.url}
|
||||||
|
|
|
||||||
|
|
@ -603,6 +603,22 @@ a.win-icon {
|
||||||
box-shadow: 0 0 0 1px #7db9e8, 0 0 12px rgba(95, 175, 255, 0.5);
|
box-shadow: 0 0 0 1px #7db9e8, 0 0 12px rgba(95, 175, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Live service sites rendered inside the window. The site fills the window
|
||||||
|
body edge-to-edge, so the iframe owns all the scrolling. */
|
||||||
|
.website-embed {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.website-embed-frame {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.app-surface .project-grid {
|
.app-surface .project-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue