Android build v1.1.14
This commit is contained in:
+148
-31
@@ -14,9 +14,12 @@ import {
|
||||
type CharacterProfile,
|
||||
} from './profile'
|
||||
import {
|
||||
applyCloudSaveSync,
|
||||
getCloudSyncStatus,
|
||||
getGameMode,
|
||||
syncCloudSave,
|
||||
previewCloudSaveSync,
|
||||
type CloudSyncChoice,
|
||||
type CloudSyncComparison,
|
||||
type GameMode,
|
||||
} from './gameRepository'
|
||||
import { focusFirstControl, useGameAction } from './input.tsx'
|
||||
@@ -99,6 +102,21 @@ type RoguelikeNavPosition = {
|
||||
column: number
|
||||
}
|
||||
|
||||
function formatSaveTimestamp(updatedAt: number | null) {
|
||||
if (!updatedAt) return 'Unknown legacy timestamp'
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(new Date(updatedAt))
|
||||
}
|
||||
|
||||
function cloudSyncRelationText(comparison: CloudSyncComparison) {
|
||||
if (comparison.relation === 'server-newer') return 'Server save is newer than this device.'
|
||||
if (comparison.relation === 'local-newer') return 'This device save is newer than the server.'
|
||||
if (comparison.relation === 'same') return 'Server and this device have the same save time.'
|
||||
return 'One save has no timestamp yet. Choose the copy you want to keep.'
|
||||
}
|
||||
|
||||
function activityInitials(name: string) {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
@@ -148,6 +166,7 @@ function App() {
|
||||
const [error, setError] = useState('')
|
||||
const [syncingCloud, setSyncingCloud] = useState(false)
|
||||
const [syncMessage, setSyncMessage] = useState('')
|
||||
const [syncComparison, setSyncComparison] = useState<CloudSyncComparison | null>(null)
|
||||
const [homeSelectedIndex, setHomeSelectedIndex] = useState(0)
|
||||
const [dungeonSelectedIndex, setDungeonSelectedIndex] = useState(0)
|
||||
const [roguelikeSelectedIndex, setRoguelikeSelectedIndex] = useState(1)
|
||||
@@ -352,6 +371,7 @@ function App() {
|
||||
setGameMode(getGameMode())
|
||||
setScreen('menu')
|
||||
setSyncMessage('')
|
||||
setSyncComparison(null)
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'Unable to sign out.')
|
||||
}
|
||||
@@ -360,11 +380,29 @@ function App() {
|
||||
async function syncSaveNow() {
|
||||
setSyncingCloud(true)
|
||||
setSyncMessage('')
|
||||
setSyncComparison(null)
|
||||
try {
|
||||
const updated = await syncCloudSave()
|
||||
const comparison = await previewCloudSaveSync()
|
||||
setSyncComparison(comparison)
|
||||
setSyncMessage('')
|
||||
} catch (reason) {
|
||||
setSyncMessage(reason instanceof Error ? reason.message : 'Unable to sync cloud save.')
|
||||
} finally {
|
||||
setSyncingCloud(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function keepCloudSave(choice: CloudSyncChoice) {
|
||||
setSyncingCloud(true)
|
||||
setSyncMessage(choice === 'local' ? 'Uploading this device save...' : 'Downloading server save...')
|
||||
try {
|
||||
const updated = await applyCloudSaveSync(choice)
|
||||
setProfile(updated)
|
||||
setGameMode(getGameMode())
|
||||
setSyncMessage('Cloud save updated.')
|
||||
setSyncComparison(null)
|
||||
setSyncMessage(choice === 'local'
|
||||
? 'Server now uses this device save.'
|
||||
: 'This device now uses the server save.')
|
||||
} catch (reason) {
|
||||
setSyncMessage(reason instanceof Error ? reason.message : 'Unable to sync cloud save.')
|
||||
} finally {
|
||||
@@ -374,13 +412,22 @@ function App() {
|
||||
|
||||
const cloudSync = getCloudSyncStatus()
|
||||
const canShowCloudSync = Boolean(account && account.id !== -1 && cloudSync.available)
|
||||
const homeMenuOffset = canShowCloudSync ? 1 : 0
|
||||
const cloudSyncEntryCount = canShowCloudSync ? (syncComparison ? 3 : 1) : 0
|
||||
const homeMenuOffset = cloudSyncEntryCount
|
||||
const homeMenuEntryCount = MENU_ITEMS.length + homeMenuOffset
|
||||
const homeActiveIndex = Math.min(homeSelectedIndex, homeMenuEntryCount - 1)
|
||||
|
||||
function openHomeMenuIndex(index: number) {
|
||||
if (canShowCloudSync && index === 0) {
|
||||
if (!syncingCloud && cloudSync.dirty) void syncSaveNow()
|
||||
if (!syncingCloud) void syncSaveNow()
|
||||
return
|
||||
}
|
||||
if (canShowCloudSync && syncComparison && index === 1) {
|
||||
if (!syncingCloud) void keepCloudSave('local')
|
||||
return
|
||||
}
|
||||
if (canShowCloudSync && syncComparison && index === 2) {
|
||||
if (!syncingCloud) void keepCloudSave('server')
|
||||
return
|
||||
}
|
||||
const item = MENU_ITEMS[index - homeMenuOffset]
|
||||
@@ -404,6 +451,52 @@ function App() {
|
||||
setScreen(item.screen)
|
||||
}
|
||||
|
||||
function moveHomeSelection(action: string) {
|
||||
setHomeSelectedIndex((current) => {
|
||||
const bounded = Math.min(current, homeMenuEntryCount - 1)
|
||||
if (canShowCloudSync && syncComparison && bounded <= 2) {
|
||||
if (bounded === 0) {
|
||||
return action === 'navigateDown' || action === 'navigateRight' ? 1 : 0
|
||||
}
|
||||
if (bounded === 1) {
|
||||
if (action === 'navigateRight') return 2
|
||||
if (action === 'navigateDown') return Math.min(homeMenuOffset, homeMenuEntryCount - 1)
|
||||
return 0
|
||||
}
|
||||
if (action === 'navigateLeft') return 1
|
||||
if (action === 'navigateDown') return Math.min(homeMenuOffset, homeMenuEntryCount - 1)
|
||||
return 0
|
||||
}
|
||||
if (canShowCloudSync && !syncComparison && bounded === 0) {
|
||||
return action === 'navigateDown' || action === 'navigateRight'
|
||||
? Math.min(homeMenuOffset, homeMenuEntryCount - 1)
|
||||
: 0
|
||||
}
|
||||
|
||||
const menuIndex = bounded - homeMenuOffset
|
||||
if (menuIndex < 0) return bounded
|
||||
const column = menuIndex % HOME_MENU_COLUMNS
|
||||
if (action === 'navigateLeft') {
|
||||
if (column > 0) return bounded - 1
|
||||
if (canShowCloudSync && syncComparison && menuIndex < HOME_MENU_COLUMNS) return 2
|
||||
return bounded
|
||||
}
|
||||
if (action === 'navigateRight') {
|
||||
const nextMenuIndex = menuIndex + 1
|
||||
return column < HOME_MENU_COLUMNS - 1 && nextMenuIndex < MENU_ITEMS.length ? bounded + 1 : bounded
|
||||
}
|
||||
if (action === 'navigateUp') {
|
||||
const previousMenuIndex = menuIndex - HOME_MENU_COLUMNS
|
||||
if (previousMenuIndex >= 0) return homeMenuOffset + previousMenuIndex
|
||||
if (canShowCloudSync && syncComparison) return column === 0 ? 1 : 2
|
||||
if (canShowCloudSync) return 0
|
||||
return bounded
|
||||
}
|
||||
const nextMenuIndex = menuIndex + HOME_MENU_COLUMNS
|
||||
return nextMenuIndex < MENU_ITEMS.length ? homeMenuOffset + nextMenuIndex : bounded
|
||||
})
|
||||
}
|
||||
|
||||
function dungeonEntries() {
|
||||
const difficulty = selectedDifficultyOption ?? selectedActivityOption?.difficulties[0]
|
||||
const locked = profile && difficulty ? profile.character.level < difficulty.unlockLevel : true
|
||||
@@ -659,19 +752,7 @@ function App() {
|
||||
return
|
||||
}
|
||||
if (!action.startsWith('navigate')) return
|
||||
setHomeSelectedIndex((current) => {
|
||||
const bounded = Math.min(current, homeMenuEntryCount - 1)
|
||||
const column = bounded % HOME_MENU_COLUMNS
|
||||
if (action === 'navigateLeft') return column > 0 ? bounded - 1 : bounded
|
||||
if (action === 'navigateRight') {
|
||||
const next = bounded + 1
|
||||
return column < HOME_MENU_COLUMNS - 1 && next < homeMenuEntryCount ? next : bounded
|
||||
}
|
||||
if (action === 'navigateUp') return bounded >= HOME_MENU_COLUMNS ? bounded - HOME_MENU_COLUMNS : bounded
|
||||
const next = bounded + HOME_MENU_COLUMNS
|
||||
if (next < homeMenuEntryCount) return next
|
||||
return column > 0 ? homeMenuEntryCount - 1 : bounded
|
||||
})
|
||||
moveHomeSelection(action)
|
||||
return
|
||||
}
|
||||
if (screen === 'dungeons' || screen === 'raids') {
|
||||
@@ -860,32 +941,68 @@ function App() {
|
||||
|
||||
{screen === 'menu' && (
|
||||
<section className="menu-screen" data-game-nav-active="true">
|
||||
<div className="main-menu-grid">
|
||||
<div className={`main-menu-grid ${canShowCloudSync ? 'has-cloud-sync' : ''}`}>
|
||||
{canShowCloudSync && (
|
||||
<div
|
||||
className={`menu-card cloud-sync-card ${homeActiveIndex === 0 ? 'game-selected' : ''}`}
|
||||
data-game-selected={homeActiveIndex === 0 ? 'true' : undefined}
|
||||
onPointerDown={() => setHomeSelectedIndex(0)}
|
||||
>
|
||||
<span>{cloudSync.dirty ? 'S' : 'C'}</span>
|
||||
<span>{syncComparison ? '!' : cloudSync.dirty ? 'S' : 'C'}</span>
|
||||
<div>
|
||||
<strong>Cloud Save</strong>
|
||||
<strong>Sync With Server</strong>
|
||||
<small>
|
||||
{cloudSync.dirty
|
||||
{syncComparison
|
||||
? cloudSyncRelationText(syncComparison)
|
||||
: cloudSync.dirty
|
||||
? 'Local progress waiting. Upload when you want to refresh the server copy.'
|
||||
: 'Server copy matches this device.'}
|
||||
</small>
|
||||
{syncComparison && (
|
||||
<div className="cloud-sync-times">
|
||||
<small>Device: {formatSaveTimestamp(syncComparison.local.updatedAt)}</small>
|
||||
<small>Server: {formatSaveTimestamp(syncComparison.server.updatedAt)}</small>
|
||||
</div>
|
||||
)}
|
||||
{syncMessage && <small className="cloud-sync-message">{syncMessage}</small>}
|
||||
</div>
|
||||
<button
|
||||
className="text-button"
|
||||
data-controller-nav="skip"
|
||||
disabled={syncingCloud || !cloudSync.dirty}
|
||||
onClick={syncSaveNow}
|
||||
type="button"
|
||||
>
|
||||
{syncingCloud ? 'Syncing...' : cloudSync.dirty ? 'Sync Save To Server' : 'Already Synced'}
|
||||
</button>
|
||||
<div className="cloud-sync-control-stack">
|
||||
<button
|
||||
className="text-button"
|
||||
data-controller-nav="skip"
|
||||
disabled={syncingCloud}
|
||||
onClick={syncSaveNow}
|
||||
type="button"
|
||||
>
|
||||
{syncingCloud ? 'Checking...' : syncComparison ? 'Check Again' : 'Sync With Server'}
|
||||
</button>
|
||||
{syncComparison && (
|
||||
<div className="cloud-sync-actions">
|
||||
<button
|
||||
className={`text-button ${homeActiveIndex === 1 ? 'game-selected' : ''}`}
|
||||
data-controller-nav="skip"
|
||||
data-game-selected={homeActiveIndex === 1 ? 'true' : undefined}
|
||||
disabled={syncingCloud}
|
||||
onClick={() => keepCloudSave('local')}
|
||||
onPointerDown={() => setHomeSelectedIndex(1)}
|
||||
type="button"
|
||||
>
|
||||
Keep Device Save
|
||||
</button>
|
||||
<button
|
||||
className={`text-button ${homeActiveIndex === 2 ? 'game-selected' : ''}`}
|
||||
data-controller-nav="skip"
|
||||
data-game-selected={homeActiveIndex === 2 ? 'true' : undefined}
|
||||
disabled={syncingCloud}
|
||||
onClick={() => keepCloudSave('server')}
|
||||
onPointerDown={() => setHomeSelectedIndex(2)}
|
||||
type="button"
|
||||
>
|
||||
Keep Server Save
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{MENU_ITEMS.map((item, index) => {
|
||||
|
||||
Reference in New Issue
Block a user