mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Completed Splash-Screen & Iniital Tauri Wire-Up
This commit is contained in:
@@ -14,14 +14,16 @@ export const boot = $state({
|
||||
loginPass: '',
|
||||
sessionExpired: false,
|
||||
skipped: false,
|
||||
serverProbeOk: false,
|
||||
})
|
||||
|
||||
let probeGeneration = 0
|
||||
|
||||
function handleProbeSuccess(gen: number) {
|
||||
if (gen !== probeGeneration) return
|
||||
boot.failed = false
|
||||
boot.skipped = false
|
||||
boot.failed = false
|
||||
boot.skipped = false
|
||||
boot.serverProbeOk = true
|
||||
appState.authenticated = true
|
||||
appState.status = 'ready'
|
||||
}
|
||||
@@ -56,6 +58,7 @@ export function startProbe(
|
||||
boot.failed = false
|
||||
boot.loginRequired = false
|
||||
boot.skipped = false
|
||||
boot.serverProbeOk = false
|
||||
appState.status = 'booting'
|
||||
let tries = 0
|
||||
|
||||
@@ -121,6 +124,7 @@ export async function submitLogin(): Promise<void> {
|
||||
boot.skipped = false
|
||||
boot.loginPass = ''
|
||||
boot.loginError = null
|
||||
boot.serverProbeOk = true
|
||||
appState.authenticated = true
|
||||
appState.status = 'ready'
|
||||
} catch (e: unknown) {
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { saveLibrary } from '$lib/core/persistence/persist'
|
||||
import type { ReadSession, ReadingStats } from '$lib/types/history'
|
||||
import { DEFAULT_READING_STATS } from '$lib/types/history'
|
||||
|
||||
const MAX_SESSIONS = 1000
|
||||
const SESSION_GAP_MS = 60 * 60 * 1_000
|
||||
|
||||
export interface ActiveSession {
|
||||
id: string
|
||||
mangaId: number
|
||||
mangaTitle: string
|
||||
thumbnailUrl: string
|
||||
startChapterId: number
|
||||
startChapterName: string
|
||||
endChapterId: number
|
||||
endChapterName: string
|
||||
startPage: number
|
||||
endPage: number
|
||||
startedAt: number
|
||||
lastTickAt: number
|
||||
seenChapterIds: Set<number>
|
||||
}
|
||||
|
||||
function dateKey(ms: number): string {
|
||||
return new Date(ms).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function computeStats(sessions: ReadSession[]): ReadingStats {
|
||||
if (!sessions.length) return { ...DEFAULT_READING_STATS }
|
||||
|
||||
const chapterIds = new Set<number>()
|
||||
const mangaIds = new Set<number>()
|
||||
const days = new Set<string>()
|
||||
let totalMs = 0
|
||||
let firstReadAt = Infinity
|
||||
let lastReadAt = 0
|
||||
|
||||
for (const s of sessions) {
|
||||
chapterIds.add(s.endChapterId)
|
||||
if (s.chaptersSpanned > 1) chapterIds.add(s.startChapterId)
|
||||
mangaIds.add(s.mangaId)
|
||||
totalMs += Math.min(s.durationMs, SESSION_GAP_MS)
|
||||
firstReadAt = Math.min(firstReadAt, s.startedAt)
|
||||
lastReadAt = Math.max(lastReadAt, s.endedAt)
|
||||
days.add(dateKey(s.endedAt))
|
||||
}
|
||||
|
||||
const sortedDays = Array.from(days).sort()
|
||||
let currentStreak = 0
|
||||
let longestStreak = 0
|
||||
let streak = 0
|
||||
const todayKey = dateKey(Date.now())
|
||||
const yestKey = dateKey(Date.now() - 86_400_000)
|
||||
const lastDay = sortedDays[sortedDays.length - 1]
|
||||
const streakActive = lastDay === todayKey || lastDay === yestKey
|
||||
|
||||
for (let i = 0; i < sortedDays.length; i++) {
|
||||
if (i === 0) {
|
||||
streak = 1
|
||||
} else {
|
||||
const prev = new Date(sortedDays[i - 1]).getTime()
|
||||
const curr = new Date(sortedDays[i]).getTime()
|
||||
streak = curr - prev <= 86_400_000 * 1.5 ? streak + 1 : 1
|
||||
}
|
||||
longestStreak = Math.max(longestStreak, streak)
|
||||
}
|
||||
currentStreak = streakActive ? streak : 0
|
||||
|
||||
return {
|
||||
totalChaptersRead: chapterIds.size,
|
||||
totalMangaRead: mangaIds.size,
|
||||
totalMinutesRead: Math.round(totalMs / 60_000),
|
||||
firstReadAt: firstReadAt === Infinity ? 0 : firstReadAt,
|
||||
lastReadAt,
|
||||
currentStreakDays: currentStreak,
|
||||
longestStreakDays: longestStreak,
|
||||
lastStreakDate: lastDay ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryStore {
|
||||
sessions = $state<ReadSession[]>([])
|
||||
dailyReadCounts = $state<Record<string, number>>({})
|
||||
stats = $state<ReadingStats>({ ...DEFAULT_READING_STATS })
|
||||
active = $state<ActiveSession | null>(null)
|
||||
|
||||
load(sessions: ReadSession[], dailyReadCounts: Record<string, number>) {
|
||||
this.sessions = sessions
|
||||
this.dailyReadCounts = dailyReadCounts
|
||||
this.stats = computeStats(sessions)
|
||||
}
|
||||
|
||||
openSession(
|
||||
mangaId: number,
|
||||
mangaTitle: string,
|
||||
thumbnailUrl: string,
|
||||
chapterId: number,
|
||||
chapterName: string,
|
||||
page: number,
|
||||
) {
|
||||
if (this.active) this._commit(Date.now())
|
||||
|
||||
this.active = {
|
||||
id: crypto.randomUUID(),
|
||||
mangaId,
|
||||
mangaTitle,
|
||||
thumbnailUrl,
|
||||
startChapterId: chapterId,
|
||||
startChapterName: chapterName,
|
||||
endChapterId: chapterId,
|
||||
endChapterName: chapterName,
|
||||
startPage: page,
|
||||
endPage: page,
|
||||
startedAt: Date.now(),
|
||||
lastTickAt: Date.now(),
|
||||
seenChapterIds: new Set([chapterId]),
|
||||
}
|
||||
}
|
||||
|
||||
tickSession(chapterId: number, chapterName: string, page: number) {
|
||||
if (!this.active) return
|
||||
const now = Date.now()
|
||||
|
||||
if (now - this.active.lastTickAt > SESSION_GAP_MS) {
|
||||
this._commit(this.active.lastTickAt)
|
||||
this.openSession(
|
||||
this.active.mangaId,
|
||||
this.active.mangaTitle,
|
||||
this.active.thumbnailUrl,
|
||||
chapterId,
|
||||
chapterName,
|
||||
page,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.active.lastTickAt = now
|
||||
this.active.endPage = page
|
||||
this.active.endChapterId = chapterId
|
||||
this.active.endChapterName = chapterName
|
||||
this.active.seenChapterIds.add(chapterId)
|
||||
}
|
||||
|
||||
closeSession() {
|
||||
if (!this.active) return
|
||||
this._commit(Date.now())
|
||||
this.active = null
|
||||
}
|
||||
|
||||
clearHistory() {
|
||||
this.sessions = []
|
||||
this.dailyReadCounts = {}
|
||||
this.stats = { ...DEFAULT_READING_STATS }
|
||||
void this._persist()
|
||||
}
|
||||
|
||||
private _commit(endedAt: number) {
|
||||
const a = this.active
|
||||
if (!a) return
|
||||
|
||||
const durationMs = Math.min(endedAt - a.startedAt, SESSION_GAP_MS)
|
||||
if (durationMs < 1_000) return
|
||||
|
||||
const session: ReadSession = {
|
||||
id: a.id,
|
||||
mangaId: a.mangaId,
|
||||
mangaTitle: a.mangaTitle,
|
||||
thumbnailUrl: a.thumbnailUrl,
|
||||
startChapterId: a.startChapterId,
|
||||
startChapterName: a.startChapterName,
|
||||
endChapterId: a.endChapterId,
|
||||
endChapterName: a.endChapterName,
|
||||
startPage: a.startPage,
|
||||
endPage: a.endPage,
|
||||
startedAt: a.startedAt,
|
||||
endedAt,
|
||||
durationMs,
|
||||
chaptersSpanned: a.seenChapterIds.size,
|
||||
}
|
||||
|
||||
const day = dateKey(endedAt)
|
||||
this.dailyReadCounts[day] = (this.dailyReadCounts[day] ?? 0) + 1
|
||||
|
||||
this.sessions = [session, ...this.sessions].slice(0, MAX_SESSIONS)
|
||||
this.stats = computeStats(this.sessions)
|
||||
|
||||
void this._persist()
|
||||
}
|
||||
|
||||
private async _persist() {
|
||||
const bookmarks = (await import('$lib/state/reader.svelte')).readerState.bookmarks
|
||||
const markers = (await import('$lib/state/reader.svelte')).readerState.markers
|
||||
await saveLibrary({
|
||||
sessions: this.sessions,
|
||||
bookmarks,
|
||||
markers,
|
||||
dailyReadCounts: this.dailyReadCounts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const historyState = new HistoryStore()
|
||||
@@ -1,46 +1,17 @@
|
||||
export interface HistoryEntry {
|
||||
mangaId: number;
|
||||
mangaTitle: string;
|
||||
thumbnailUrl: string;
|
||||
chapterId: number;
|
||||
chapterName: string;
|
||||
chapterNumber: number;
|
||||
pageNumber: number;
|
||||
readAt: number;
|
||||
}
|
||||
|
||||
export interface ReadingStats {
|
||||
currentStreakDays: number;
|
||||
totalChaptersRead: number;
|
||||
totalMinutesRead: number;
|
||||
totalMangaRead: number;
|
||||
longestStreakDays: number;
|
||||
}
|
||||
import { historyState } from '$lib/state/history.svelte'
|
||||
|
||||
export const homeState = $state({
|
||||
history: [] as HistoryEntry[],
|
||||
dailyReadCounts: {} as Record<string, number>,
|
||||
stats: {
|
||||
currentStreakDays: 0,
|
||||
totalChaptersRead: 0,
|
||||
totalMinutesRead: 0,
|
||||
totalMangaRead: 0,
|
||||
longestStreakDays: 0,
|
||||
} as ReadingStats,
|
||||
heroSlots: [null, null, null, null] as [number | null, number | null, number | null, number | null],
|
||||
});
|
||||
})
|
||||
|
||||
export function getHistoryStats() { return historyState.stats }
|
||||
export function getHistorySessions() { return historyState.sessions }
|
||||
export function getHistoryDailyCounts() { return historyState.dailyReadCounts }
|
||||
|
||||
export function setHeroSlot(i: 1 | 2 | 3, mangaId: number | null) {
|
||||
homeState.heroSlots[i] = mangaId;
|
||||
}
|
||||
|
||||
export function recordRead(entry: HistoryEntry) {
|
||||
homeState.history = [entry, ...homeState.history.filter(e => e.chapterId !== entry.chapterId)];
|
||||
const dateStr = new Date(entry.readAt).toISOString().slice(0, 10);
|
||||
homeState.dailyReadCounts[dateStr] = (homeState.dailyReadCounts[dateStr] ?? 0) + 1;
|
||||
homeState.stats.totalChaptersRead++;
|
||||
homeState.heroSlots[i] = mangaId
|
||||
}
|
||||
|
||||
export function clearHistory() {
|
||||
homeState.history = [];
|
||||
historyState.clearHistory()
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class ReaderState {
|
||||
zoomOpen = $state(false);
|
||||
winOpen = $state(false);
|
||||
presetOpen = $state(false);
|
||||
actionsOpen = $state(false);
|
||||
nextN = $state(5);
|
||||
dlBusy = $state(false);
|
||||
|
||||
@@ -116,11 +117,12 @@ class ReaderState {
|
||||
}
|
||||
|
||||
closeAllPopovers(): boolean {
|
||||
if (this.markerOpen) { this.markerOpen = false; return true; }
|
||||
if (this.zoomOpen) { this.zoomOpen = false; return true; }
|
||||
if (this.dlOpen) { this.dlOpen = false; return true; }
|
||||
if (this.winOpen) { this.winOpen = false; return true; }
|
||||
if (this.presetOpen) { this.presetOpen = false; return true; }
|
||||
if (this.markerOpen) { this.markerOpen = false; return true; }
|
||||
if (this.zoomOpen) { this.zoomOpen = false; return true; }
|
||||
if (this.dlOpen) { this.dlOpen = false; return true; }
|
||||
if (this.winOpen) { this.winOpen = false; return true; }
|
||||
if (this.presetOpen) { this.presetOpen = false; return true; }
|
||||
if (this.actionsOpen) { this.actionsOpen = false; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
import type { Settings } from "$lib/types/settings";
|
||||
import { DEFAULT_SETTINGS } from "$lib/types/settings";
|
||||
import type { Settings } from '$lib/types/settings'
|
||||
import { DEFAULT_SETTINGS } from '$lib/types/settings'
|
||||
import { saveSettings } from '$lib/core/persistence/persist'
|
||||
|
||||
const KEY = "moku_settings";
|
||||
export const settingsState = $state({ settings: { ...DEFAULT_SETTINGS } as Settings })
|
||||
|
||||
function load(): Settings {
|
||||
try {
|
||||
const raw = localStorage.getItem(KEY);
|
||||
if (raw) return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
|
||||
} catch {}
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
function save(s: Settings) {
|
||||
try { localStorage.setItem(KEY, JSON.stringify(s)); } catch {}
|
||||
}
|
||||
|
||||
export const settingsState = $state({ settings: load() });
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.style.zoom = String(settingsState.settings.uiZoom ?? 1.0);
|
||||
export async function loadSettingsIntoState(raw: unknown) {
|
||||
if (raw && typeof raw === 'object') {
|
||||
Object.assign(settingsState.settings, raw)
|
||||
}
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.style.zoom = String(settingsState.settings.uiZoom ?? 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSettings(patch: Partial<Settings>) {
|
||||
Object.assign(settingsState.settings, patch);
|
||||
save(settingsState.settings);
|
||||
Object.assign(settingsState.settings, patch)
|
||||
void saveSettings({ storeVersion: 2, settings: settingsState.settings })
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
if (patch.uiZoom !== undefined) {
|
||||
document.documentElement.style.zoom = String(patch.uiZoom);
|
||||
}
|
||||
if (typeof document !== 'undefined' && patch.uiZoom !== undefined) {
|
||||
document.documentElement.style.zoom = String(patch.uiZoom)
|
||||
}
|
||||
}
|
||||
|
||||
export function resetSettings() {
|
||||
settingsState.settings = { ...DEFAULT_SETTINGS };
|
||||
save(settingsState.settings);
|
||||
settingsState.settings = { ...DEFAULT_SETTINGS }
|
||||
void saveSettings({ storeVersion: 2, settings: settingsState.settings })
|
||||
}
|
||||
Reference in New Issue
Block a user