mobile: iOS-style widget + animations, 2-click desktop icons, new wallpapers

Mobile:
- iOS-style open/close animation for app views (grows from the tapped icon,
  always slides straight down out of view and shrinks on close)
- digital 2x1 clock widget in an iOS-style frosted-glass panel
- new phone wallpaper (phonewallpaper.jpg)
- larger notch

Desktop:
- desktop icons and shortcuts now launch on the second click
- open/close/launcher animations
- windows keep their state when toggling Gruvbox/Desktop themes, and the
  Welcome window only appears on the first page load

Other:
- update wallpaper credits to wallpapercave
- allow LAN hosts in dev (allowedDevOrigins) so phones can load the dev
  bundle instead of getting a 403 and an inert page
This commit is contained in:
Martin Dvořák 2026-09-22 21:02:49 +02:00
commit 6b645ac636
11 changed files with 397 additions and 159 deletions

View file

@ -54,6 +54,7 @@ export default function WindowsDesktop() {
const [activeId, setActiveId] = useState<number | null>(1);
const [startOpen, setStartOpen] = useState(false);
const [selectedIcon, setSelectedIcon] = useState<string | null>(null);
const [closingIds, setClosingIds] = useState<number[]>([]);
const idRef = useRef(2);
const zRef = useRef(11);
@ -65,6 +66,17 @@ export default function WindowsDesktop() {
);
};
// Desktop icons behave like a real desktop: the first click selects the
// icon, a second click on the same icon launches it.
const activateIcon = (id: string, action: () => void) => {
if (selectedIcon === id) {
setSelectedIcon(null);
action();
} else {
setSelectedIcon(id);
}
};
const openApp = (appId: string) => {
setStartOpen(false);
const existing = wins.find((w) => w.appId === appId);
@ -105,8 +117,13 @@ export default function WindowsDesktop() {
setActiveId(id);
};
// Animate the window out first, then drop it from state.
const closeWin = (id: number) =>
setClosingIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
const finishClose = (id: number) => {
setWins((prev) => prev.filter((w) => w.id !== id));
setClosingIds((prev) => prev.filter((x) => x !== id));
};
const minimizeWin = (id: number) =>
setWins((prev) => prev.map((w) => (w.id === id ? { ...w, minimized: true } : w)));
const toggleMax = (id: number) =>
@ -165,10 +182,7 @@ export default function WindowsDesktop() {
key={app.id}
type="button"
className={"win-icon" + (selectedIcon === app.id ? " selected" : "")}
onClick={() => {
setSelectedIcon(app.id);
openApp(app.id);
}}
onClick={() => activateIcon(app.id, () => openApp(app.id))}
title={`Open ${app.title}`}
>
<span className="win-icon-glyph" style={{ background: app.color }}>
@ -184,32 +198,24 @@ export default function WindowsDesktop() {
className="win-icons win-icons-tr"
onPointerDown={(e) => e.stopPropagation()}
>
{SERVICE_SHORTCUTS.map((s) =>
s.appId ? (
<button
key={s.id}
type="button"
className="win-icon"
title={s.label}
onClick={() => openApp(s.appId as string)}
>
<span className="win-icon-glyph">{s.icon}</span>
<span className="win-icon-label">{s.label}</span>
</button>
) : (
<a
key={s.id}
href={s.url}
target="_blank"
rel="noopener noreferrer"
className="win-icon"
title={s.label}
>
<span className="win-icon-glyph">{s.icon}</span>
<span className="win-icon-label">{s.label}</span>
</a>
)
)}
{SERVICE_SHORTCUTS.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")
)
}
>
<span className="win-icon-glyph">{s.icon}</span>
<span className="win-icon-label">{s.label}</span>
</button>
))}
</div>
{/* Friends - bottom right */}
@ -218,19 +224,22 @@ export default function WindowsDesktop() {
onPointerDown={(e) => e.stopPropagation()}
>
{FRIEND_SHORTCUTS.map((f) => (
<a
<button
key={f.id}
href={f.url}
target="_blank"
rel="noopener noreferrer"
className="win-icon"
type="button"
className={"win-icon" + (selectedIcon === f.id ? " selected" : "")}
title={f.label}
onClick={() =>
activateIcon(f.id, () =>
window.open(f.url, "_blank", "noopener,noreferrer")
)
}
>
<span className="win-icon-glyph">
<Image src={f.img} alt={f.label} width={38} height={38} className="win-icon-img" />
</span>
<span className="win-icon-label">{f.label}</span>
</a>
</button>
))}
</div>
@ -242,8 +251,10 @@ export default function WindowsDesktop() {
win={w}
app={APP_BY_ID[w.appId]}
active={activeId === w.id}
closing={closingIds.includes(w.id)}
onFocus={() => focus(w.id)}
onClose={() => closeWin(w.id)}
onCloseAnimationEnd={() => finishClose(w.id)}
onMinimize={() => minimizeWin(w.id)}
onToggleMaximize={() => toggleMax(w.id)}
onMove={(x, y) => moveWin(w.id, x, y)}