added ability to drag apps wherever on desktop theme
This commit is contained in:
parent
d4ea37f29e
commit
cd1766619d
3 changed files with 566 additions and 83 deletions
|
|
@ -6,7 +6,7 @@ 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 { useDesktopIcons } from "./useDesktopIcons";
|
||||
import { WALLPAPER, friends } from "../data";
|
||||
|
||||
const TASKBAR_HEIGHT = 48;
|
||||
|
|
@ -24,6 +24,16 @@ interface Shortcut {
|
|||
image?: string;
|
||||
}
|
||||
|
||||
interface IconEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
title: string;
|
||||
glyph: ReactNode;
|
||||
color?: string;
|
||||
image?: string;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
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" },
|
||||
|
|
@ -43,6 +53,13 @@ const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({
|
|||
desc: f.desc,
|
||||
}));
|
||||
|
||||
/** Stable list of every desktop icon, used to seed the placement grid. */
|
||||
const ICON_KEYS = [
|
||||
...LEFT_APPS.map((a) => a.id),
|
||||
...SERVICE_SHORTCUTS.map((s) => s.id),
|
||||
...FRIEND_SHORTCUTS.map((f) => f.id),
|
||||
];
|
||||
|
||||
export default function WindowsDesktop() {
|
||||
const { theme, toggle } = useTheme();
|
||||
|
||||
|
|
@ -67,13 +84,13 @@ export default function WindowsDesktop() {
|
|||
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 leftApps = LEFT_APPS;
|
||||
const serviceItems = SERVICE_SHORTCUTS;
|
||||
const friendItems = FRIEND_SHORTCUTS;
|
||||
|
||||
const leftDrag = useDragOrder(setLeftApps);
|
||||
const serviceDrag = useDragOrder(setServiceItems);
|
||||
const friendDrag = useDragOrder(setFriendItems);
|
||||
const desktopRef = useRef<HTMLDivElement>(null);
|
||||
const { positions, dragId, onPointerDown, consumeDrag } =
|
||||
useDesktopIcons(desktopRef, ICON_KEYS);
|
||||
|
||||
const [dynamicApps, setDynamicApps] = useState<Record<string, AppDef>>({});
|
||||
const getApp = (id: string): AppDef | undefined => APP_BY_ID[id] ?? dynamicApps[id];
|
||||
|
|
@ -236,13 +253,79 @@ export default function WindowsDesktop() {
|
|||
|
||||
const wallpaper = `url('${WALLPAPER}')`;
|
||||
|
||||
const iconClick = (drag: ReturnType<typeof useDragOrder>, id: string, action: () => void) => {
|
||||
if (drag.consumeDragged()) return;
|
||||
const iconClick = (id: string, action: () => void) => {
|
||||
if (consumeDrag()) return;
|
||||
activateIcon(id, action);
|
||||
};
|
||||
|
||||
const leftEntries: IconEntry[] = leftApps.map((app) => ({
|
||||
id: app.id,
|
||||
label: app.title,
|
||||
title: `Open ${app.title}`,
|
||||
glyph: app.icon,
|
||||
color: app.color,
|
||||
onOpen: () => openApp(app.id),
|
||||
}));
|
||||
const serviceEntries: IconEntry[] = serviceItems.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
title: s.label,
|
||||
glyph: s.icon,
|
||||
onOpen: () => openShortcut(s),
|
||||
}));
|
||||
const friendEntries: IconEntry[] = friendItems.map((f) => ({
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
title: f.label,
|
||||
glyph: "🫂",
|
||||
image: f.image,
|
||||
onOpen: () => openShortcut(f),
|
||||
}));
|
||||
const allEntries = [...leftEntries, ...serviceEntries, ...friendEntries];
|
||||
|
||||
const renderIcon = (entry: IconEntry, interactive = true) => {
|
||||
const pos = positions[entry.id];
|
||||
return (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
data-drag-id={entry.id}
|
||||
className={
|
||||
"win-icon" +
|
||||
(selectedIcon === entry.id ? " selected" : "") +
|
||||
(dragId === entry.id ? " dragging" : "")
|
||||
}
|
||||
style={interactive && pos ? { left: pos.x, top: pos.y } : undefined}
|
||||
onPointerDown={interactive ? onPointerDown(entry.id) : undefined}
|
||||
onClick={interactive ? () => iconClick(entry.id, entry.onOpen) : undefined}
|
||||
tabIndex={interactive ? undefined : -1}
|
||||
title={entry.title}
|
||||
>
|
||||
<span
|
||||
className="win-icon-glyph"
|
||||
style={entry.color ? { background: entry.color } : undefined}
|
||||
>
|
||||
{entry.image ? (
|
||||
<Image
|
||||
src={entry.image}
|
||||
alt={entry.label}
|
||||
width={38}
|
||||
height={38}
|
||||
className="win-icon-img"
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
entry.glyph
|
||||
)}
|
||||
</span>
|
||||
<span className="win-icon-label">{entry.label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={desktopRef}
|
||||
className="win-desktop"
|
||||
style={{ backgroundImage: wallpaper }}
|
||||
onPointerDown={(e) => {
|
||||
|
|
@ -254,83 +337,20 @@ export default function WindowsDesktop() {
|
|||
>
|
||||
<div className="win-wallpaper-tint" aria-hidden="true" />
|
||||
|
||||
{/* Desktop icons */}
|
||||
<div className="win-icons" onPointerDown={(e) => e.stopPropagation()}>
|
||||
{leftApps.map((app) => (
|
||||
<button
|
||||
key={app.id}
|
||||
type="button"
|
||||
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 }}>
|
||||
{app.icon}
|
||||
</span>
|
||||
<span className="win-icon-label">{app.title}</span>
|
||||
</button>
|
||||
))}
|
||||
{/* Desktop icons. The three stacks below are rendered once so the grid
|
||||
can be measured from them, then the same icons are shown as an
|
||||
absolutely positioned grid. */}
|
||||
<div className="win-icons win-measure" aria-hidden="true">
|
||||
{leftEntries.map((e) => renderIcon(e, false))}
|
||||
</div>
|
||||
<div className="win-icons win-icons-tr win-measure" aria-hidden="true">
|
||||
{serviceEntries.map((e) => renderIcon(e, false))}
|
||||
</div>
|
||||
<div className="win-icons win-icons-br win-measure" aria-hidden="true">
|
||||
{friendEntries.map((e) => renderIcon(e, false))}
|
||||
</div>
|
||||
|
||||
{/* Services - top right */}
|
||||
<div
|
||||
className="win-icons win-icons-tr"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{serviceItems.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Friends - bottom right */}
|
||||
<div
|
||||
className="win-icons win-icons-br"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{friendItems.map((f) => (
|
||||
<button
|
||||
key={f.id}
|
||||
type="button"
|
||||
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">
|
||||
{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>
|
||||
))}
|
||||
</div>
|
||||
<div className="win-icon-layer">{allEntries.map((e) => renderIcon(e))}</div>
|
||||
|
||||
{/* Windows */}
|
||||
<div className="win-layer">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue