version bump, 2.1.0, some bug fixes and animations and a full on working terminal (WiP)
This commit is contained in:
parent
ee11b4f71b
commit
d4ea37f29e
9 changed files with 780 additions and 143 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue