diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 94f17c5..0eadd2f 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -572,12 +572,12 @@ export function setupIPC( return { success: true } }) - // Export progress data (v1.2 includes review history for streak sync) + // Export progress data (v1.3 includes review history + star/block flags) ipcMain.handle('export-progress', () => { return buildExportData(db, app.getVersion()) }) - // Import progress data (supports v1.0–v1.2; reviewHistory optional) + // Import progress data (supports v1.0–v1.3; reviewHistory / problemFlags optional) ipcMain.handle('import-progress', (_event, data: ExportData) => { return importProgressData(db, data) }) @@ -693,7 +693,7 @@ export function setupIPC( return result.filePaths[0] || null }) - // Perform auto-export to specified folder (v1.2 includes review history) + // Perform auto-export to specified folder (v1.3 includes review history + flags) ipcMain.handle('perform-auto-export', (_event, folderPath: string) => { return autoExport(folderPath) }) diff --git a/src/main/lib/sync-data.ts b/src/main/lib/sync-data.ts index 414368d..f0a1387 100644 --- a/src/main/lib/sync-data.ts +++ b/src/main/lib/sync-data.ts @@ -24,15 +24,24 @@ export interface ExportReviewHistoryEntry { ease_factor_after: number | null } +/** Star / block flags live on `problems`, not progress — synced separately. */ +export interface ExportProblemFlagsEntry { + neet_id: number + starred: number + blocked: number +} + export interface ExportData { version: string exportDate: string appVersion: string progress: ExportProgressEntry[] reviewHistory?: ExportReviewHistoryEntry[] + /** Present in v1.3+; starred/blocked problem flags */ + problemFlags?: ExportProblemFlagsEntry[] } -export const EXPORT_VERSION = '1.2' +export const EXPORT_VERSION = '1.3' function reviewHistoryKey(entry: { neet_id: number @@ -99,13 +108,30 @@ export function fetchReviewHistory(db: Database.Database): ExportReviewHistoryEn .all() as ExportReviewHistoryEntry[] } +export function fetchProblemFlags(db: Database.Database): ExportProblemFlagsEntry[] { + return db + .prepare( + ` + SELECT + neet_id, + COALESCE(starred, 0) as starred, + COALESCE(blocked, 0) as blocked + FROM problems + WHERE COALESCE(starred, 0) = 1 OR COALESCE(blocked, 0) = 1 + ORDER BY neet_id + ` + ) + .all() as ExportProblemFlagsEntry[] +} + export function buildExportData(db: Database.Database, appVersion: string): ExportData { return { version: EXPORT_VERSION, exportDate: new Date().toISOString(), appVersion, progress: fetchProgress(db), - reviewHistory: fetchReviewHistory(db) + reviewHistory: fetchReviewHistory(db), + problemFlags: fetchProblemFlags(db) } } @@ -237,6 +263,21 @@ export function importProgressData( } historyImported = mergeReviewHistory(db, data.reviewHistory) + + // v1.3+: replace local star/block flags from the backup when the field is present. + // Older backups omit problemFlags — leave local flags untouched. + if (Array.isArray(data.problemFlags)) { + db.prepare('UPDATE problems SET starred = 0, blocked = 0').run() + const updateFlags = db.prepare( + 'UPDATE problems SET starred = ?, blocked = ? WHERE neet_id = ?' + ) + for (const entry of data.problemFlags) { + if (typeof entry?.neet_id !== 'number') continue + const starred = entry.starred ? 1 : 0 + const blocked = entry.blocked ? 1 : 0 + updateFlags.run(starred, blocked, entry.neet_id) + } + } }) transaction() diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index d5e6778..32e4382 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -98,6 +98,12 @@ export interface ExportReviewHistoryEntry { ease_factor_after: number | null } +export interface ExportProblemFlagsEntry { + neet_id: number + starred: number + blocked: number +} + export interface ExportData { version: string exportDate: string @@ -105,6 +111,8 @@ export interface ExportData { progress: ExportProgressEntry[] // Present in v1.2+; used for activity heatmap / streak sync reviewHistory?: ExportReviewHistoryEntry[] + // Present in v1.3+; starred / blocked flags + problemFlags?: ExportProblemFlagsEntry[] } export interface API { diff --git a/src/renderer/src/stores/problems.ts b/src/renderer/src/stores/problems.ts index f1238a1..5218eae 100644 --- a/src/renderer/src/stores/problems.ts +++ b/src/renderer/src/stores/problems.ts @@ -252,6 +252,19 @@ export async function selectProblem(problem: Problem): Promise { selectedProblem.set(problem) } + +async function maybeAutoExport(reason: string): Promise { + try { + const syncPrefs = await window.api.getAutoSyncPreferences() + if (syncPrefs.enabled && syncPrefs.folderPath) { + await window.api.performAutoExport(syncPrefs.folderPath) + } + } catch (syncError) { + // Don't fail the user action if sync fails + console.error(`Auto-sync after ${reason} failed:`, syncError) + } +} + export async function submitReview( problemId: number, quality: number @@ -259,17 +272,8 @@ export async function submitReview( try { const result = await window.api.submitReview({ problemId, quality }) - // Trigger auto-export if enabled if (result.success) { - try { - const syncPrefs = await window.api.getAutoSyncPreferences() - if (syncPrefs.enabled && syncPrefs.folderPath) { - await window.api.performAutoExport(syncPrefs.folderPath) - } - } catch (syncError) { - // Don't fail the review if sync fails - console.error('Auto-sync after review failed:', syncError) - } + await maybeAutoExport('review') } return result @@ -293,6 +297,7 @@ export async function setProblemBlocked(problemId: number, blocked: boolean): Pr try { await window.api.setProblemBlocked(problemId, blocked) await Promise.all([loadProblems(), loadTodayReviews()]) + await maybeAutoExport('block') } catch (error) { console.error('Failed to set problem blocked:', error) } @@ -302,6 +307,7 @@ export async function setProblemStarred(problemId: number, starred: boolean): Pr try { await window.api.setProblemStarred(problemId, starred) await loadProblems() + await maybeAutoExport('star') } catch (error) { console.error('Failed to set problem starred:', error) }