Initial commit

This commit is contained in:
AnRil
2026-05-16 13:43:29 +07:00
commit 688a86b611
208 changed files with 44350 additions and 0 deletions

235
src/main/store.ts Normal file
View 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()
}