diff --git a/cmd/opensave-app/frontend/src/components/ConflictModal.svelte b/cmd/opensave-app/frontend/src/components/ConflictModal.svelte index 4a9311ff..f24a7baa 100644 --- a/cmd/opensave-app/frontend/src/components/ConflictModal.svelte +++ b/cmd/opensave-app/frontend/src/components/ConflictModal.svelte @@ -1,6 +1,6 @@
@@ -99,12 +108,22 @@ {peer.status} + {#if peer.needsUpdate} + + needs update + + {/if}
{peer.address === 'relay' ? 'internet relay' : `${peer.address}:${peer.port}`} · last synced {fmtTime(peer.lastSynced)} {#if peer.appVersion}· OpenSave {peer.appVersion}{/if}
+ {#if peer.link} +
+ {linkLabel(peer.link)} +
+ {/if}
diff --git a/cmd/opensave-app/frontend/src/views/game/ConfigTab.svelte b/cmd/opensave-app/frontend/src/views/game/ConfigTab.svelte index 459703f1..8d6f8bfb 100644 --- a/cmd/opensave-app/frontend/src/views/game/ConfigTab.svelte +++ b/cmd/opensave-app/frontend/src/views/game/ConfigTab.svelte @@ -25,8 +25,12 @@ autoSync: game.autoSync ?? true, maxSnapshots: game.maxSnapshots ?? 5, maxManualSnapshots: game.maxManualSnapshots ?? 0, - syncIgnore: game.syncIgnore ?? '' + syncIgnore: game.syncIgnore ?? '', + syncDeviceSettings: game.syncDeviceSettings ?? false }; + // The files the game database names as this game's settings rather than its + // save (graphics, resolution): left out of syncing unless asked for. + $: deviceSettings = game.deviceSettings ?? []; let locations = []; // Cover preview: a custom URL wins, else the proxied Steam art for the App @@ -76,7 +80,8 @@ autoSync: cfg.autoSync, maxSnapshots: Number(cfg.maxSnapshots), maxManualSnapshots: Number(cfg.maxManualSnapshots), - syncIgnore: cfg.syncIgnore ?? '' + syncIgnore: cfg.syncIgnore ?? '', + syncDeviceSettings: !!cfg.syncDeviceSettings }) ); } @@ -160,6 +165,32 @@ can't push them out. 0 = keep forever (the default).
+ {#if deviceSettings.length > 0} +
+ + +
+ + + These are this game's settings, not its save — named by the game database or found by + what they hold — so each device keeps its own (graphics, resolution). They are still in + every snapshot. A file that turns out to change along with the save syncs again by + itself; to sync one yourself, untick it under “Pick from your save folder” below. + + {/if} 0} />
@@ -206,6 +237,29 @@ .fields .field { margin-bottom: 14px; } + .device-settings { + margin: 4px 0 6px; + padding-left: 18px; + color: var(--text-dim, inherit); + } + .device-settings code { + font-size: 0.9em; + } + .device-settings .source { + font-size: 0.82em; + color: var(--text-faint); + margin-left: 6px; + } + .device-settings .given-back code { + text-decoration: line-through; + } + .device-block { + margin-bottom: 4px !important; + } + .device-hint { + display: block; + margin-bottom: 14px; + } .hint-warn { color: var(--warn); } diff --git a/cmd/opensave-app/frontend/src/views/game/GameHeader.svelte b/cmd/opensave-app/frontend/src/views/game/GameHeader.svelte index 19750020..327f2a34 100644 --- a/cmd/opensave-app/frontend/src/views/game/GameHeader.svelte +++ b/cmd/opensave-app/frontend/src/views/game/GameHeader.svelte @@ -13,10 +13,12 @@ import ArrowLeft from 'lucide-svelte/icons/arrow-left'; import Play from 'lucide-svelte/icons/play'; import RefreshCw from 'lucide-svelte/icons/refresh-cw'; + import Upload from 'lucide-svelte/icons/upload'; import FolderOpen from 'lucide-svelte/icons/folder-open'; import Pencil from 'lucide-svelte/icons/pencil'; import Star from 'lucide-svelte/icons/star'; import TriangleAlert from 'lucide-svelte/icons/triangle-alert'; + import Info from 'lucide-svelte/icons/info'; import { collections, isFavourite, toggleFavourite } from '../../lib/collections.js'; export let game; @@ -24,6 +26,14 @@ export let runner; const { busy, run } = runner; + // A game OpenSave cannot find here: its program, chosen by hand, says where + // it is installed and what runs when it is played (sessions, launch). + async function pickProgram() { + const exe = await native.selectFile(`Program of “${game.name}”`); + if (!exe) return; + await run(`Program set for ${game.name}`, () => api.patch(`/api/games/${game.id}`, { exePath: exe })); + } + $: activity = $syncActivity[game.id]; // Is my Deck up to date with THIS save? One entry per paired device, in @@ -42,6 +52,20 @@ const syncNow = () => run('Sync triggered', () => api.post(`/api/games/${game.id}/sync`)); const launchGame = () => run('Launching…', () => api.post(`/api/games/${game.id}/launch`)); + // For when the devices hold states nobody can rank — a copy that arrived + // only half-way, a mixture of two saves — and the person knows which device + // has the right one. Every other device takes this save whole, keeping a + // snapshot of its own first; one that changed the save on its own since is + // still asked. + async function useEverywhere() { + const ok = await askConfirm( + `Make this device's save of ${game.name} the one every other device uses? They each replace theirs with it — every file, including removing files this device does not have — and keep a snapshot of their own first. A device where the save was changed after this update is asked instead.`, + { title: 'Use this save everywhere?', confirmText: 'Use this save everywhere', danger: true } + ); + if (!ok) return; + await run(`${game.name}: this save goes to your other devices`, () => api.post(`/api/games/${game.id}/use-everywhere`)); + } + let editPath = false; let pathDraft = ''; @@ -118,7 +142,12 @@ · played {timeAgo(game.lastPlayedAt, now)}, {playLength(game.playtimeMs)} in all {/if} {#if game.installed === 'not-found'} - · not found on this PC + · {/if}
{#if syncedWith.length > 0} @@ -137,6 +166,7 @@ {#if canLaunch(game)} {/if} + @@ -188,7 +218,18 @@ {/if} -{#if game.savePathMissing && !editPath} +{#if game.savePathMissing && game.saveDriveMissing && !editPath} + +
+ +
+ This game isn't on this device. + Its saves are on {game.savePath.slice(0, 2)}, which this PC doesn't have, so it's skipped here and keeps syncing + between your other devices. If it is installed here, point the game at its save folder with Edit. +
+
+{:else if game.savePathMissing && !editPath} @@ -204,6 +245,15 @@ {/if}