version bump, 2.1.0, some bug fixes and animations and a full on working terminal (WiP)

This commit is contained in:
dvorakmortonxd 2026-09-23 14:40:21 +02:00
commit d4ea37f29e
9 changed files with 780 additions and 143 deletions

View file

@ -1,37 +1,46 @@
"use client";
import { useRef, useState } from "react";
import { useRef, useState, type ReactNode } from "react";
import Image from "next/image";
import { APPS, APP_BY_ID } from "./apps";
import { APPS, APP_BY_ID, createWebsiteApp, type AppDef } from "./apps";
import WinWindow, { type WinState } from "./WinWindow";
import Clock from "./Clock";
import { useTheme } from "./useTheme";
import { useDragOrder } from "./useDragOrder";
import { WALLPAPER, friends } from "../data";
const TASKBAR_HEIGHT = 48;
const LEFT_APPS = APPS.filter((a) => a.id !== "services" && a.id !== "friends");
const SERVICE_SHORTCUTS: {
interface Shortcut {
id: string;
icon: string;
icon: ReactNode;
label: string;
color: string;
url?: string;
appId?: string;
}[] = [
{ id: "svc-proxmox", icon: "🖥️", label: "Proxmox", appId: "alacritty" },
{ id: "svc-ai", icon: "🦾", label: "Llama.cpp", url: "https://ai.dvorakmortonxd.dev/" },
{ id: "svc-jellyfin", icon: "🎬", label: "Jellyfin", url: "https://jelly.dvorakmortonxd.dev/" },
{ id: "svc-smp", icon: "🎮", label: "SMP", url: "https://discord.gg/vyvhk7d8bX" },
{ id: "svc-navidrome", icon: "🎸", label: "Navidrome", url: "https://navidrome.dvorakmortonxd.dev/" },
{ id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev" },
desc?: string;
image?: string;
}
const SERVICE_SHORTCUTS: Shortcut[] = [
{ 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-jellyfin", icon: "🎬", label: "Jellyfin", url: "https://jelly.dvorakmortonxd.dev/", color: "#6a4fb3", desc: "Local Media 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-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT" },
];
const FRIEND_SHORTCUTS = friends.map((f, i) => ({
const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({
id: `friend-${i}`,
label: f.name,
url: f.realUrl || f.url,
img: f.img,
color: "#c0392b",
icon: "🫂",
image: f.img,
desc: f.desc,
}));
export default function WindowsDesktop() {
@ -56,6 +65,18 @@ export default function WindowsDesktop() {
const [startClosing, setStartClosing] = useState(false);
const [selectedIcon, setSelectedIcon] = useState<string | null>(null);
const [closingIds, setClosingIds] = useState<number[]>([]);
const [minimizingIds, setMinimizingIds] = useState<number[]>([]);
const [leftApps, setLeftApps] = useState(LEFT_APPS);
const [serviceItems, setServiceItems] = useState(SERVICE_SHORTCUTS);
const [friendItems, setFriendItems] = useState(FRIEND_SHORTCUTS);
const leftDrag = useDragOrder(setLeftApps);
const serviceDrag = useDragOrder(setServiceItems);
const friendDrag = useDragOrder(setFriendItems);
const [dynamicApps, setDynamicApps] = useState<Record<string, AppDef>>({});
const getApp = (id: string): AppDef | undefined => APP_BY_ID[id] ?? dynamicApps[id];
const idRef = useRef(2);
const zRef = useRef(11);
@ -78,9 +99,9 @@ export default function WindowsDesktop() {
}
};
const openApp = (appId: string) => {
const openAppDef = (app: AppDef) => {
if (startOpen) setStartClosing(true);
const existing = wins.find((w) => w.appId === appId);
const existing = wins.find((w) => w.appId === app.id);
if (existing) {
setWins((prev) =>
prev.map((w) =>
@ -90,8 +111,6 @@ export default function WindowsDesktop() {
setActiveId(existing.id);
return;
}
const app = APP_BY_ID[appId];
if (!app) return;
const id = idRef.current++;
const vw = window.innerWidth;
const vh = window.innerHeight - TASKBAR_HEIGHT;
@ -104,7 +123,7 @@ export default function WindowsDesktop() {
...prev,
{
id,
appId,
appId: app.id,
x,
y,
w: app.width,
@ -118,6 +137,35 @@ export default function WindowsDesktop() {
setActiveId(id);
};
const openApp = (appId: string) => {
const app = getApp(appId);
if (app) openAppDef(app);
};
// Service and friend shortcuts either open an app (Proxmox) or an in-desktop
// window representing the external website, with a www button to actually go
// there.
const openShortcut = (s: Shortcut) => {
if (s.appId) {
openApp(s.appId);
return;
}
if (!s.url) return;
const id = `web:${s.url}`;
const app =
getApp(id) ??
createWebsiteApp({
id,
title: s.label,
url: s.url,
desc: s.desc,
icon: s.icon,
image: s.image,
});
setDynamicApps((prev) => (prev[id] ? prev : { ...prev, [id]: app }));
openAppDef(app);
};
const closeStart = () => {
if (startOpen && !startClosing) setStartClosing(true);
};
@ -140,8 +188,13 @@ export default function WindowsDesktop() {
setWins((prev) => prev.filter((w) => w.id !== id));
setClosingIds((prev) => prev.filter((x) => x !== id));
};
// Minimize plays the same animation as closing, then hides the window.
const minimizeWin = (id: number) =>
setMinimizingIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
const finishMinimize = (id: number) => {
setWins((prev) => prev.map((w) => (w.id === id ? { ...w, minimized: true } : w)));
setMinimizingIds((prev) => prev.filter((x) => x !== id));
};
const toggleMax = (id: number) =>
setWins((prev) =>
prev.map((w) => (w.id === id ? { ...w, maximized: !w.maximized } : w))
@ -162,6 +215,11 @@ export default function WindowsDesktop() {
)
);
};
const resizeWin = (id: number, x: number, y: number, w: number, h: number) => {
setWins((prev) =>
prev.map((win) => (win.id === id ? { ...win, centered: false, x, y, w, h } : win))
);
};
const taskbarClick = (w: WinState) => {
if (w.minimized) {
@ -178,6 +236,11 @@ export default function WindowsDesktop() {
const wallpaper = `url('${WALLPAPER}')`;
const iconClick = (drag: ReturnType<typeof useDragOrder>, id: string, action: () => void) => {
if (drag.consumeDragged()) return;
activateIcon(id, action);
};
return (
<div
className="win-desktop"
@ -193,12 +256,18 @@ export default function WindowsDesktop() {
{/* Desktop icons */}
<div className="win-icons" onPointerDown={(e) => e.stopPropagation()}>
{LEFT_APPS.map((app) => (
{leftApps.map((app) => (
<button
key={app.id}
type="button"
className={"win-icon" + (selectedIcon === app.id ? " selected" : "")}
onClick={() => activateIcon(app.id, () => openApp(app.id))}
data-drag-id={app.id}
className={
"win-icon" +
(selectedIcon === app.id ? " selected" : "") +
(leftDrag.dragId === app.id ? " dragging" : "")
}
onPointerDown={leftDrag.onPointerDown(app.id)}
onClick={() => iconClick(leftDrag, app.id, () => openApp(app.id))}
title={`Open ${app.title}`}
>
<span className="win-icon-glyph" style={{ background: app.color }}>
@ -214,19 +283,19 @@ export default function WindowsDesktop() {
className="win-icons win-icons-tr"
onPointerDown={(e) => e.stopPropagation()}
>
{SERVICE_SHORTCUTS.map((s) => (
{serviceItems.map((s) => (
<button
key={s.id}
type="button"
className={"win-icon" + (selectedIcon === s.id ? " selected" : "")}
title={s.label}
onClick={() =>
activateIcon(s.id, () =>
s.appId
? openApp(s.appId)
: window.open(s.url, "_blank", "noopener,noreferrer")
)
data-drag-id={s.id}
className={
"win-icon" +
(selectedIcon === s.id ? " selected" : "") +
(serviceDrag.dragId === s.id ? " dragging" : "")
}
onPointerDown={serviceDrag.onPointerDown(s.id)}
title={s.label}
onClick={() => iconClick(serviceDrag, s.id, () => openShortcut(s))}
>
<span className="win-icon-glyph">{s.icon}</span>
<span className="win-icon-label">{s.label}</span>
@ -239,20 +308,24 @@ export default function WindowsDesktop() {
className="win-icons win-icons-br"
onPointerDown={(e) => e.stopPropagation()}
>
{FRIEND_SHORTCUTS.map((f) => (
{friendItems.map((f) => (
<button
key={f.id}
type="button"
className={"win-icon" + (selectedIcon === f.id ? " selected" : "")}
title={f.label}
onClick={() =>
activateIcon(f.id, () =>
window.open(f.url, "_blank", "noopener,noreferrer")
)
data-drag-id={f.id}
className={
"win-icon" +
(selectedIcon === f.id ? " selected" : "") +
(friendDrag.dragId === f.id ? " dragging" : "")
}
onPointerDown={friendDrag.onPointerDown(f.id)}
title={f.label}
onClick={() => iconClick(friendDrag, f.id, () => openShortcut(f))}
>
<span className="win-icon-glyph">
<Image src={f.img} alt={f.label} width={38} height={38} className="win-icon-img" />
{f.image && (
<Image src={f.image} alt={f.label} width={38} height={38} className="win-icon-img" />
)}
</span>
<span className="win-icon-label">{f.label}</span>
</button>
@ -265,15 +338,18 @@ export default function WindowsDesktop() {
<WinWindow
key={w.id}
win={w}
app={APP_BY_ID[w.appId]}
app={getApp(w.appId) ?? APP_BY_ID.welcome}
active={activeId === w.id}
closing={closingIds.includes(w.id)}
minimizing={minimizingIds.includes(w.id)}
onFocus={() => focus(w.id)}
onClose={() => closeWin(w.id)}
onCloseAnimationEnd={() => finishClose(w.id)}
onMinimize={() => minimizeWin(w.id)}
onMinimizeAnimationEnd={() => finishMinimize(w.id)}
onToggleMaximize={() => toggleMax(w.id)}
onMove={(x, y) => moveWin(w.id, x, y)}
onResize={(x, y, width, height) => resizeWin(w.id, x, y, width, height)}
/>
))}
</div>
@ -294,7 +370,7 @@ export default function WindowsDesktop() {
<div className="kickoff-content">
<div className="kickoff-section-title">Applications</div>
<div className="kickoff-list">
{LEFT_APPS.map((app) => (
{leftApps.map((app) => (
<button
key={app.id}
type="button"
@ -305,49 +381,37 @@ export default function WindowsDesktop() {
<span className="kickoff-label">{app.title}</span>
</button>
))}
{SERVICE_SHORTCUTS.map((s) =>
s.appId ? (
<button
key={s.id}
type="button"
className="kickoff-item"
onClick={() => openApp(s.appId as string)}
>
<span className="kickoff-icon">{s.icon}</span>
<span className="kickoff-label">{s.label}</span>
</button>
) : (
<a
key={s.id}
className="kickoff-item"
href={s.url}
target="_blank"
rel="noopener noreferrer"
>
<span className="kickoff-icon">{s.icon}</span>
<span className="kickoff-label">{s.label}</span>
</a>
)
)}
{FRIEND_SHORTCUTS.map((f) => (
<a
key={f.id}
{serviceItems.map((s) => (
<button
key={s.id}
type="button"
className="kickoff-item"
href={f.url}
target="_blank"
rel="noopener noreferrer"
onClick={() => openShortcut(s)}
>
<span className="kickoff-icon">{s.icon}</span>
<span className="kickoff-label">{s.label}</span>
</button>
))}
{friendItems.map((f) => (
<button
key={f.id}
type="button"
className="kickoff-item"
onClick={() => openShortcut(f)}
>
<span className="kickoff-icon">
<Image
src={f.img}
alt={f.label}
width={20}
height={20}
className="kickoff-icon-img"
/>
{f.image && (
<Image
src={f.image}
alt={f.label}
width={20}
height={20}
className="kickoff-icon-img"
/>
)}
</span>
<span className="kickoff-label">{f.label}</span>
</a>
</button>
))}
</div>
</div>
@ -370,7 +434,7 @@ export default function WindowsDesktop() {
<div className="win-tb-buttons">
{wins.map((w) => {
const app = APP_BY_ID[w.appId];
const app = getApp(w.appId) ?? APP_BY_ID.welcome;
return (
<button
key={w.id}