Initial commit
This commit is contained in:
235
src/main/store.ts
Normal file
235
src/main/store.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { app } from 'electron'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import {
|
||||
AppState,
|
||||
Challenge,
|
||||
DEFAULT_SETTINGS,
|
||||
Exercise,
|
||||
GameId,
|
||||
SAMPLE_EXERCISES,
|
||||
Settings
|
||||
} from '@shared/types'
|
||||
|
||||
let cache: AppState | null = null
|
||||
let storePath = ''
|
||||
let pendingWrite: NodeJS.Timeout | null = null
|
||||
|
||||
function getStorePath(): string {
|
||||
if (!storePath) {
|
||||
const dir = app.getPath('userData')
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
storePath = join(dir, 'app-state.json')
|
||||
}
|
||||
return storePath
|
||||
}
|
||||
|
||||
function makeInitial(): AppState {
|
||||
const now = Date.now()
|
||||
return {
|
||||
exercises: SAMPLE_EXERCISES.map((e) => ({
|
||||
...e,
|
||||
id: randomUUID(),
|
||||
nextFireAt: now + e.intervalMinutes * 60_000
|
||||
})),
|
||||
settings: { ...DEFAULT_SETTINGS },
|
||||
challenges: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: 'За смерти в Dota — приседания',
|
||||
gameId: 'dota2',
|
||||
stat: 'deaths',
|
||||
multiplier: 3,
|
||||
exerciseName: 'Приседания',
|
||||
icon: 'Activity',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: 'За убийства — отжимания',
|
||||
gameId: 'dota2',
|
||||
stat: 'kills',
|
||||
multiplier: 1,
|
||||
exerciseName: 'Отжимания',
|
||||
icon: 'Dumbbell',
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
gamesEnabled: {}
|
||||
}
|
||||
}
|
||||
|
||||
function load(): AppState {
|
||||
const p = getStorePath()
|
||||
if (!existsSync(p)) {
|
||||
const initial = makeInitial()
|
||||
writeFileSync(p, JSON.stringify(initial, null, 2), 'utf-8')
|
||||
return initial
|
||||
}
|
||||
try {
|
||||
const raw = readFileSync(p, 'utf-8')
|
||||
const parsed = JSON.parse(raw) as Partial<AppState>
|
||||
return {
|
||||
exercises: parsed.exercises ?? [],
|
||||
settings: { ...DEFAULT_SETTINGS, ...(parsed.settings ?? {}) },
|
||||
challenges: parsed.challenges ?? [],
|
||||
gamesEnabled: parsed.gamesEnabled ?? {}
|
||||
}
|
||||
} catch {
|
||||
return makeInitial()
|
||||
}
|
||||
}
|
||||
|
||||
function flush(): void {
|
||||
if (!cache) return
|
||||
writeFileSync(getStorePath(), JSON.stringify(cache, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
function scheduleWrite(): void {
|
||||
if (pendingWrite) return
|
||||
pendingWrite = setTimeout(() => {
|
||||
pendingWrite = null
|
||||
flush()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
export function getState(): AppState {
|
||||
if (!cache) cache = load()
|
||||
return cache
|
||||
}
|
||||
|
||||
export function getSettings(): Settings {
|
||||
return getState().settings
|
||||
}
|
||||
|
||||
export function getExercises(): Exercise[] {
|
||||
return getState().exercises
|
||||
}
|
||||
|
||||
export function updateSettings(patch: Partial<Settings>): Settings {
|
||||
const state = getState()
|
||||
state.settings = { ...state.settings, ...patch }
|
||||
scheduleWrite()
|
||||
return state.settings
|
||||
}
|
||||
|
||||
export function addExercise(
|
||||
input: Omit<Exercise, 'id' | 'nextFireAt' | 'lastDoneAt'>
|
||||
): Exercise {
|
||||
const state = getState()
|
||||
const exercise: Exercise = {
|
||||
...input,
|
||||
id: randomUUID(),
|
||||
nextFireAt: Date.now() + input.intervalMinutes * 60_000
|
||||
}
|
||||
state.exercises.push(exercise)
|
||||
scheduleWrite()
|
||||
return exercise
|
||||
}
|
||||
|
||||
export function updateExercise(
|
||||
id: string,
|
||||
patch: Partial<Omit<Exercise, 'id'>>
|
||||
): Exercise | undefined {
|
||||
const state = getState()
|
||||
const idx = state.exercises.findIndex((e) => e.id === id)
|
||||
if (idx === -1) return undefined
|
||||
const prev = state.exercises[idx]
|
||||
const merged: Exercise = { ...prev, ...patch }
|
||||
// If interval changed, reschedule from now.
|
||||
if (patch.intervalMinutes !== undefined && patch.intervalMinutes !== prev.intervalMinutes) {
|
||||
merged.nextFireAt = Date.now() + merged.intervalMinutes * 60_000
|
||||
}
|
||||
state.exercises[idx] = merged
|
||||
scheduleWrite()
|
||||
return merged
|
||||
}
|
||||
|
||||
export function deleteExercise(id: string): boolean {
|
||||
const state = getState()
|
||||
const before = state.exercises.length
|
||||
state.exercises = state.exercises.filter((e) => e.id !== id)
|
||||
const changed = state.exercises.length !== before
|
||||
if (changed) scheduleWrite()
|
||||
return changed
|
||||
}
|
||||
|
||||
export function markDone(id: string): Exercise | undefined {
|
||||
const state = getState()
|
||||
const ex = state.exercises.find((e) => e.id === id)
|
||||
if (!ex) return undefined
|
||||
ex.lastDoneAt = Date.now()
|
||||
ex.nextFireAt = Date.now() + ex.intervalMinutes * 60_000
|
||||
scheduleWrite()
|
||||
return ex
|
||||
}
|
||||
|
||||
export function snooze(id: string, minutes: number): Exercise | undefined {
|
||||
const state = getState()
|
||||
const ex = state.exercises.find((e) => e.id === id)
|
||||
if (!ex) return undefined
|
||||
ex.nextFireAt = Date.now() + minutes * 60_000
|
||||
scheduleWrite()
|
||||
return ex
|
||||
}
|
||||
|
||||
export function skip(id: string): Exercise | undefined {
|
||||
const state = getState()
|
||||
const ex = state.exercises.find((e) => e.id === id)
|
||||
if (!ex) return undefined
|
||||
ex.nextFireAt = Date.now() + ex.intervalMinutes * 60_000
|
||||
scheduleWrite()
|
||||
return ex
|
||||
}
|
||||
|
||||
export function flushNow(): void {
|
||||
if (pendingWrite) {
|
||||
clearTimeout(pendingWrite)
|
||||
pendingWrite = null
|
||||
}
|
||||
flush()
|
||||
}
|
||||
|
||||
export function getChallenges(): Challenge[] {
|
||||
return getState().challenges
|
||||
}
|
||||
|
||||
export function addChallenge(input: Omit<Challenge, 'id'>): Challenge {
|
||||
const state = getState()
|
||||
const challenge: Challenge = { ...input, id: randomUUID() }
|
||||
state.challenges.push(challenge)
|
||||
scheduleWrite()
|
||||
return challenge
|
||||
}
|
||||
|
||||
export function updateChallenge(
|
||||
id: string,
|
||||
patch: Partial<Omit<Challenge, 'id'>>
|
||||
): Challenge | undefined {
|
||||
const state = getState()
|
||||
const idx = state.challenges.findIndex((c) => c.id === id)
|
||||
if (idx === -1) return undefined
|
||||
state.challenges[idx] = { ...state.challenges[idx], ...patch }
|
||||
scheduleWrite()
|
||||
return state.challenges[idx]
|
||||
}
|
||||
|
||||
export function deleteChallenge(id: string): boolean {
|
||||
const state = getState()
|
||||
const before = state.challenges.length
|
||||
state.challenges = state.challenges.filter((c) => c.id !== id)
|
||||
const changed = state.challenges.length !== before
|
||||
if (changed) scheduleWrite()
|
||||
return changed
|
||||
}
|
||||
|
||||
export function getGamesEnabled(): Partial<Record<GameId, boolean>> {
|
||||
return getState().gamesEnabled
|
||||
}
|
||||
|
||||
export function setGameEnabled(id: GameId, enabled: boolean): void {
|
||||
const state = getState()
|
||||
state.gamesEnabled = { ...state.gamesEnabled, [id]: enabled }
|
||||
scheduleWrite()
|
||||
}
|
||||
Reference in New Issue
Block a user