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:
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "whattoplay",
|
||||
"private": true,
|
||||
"version": "2026.03.01",
|
||||
"version": "2026.03.02",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTestState("testing")
|
||||
try {
|
||||
const res = await api.health.$get()
|
||||
if (res.ok) {
|
||||
setTestState("ok")
|
||||
} else {
|
||||
setTestState("failed")
|
||||
}
|
||||
} catch {
|
||||
setTestState("failed")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<h2 className="mb-3 text-sm font-medium text-muted-foreground">
|
||||
<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")}
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
</h3>
|
||||
<div className="divide-y rounded-lg border bg-card">
|
||||
{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"}>
|
||||
<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>
|
||||
</Card>
|
||||
</Link>
|
||||
}
|
||||
onClick={() => navigate({ to: "/settings/$provider", params: { provider: p.id } })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
2
src/vite-env.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string
|
||||
|
||||
declare module "*.sql?raw" {
|
||||
const content: string
|
||||
export default content
|
||||
|
||||
@@ -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/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user