From b3b63d1985015f3bfd8b1e96aa2d6062bc796a64 Mon Sep 17 00:00:00 2001 From: Failerko Date: Sat, 7 Feb 2026 01:40:12 +0100 Subject: [PATCH] feat: branch and checkpoint aware bg images --- .../migrations/024_background_images.sql | 21 +++++ src-tauri/migrations/024_story_bg_image.sql | 2 - src-tauri/src/lib.rs | 2 +- src/lib/services/database.ts | 85 ++++++++++++++++--- src/lib/stores/story.svelte.ts | 34 +++++++- 5 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 src-tauri/migrations/024_background_images.sql delete mode 100644 src-tauri/migrations/024_story_bg_image.sql diff --git a/src-tauri/migrations/024_background_images.sql b/src-tauri/migrations/024_background_images.sql new file mode 100644 index 0000000..6f0214d --- /dev/null +++ b/src-tauri/migrations/024_background_images.sql @@ -0,0 +1,21 @@ +-- Migration 024: Table-based background images +-- Supports per-branch and per-checkpoint backgrounds + +CREATE TABLE IF NOT EXISTS background_images ( + id TEXT PRIMARY KEY, + story_id TEXT NOT NULL, + branch_id TEXT, -- NULL for main branch context + checkpoint_id TEXT, -- Linked to a specific checkpoint + image_data TEXT NOT NULL, -- The image data (URL or base64) + created_at INTEGER NOT NULL, + + FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE, + FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE, + FOREIGN KEY (checkpoint_id) REFERENCES checkpoints(id) ON DELETE CASCADE +); + +-- Index for fast lookup by story/branch +CREATE INDEX IF NOT EXISTS idx_backgrounds_branch ON background_images(story_id, branch_id); + +-- Index for fast lookup by story/checkpoint +CREATE INDEX IF NOT EXISTS idx_backgrounds_checkpoint ON background_images(story_id, checkpoint_id); diff --git a/src-tauri/migrations/024_story_bg_image.sql b/src-tauri/migrations/024_story_bg_image.sql deleted file mode 100644 index 5f23c4d..0000000 --- a/src-tauri/migrations/024_story_bg_image.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Add current_background_image column to stories table -ALTER TABLE stories ADD COLUMN current_background_image TEXT; \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7b74b92..f0896e9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -151,7 +151,7 @@ pub fn run() { Migration { version: 24, description: "story_bg_image", - sql: include_str!("../migrations/024_story_bg_image.sql"), + sql: include_str!("../migrations/024_background_images.sql"), kind: MigrationKind::Up, } ]; diff --git a/src/lib/services/database.ts b/src/lib/services/database.ts index fb2d8a6..5c08d4a 100644 --- a/src/lib/services/database.ts +++ b/src/lib/services/database.ts @@ -164,9 +164,8 @@ class DatabaseService { retry_state, style_review_state, time_tracker - current_background_image ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ story.id, story.title, @@ -181,7 +180,6 @@ class DatabaseService { story.retryState ? JSON.stringify(story.retryState) : null, story.styleReviewState ? JSON.stringify(story.styleReviewState) : null, story.timeTracker ? JSON.stringify(story.timeTracker) : null, - story.currentBgImage, ], ) return { ...story, createdAt: now, updatedAt: now } @@ -1670,14 +1668,81 @@ class DatabaseService { } /** - * Save the current background image for a story. + * Get the background image for a specific branch. */ - async saveCurrentBackgroundImage(storyId: string, imageData: string | null): Promise { + async getBackgroundForBranch(storyId: string, branchId: string | null): Promise { const db = await this.getDb() - await db.execute('UPDATE stories SET current_background_image = ? WHERE id = ?', [ - imageData, - storyId, - ]) + const results = await db.select<{ image_data: string }[]>( + 'SELECT image_data FROM background_images WHERE story_id = ? AND branch_id IS ? AND checkpoint_id IS NULL ORDER BY created_at DESC LIMIT 1', + [storyId, branchId], + ) + return results.length > 0 ? results[0].image_data : null + } + + /** + * Get the background image for a specific checkpoint. + */ + async getBackgroundForCheckpoint(storyId: string, checkpointId: string): Promise { + const db = await this.getDb() + const results = await db.select<{ image_data: string }[]>( + 'SELECT image_data FROM background_images WHERE story_id = ? AND checkpoint_id = ? LIMIT 1', + [storyId, checkpointId], + ) + return results.length > 0 ? results[0].image_data : null + } + + /** + * Save a background image for a story/branch/checkpoint. + */ + async saveBackground( + storyId: string, + branchId: string | null, + checkpointId: string | null, + imageData: string | null, + ): Promise { + const db = await this.getDb() + + if (!imageData) { + // If clearing, delete entries for this specific context + if (checkpointId) { + await db.execute('DELETE FROM background_images WHERE story_id = ? AND checkpoint_id = ?', [ + storyId, + checkpointId, + ]) + } else { + await db.execute( + 'DELETE FROM background_images WHERE story_id = ? AND branch_id IS ? AND checkpoint_id IS NULL', + [storyId, branchId], + ) + } + return + } + + // Insert or update + const id = crypto.randomUUID() + const now = Date.now() + + if (checkpointId) { + // Checkpoints always get a new entry or replace existing for that checkpoint + await db.execute( + 'INSERT OR REPLACE INTO background_images (id, story_id, branch_id, checkpoint_id, image_data, created_at) VALUES (?, ?, ?, ?, ?, ?)', + [id, storyId, branchId, checkpointId, imageData, now], + ) + } else { + // For branches (including main), we update the single "current" record for that branch + const existing = await this.getBackgroundForBranch(storyId, branchId) + if (existing) { + await db.execute( + 'UPDATE background_images SET image_data = ?, created_at = ? WHERE story_id = ? AND branch_id IS ? AND checkpoint_id IS NULL', + [imageData, now, storyId, branchId], + ) + } else { + await db.execute( + 'INSERT INTO background_images (id, story_id, branch_id, checkpoint_id, image_data, created_at) VALUES (?, ?, ?, ?, ?, ?)', + [id, storyId, branchId, null, imageData, now], + ) + } + } } /** @@ -1752,7 +1817,7 @@ class DatabaseService { styleReviewState: row.style_review_state ? JSON.parse(row.style_review_state) : null, timeTracker: row.time_tracker ? JSON.parse(row.time_tracker) : null, currentBranchId: row.current_branch_id || null, - currentBgImage: row.current_background_image || null, + currentBgImage: null, // Loaded separately now } } diff --git a/src/lib/stores/story.svelte.ts b/src/lib/stores/story.svelte.ts index e0f7bee..4b327b4 100644 --- a/src/lib/stores/story.svelte.ts +++ b/src/lib/stores/story.svelte.ts @@ -431,7 +431,7 @@ class StoryStore { await database.cleanupOrphanedEmbeddedImages() this.currentStory = story - this.currentBgImage = story.currentBgImage + this.currentBgImage = await database.getBackgroundForBranch(storyId, story.currentBranchId) // Load branch-independent data first const [characters, locations, items, storyBeats, checkpoints, lorebookEntries, branches] = @@ -745,7 +745,12 @@ class StoryStore { this.currentStory.currentBgImage = imageData } - await database.saveCurrentBackgroundImage(this.currentStory.id, imageData) + await database.saveBackground( + this.currentStory.id, + this.currentStory.currentBranchId, + null, + imageData, + ) log('Background image updated and persisted') } @@ -1915,6 +1920,18 @@ class StoryStore { await database.createCheckpoint(checkpoint) this.checkpoints = [checkpoint, ...this.checkpoints] + + // Save current background for this checkpoint + if (this.currentBgImage) { + log('Saving background for checkpoint:', name) + await database.saveBackground( + this.currentStory.id, + this.currentStory.currentBranchId, + checkpoint.id, + this.currentBgImage, + ) + } + log('Checkpoint created:', name) // Emit event @@ -2020,6 +2037,16 @@ class StoryStore { await database.addBranch(branch) this.branches = [...this.branches, branch] + // Inherit background from checkpoint + const checkpointBg = await database.getBackgroundForCheckpoint( + this.currentStory.id, + checkpointId, + ) + if (checkpointBg) { + log('Inheriting background from checkpoint for new branch:', branch.name) + await database.saveBackground(this.currentStory.id, branch.id, null, checkpointBg) + } + // Copy world state from checkpoint into database with the new branch_id // This ensures the branch has its own copy of the world state at the fork point log('Copying world state from checkpoint to branch:', branch.name) @@ -2170,6 +2197,9 @@ class StoryStore { this.invalidateWordCountCache() this.invalidateChapterCache() + // Reload background from database for the branch + this.currentBgImage = await database.getBackgroundForBranch(this.currentStory.id, branchId) + log('Switched to branch:', branchId ?? 'main') }