add app versioning, state-aware update button, disable pinch zoom

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 21:31:14 +01:00
parent b50fde1af5
commit 7337f38710
9 changed files with 173 additions and 57 deletions

View File

@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<title>WhatToPlay</title>
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/whattoplay/icons/icon-192.png" />
</head>
<body>
<div id="root"></div>

View File

@@ -1,7 +1,7 @@
{
"name": "whattoplay",
"private": true,
"version": "2026.03.01",
"version": "2026.03.02",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,21 +0,0 @@
{
"name": "WhatToPlay",
"short_name": "WhatToPlay",
"description": "Manage your game library across platforms",
"start_url": "/",
"display": "standalone",
"theme_color": "#0f172a",
"background_color": "#0f172a",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "whattoplay-server",
"private": true,
"version": "2026.03.01",
"version": "2026.03.02",
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts",

View File

@@ -1,8 +1,13 @@
import { useRegisterSW } from "virtual:pwa-register/react"
import { Badge } from "@/shared/components/ui/badge"
import { Card } from "@/shared/components/ui/card"
import { Button } from "@/shared/components/ui/button"
import { ListItem } from "@/shared/components/ui/list-item"
import { useConfig } from "@/shared/db/hooks"
import { t } from "@/shared/i18n"
import { Link } from "@tanstack/react-router"
import { api } from "@/shared/lib/api"
import { useNavigate } from "@tanstack/react-router"
import { Loader2 } from "lucide-react"
import { useState } from "react"
const providers = [
{ id: "steam", label: "Steam" },
@@ -12,6 +17,14 @@ const providers = [
export function SettingsList() {
const steamConfig = useConfig<{ apiKey: string; steamId: string }>("steam")
const gogConfig = useConfig<{ accessToken: string }>("gog")
const navigate = useNavigate()
const [testState, setTestState] = useState<"idle" | "testing" | "ok" | "failed">("idle")
const {
needRefresh: [needRefresh],
updateServiceWorker,
} = useRegisterSW()
const isConnected = (id: string) => {
if (id === "steam") return Boolean(steamConfig?.apiKey)
@@ -19,34 +32,106 @@ export function SettingsList() {
return false
}
return (
<div className="space-y-6">
<section>
<h2 className="mb-3 text-sm font-medium text-muted-foreground">
{t("settings.providers")}
</h2>
<div className="space-y-2">
{providers.map((p) => (
<Link key={p.id} to="/settings/$provider" params={{ provider: p.id }}>
<Card className="flex items-center justify-between p-4">
<span className="font-medium">{p.label}</span>
<Badge variant={isConnected(p.id) ? "default" : "secondary"}>
{isConnected(p.id) ? "Connected" : "Not configured"}
</Badge>
</Card>
</Link>
))}
</div>
</section>
const handleTestConnection = async () => {
setTestState("testing")
try {
const res = await api.health.$get()
if (res.ok) {
setTestState("ok")
} else {
setTestState("failed")
}
} catch {
setTestState("failed")
}
}
<section>
<h2 className="mb-3 text-sm font-medium text-muted-foreground">{t("settings.data")}</h2>
<Link to="/settings/$provider" params={{ provider: "data" }}>
<Card className="p-4">
<span className="font-medium">{t("settings.data")}</span>
</Card>
</Link>
</section>
return (
<div className="px-4">
<h3 className="pt-4 pb-1 text-xs font-medium uppercase text-muted-foreground">
{t("settings.app")}
</h3>
<div className="divide-y rounded-lg border bg-card">
<ListItem
title={needRefresh ? t("settings.updateAvailable") : t("settings.appUpToDate")}
after={
needRefresh ? (
<Button size="sm" onClick={() => updateServiceWorker()}>
{t("settings.updateApp")}
</Button>
) : (
<span className="text-sm text-muted-foreground">{t("settings.upToDate")}</span>
)
}
/>
</div>
<p className="px-1 pt-1 text-xs text-muted-foreground/60">v{__APP_VERSION__}</p>
<h3 className="pt-4 pb-1 text-xs font-medium uppercase text-muted-foreground">
{t("settings.server")}
</h3>
<div className="divide-y rounded-lg border bg-card">
<ListItem
title={t("settings.connection")}
after={
<div className="flex items-center gap-2">
{testState === "testing" && <Loader2 className="h-5 w-5 animate-spin" />}
{testState === "ok" && (
<span className="text-sm font-medium text-green-600">
{t("settings.connectionOk")}
</span>
)}
{testState === "failed" && (
<span className="text-sm font-medium text-red-500">
{t("settings.connectionFailed")}
</span>
)}
<Button
size="sm"
variant="outline"
onClick={handleTestConnection}
disabled={testState === "testing"}
>
{t("settings.testConnection")}
</Button>
</div>
}
/>
</div>
<h3 className="pt-4 pb-1 text-xs font-medium uppercase text-muted-foreground">
{t("settings.providers")}
</h3>
<div className="divide-y rounded-lg border bg-card">
{providers.map((p) => (
<ListItem
key={p.id}
link
title={p.label}
after={
<Badge
className={
isConnected(p.id) ? "bg-green-500 text-white" : "bg-muted text-muted-foreground"
}
>
{isConnected(p.id) ? "Connected" : "Not configured"}
</Badge>
}
onClick={() => navigate({ to: "/settings/$provider", params: { provider: p.id } })}
/>
))}
</div>
<h3 className="pt-4 pb-1 text-xs font-medium uppercase text-muted-foreground">
{t("settings.data")}
</h3>
<div className="divide-y rounded-lg border bg-card">
<ListItem
link
title={t("settings.data")}
onClick={() => navigate({ to: "/settings/$provider", params: { provider: "data" } })}
/>
</div>
</div>
)
}

View File

@@ -33,6 +33,8 @@ export const de: Record<TranslationKey, string> = {
"settings.data": "Daten",
"settings.steam": "Steam",
"settings.gog": "GOG",
"settings.steam.instructions":
"Du brauchst einen Steam Web API Key und deine Steam-ID. Registriere dir kostenlos einen API Key im Steam-Entwicklerportal und füge ihn zusammen mit deiner Steam-ID oder Profil-URL unten ein.",
"settings.steam.apiKey": "API-Schlüssel",
"settings.steam.steamId": "Steam-ID",
"settings.steam.sync": "Spiele synchronisieren",
@@ -43,8 +45,20 @@ export const de: Record<TranslationKey, string> = {
"settings.data.import": "Daten importieren",
"settings.data.clear": "Alle Daten löschen",
"settings.data.clearConfirm": "Bist du sicher? Alle Spiele und Playlisten werden gelöscht.",
"settings.app": "App",
"settings.updateAvailable": "Update verfügbar",
"settings.appUpToDate": "App ist aktuell",
"settings.updateApp": "Aktualisieren",
"settings.upToDate": "✓ Aktuell",
"settings.server": "Server",
"settings.connection": "Verbindung",
"settings.testConnection": "Testen",
"settings.connectionOk": "OK",
"settings.connectionFailed": "Fehlgeschlagen",
"settings.lastSync": "Letzte Synchronisierung",
"settings.syncing": "Synchronisiere...",
"settings.syncFetching": "Spiele werden vom Server geladen...",
"settings.syncSaving": "Speichere {current} / {total} Spiele...",
"settings.syncSuccess": "{count} Spiele synchronisiert",
"state.not_set": "Nicht gesetzt",

View File

@@ -36,6 +36,8 @@ export const en = {
"settings.data": "Data",
"settings.steam": "Steam",
"settings.gog": "GOG",
"settings.steam.instructions":
"You need a Steam Web API key and your Steam ID. Register for a free API key on the Steam developer portal, then paste it below along with your Steam ID or profile URL.",
"settings.steam.apiKey": "API Key",
"settings.steam.steamId": "Steam ID",
"settings.steam.sync": "Sync Games",
@@ -46,8 +48,20 @@ export const en = {
"settings.data.import": "Import Data",
"settings.data.clear": "Clear All Data",
"settings.data.clearConfirm": "Are you sure? This will delete all games and playlists.",
"settings.app": "App",
"settings.updateAvailable": "Update available",
"settings.appUpToDate": "App is up to date",
"settings.updateApp": "Update",
"settings.upToDate": "✓ Up to date",
"settings.server": "Server",
"settings.connection": "Connection",
"settings.testConnection": "Test",
"settings.connectionOk": "OK",
"settings.connectionFailed": "Failed",
"settings.lastSync": "Last sync",
"settings.syncing": "Syncing...",
"settings.syncFetching": "Fetching games from server...",
"settings.syncSaving": "Saving {current} / {total} games...",
"settings.syncSuccess": "Synced {count} games",
// Game states

2
src/vite-env.d.ts vendored
View File

@@ -1,5 +1,7 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string
declare module "*.sql?raw" {
const content: string
export default content

View File

@@ -1,16 +1,37 @@
import { execSync } from "node:child_process"
import { readFileSync } from "node:fs"
import tailwindcss from "@tailwindcss/vite"
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import { VitePWA } from "vite-plugin-pwa"
const pkg = JSON.parse(readFileSync("./package.json", "utf-8"))
const gitCount = execSync("git rev-list --count HEAD", { encoding: "utf-8" }).trim()
export default defineConfig({
base: "/whattoplay/",
define: {
__APP_VERSION__: JSON.stringify(`${pkg.version}+${gitCount}`),
},
plugins: [
TanStackRouterVite(),
react(),
tailwindcss(),
VitePWA({
registerType: "autoUpdate",
registerType: "prompt",
manifest: {
name: "WhatToPlay",
short_name: "WhatToPlay",
description: "Manage your game library across platforms",
theme_color: "#0f172a",
background_color: "#0f172a",
display: "standalone",
icons: [
{ src: "icons/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "icons/icon-512.png", sizes: "512x512", type: "image/png" },
],
},
workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,wasm,data}"],
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
@@ -24,9 +45,10 @@ export default defineConfig({
},
server: {
proxy: {
"/api": {
"/whattoplay/api": {
target: "http://localhost:3001",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/whattoplay\/api/, ""),
},
},
},