From e0eb87f59faeeddf193522e2fc7a123f6cf44c92 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Mar 2026 21:32:14 +0000
Subject: [PATCH 1/2] Initial plan
From 2e3b0f139827595859c47ec6eb3490a8d224624d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Mar 2026 21:39:11 +0000
Subject: [PATCH 2/2] Add Tauri v2 desktop wrapper with Rust SQLite backend and
Windows MSI CI
Agent-Logs-Url: https://github.com/crazyrob425/KDP-E-Book-Generator/sessions/c1184aff-4831-4ecb-a697-e79461fa6cb9
Co-authored-by: crazyrob425 <247058665+crazyrob425@users.noreply.github.com>
---
.github/workflows/build-windows.yml | 51 ++++++++
README.md | 46 ++++++-
src-tauri/Cargo.toml | 14 +--
src-tauri/src/lib.rs | 179 ++++++++++++++++++++++++++--
src-tauri/tauri.conf.json | 20 ++--
src/tauri/backend.ts | 29 +++++
6 files changed, 309 insertions(+), 30 deletions(-)
create mode 100644 .github/workflows/build-windows.yml
create mode 100644 src/tauri/backend.ts
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
new file mode 100644
index 0000000..6b12893
--- /dev/null
+++ b/.github/workflows/build-windows.yml
@@ -0,0 +1,51 @@
+name: Build Windows MSI
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ build-windows:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Set up Rust stable
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: x86_64-pc-windows-msvc
+
+ - name: Cache Rust build artifacts
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ src-tauri/target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-
+
+ - name: Install npm dependencies
+ run: npm ci
+
+ - name: Build Tauri app (Windows MSI)
+ run: npm run tauri:build
+ env:
+ TAURI_SIGNING_PRIVATE_KEY: ""
+
+ - name: Upload MSI artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: windows-msi
+ path: src-tauri/target/release/bundle/msi/*.msi
+ if-no-files-found: error
diff --git a/README.md b/README.md
index 989eefc..27ce242 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,47 @@ Don't write blindly. The app analyzes trends before you type a single word.
---
-## 💾 Installation & Setup
+## 🖥️ Desktop App (Tauri v2 — Windows)
+
+The app ships as a native Windows desktop application built with [Tauri v2](https://tauri.app/). The Rust backend provides a bundled SQLite database (no external DB required) and exposes background job progress events to the frontend.
+
+### Windows Prerequisites
+
+| Tool | Install |
+|------|---------|
+| **Rust** (stable) | |
+| **Node.js ≥ 20** | |
+| **WebView2 Runtime** | Usually pre-installed on Windows 10/11; if the app fails to open, download the **Evergreen Bootstrapper** from [Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) and run it |
+| **Visual Studio Build Tools** | Install the "Desktop development with C++" workload from [VS Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) |
+
+### Run locally in dev mode
+
+```powershell
+npm install
+npm run tauri:dev
+```
+
+This starts the Vite dev server and the Tauri desktop window simultaneously.
+
+### Build a release MSI locally
+
+```powershell
+npm run tauri:build
+# Installer: src-tauri\target\release\bundle\msi\*.msi
+```
+
+### Download a CI-built MSI (no local build required)
+
+1. Go to the **Actions** tab of this repository.
+2. Click the latest **"Build Windows MSI"** workflow run.
+3. Scroll to **Artifacts** → download **`windows-msi`**.
+4. Extract the zip and run the `.msi` installer.
+
+The workflow runs automatically on every push to `main` and can also be triggered manually via **Actions → Build Windows MSI → Run workflow**.
+
+---
+
+## 💾 Web / Development Setup
1. **Clone the Repo**
```bash
@@ -104,9 +144,9 @@ Don't write blindly. The app analyzes trends before you type a single word.
API_KEY=your_google_gemini_api_key_here
```
-4. **Run the App**
+4. **Run the App (web only)**
```bash
- npm start
+ npm run dev
```
### 🤖 Setting up the Automation Bot (Optional)
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 23b83e3..ddcf9f3 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,15 +1,11 @@
[package]
-name = "app"
+name = "binary-books-e-crafter"
version = "0.1.0"
-description = "A Tauri App"
-authors = ["you"]
-license = ""
-repository = ""
+description = "Binary Books E-Crafter"
+authors = ["Binary Books"]
edition = "2021"
rust-version = "1.77.2"
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
@@ -23,3 +19,7 @@ serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.9.5", features = [] }
tauri-plugin-log = "2"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
+rusqlite = { version = "0.31", features = ["bundled"] }
+thiserror = "1"
+uuid = { version = "1", features = ["v4", "serde"] }
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 9c3118c..6b864c6 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,16 +1,169 @@
+use rusqlite::Connection;
+use serde::{Deserialize, Serialize};
+use std::{path::PathBuf, sync::Arc};
+use tauri::{Emitter, Manager, State};
+use thiserror::Error;
+use tokio::sync::Mutex;
+use uuid::Uuid;
+
+#[derive(Debug, Error)]
+enum AppError {
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("SQLite error: {0}")]
+ Sqlite(#[from] rusqlite::Error),
+ #[error("Tauri error: {0}")]
+ Tauri(String),
+}
+
+type AppResult = Result;
+
+#[derive(Clone)]
+struct DbState {
+ db_path: PathBuf,
+}
+
+#[derive(Clone)]
+struct JobsState {
+ active: Arc>>,
+}
+
+#[derive(Debug, Serialize, Clone)]
+struct JobProgressEvent {
+ job_id: String,
+ stage: String,
+ percent: u8,
+ message: String,
+}
+
+fn get_data_dir(app: &tauri::AppHandle) -> Result {
+ app.path()
+ .app_data_dir()
+ .map_err(|e| AppError::Tauri(e.to_string()))
+}
+
+fn ensure_sqlite_schema(db_path: &PathBuf) -> Result<(), AppError> {
+ if let Some(parent) = db_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ let conn = Connection::open(db_path)?;
+ conn.execute_batch(
+ r#"
+ PRAGMA journal_mode=WAL;
+
+ CREATE TABLE IF NOT EXISTS projects (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at INTEGER NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS project_notes (
+ id TEXT PRIMARY KEY,
+ project_id TEXT NOT NULL,
+ note_type TEXT NOT NULL,
+ content TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ FOREIGN KEY(project_id) REFERENCES projects(id)
+ );
+
+ CREATE VIRTUAL TABLE IF NOT EXISTS project_text_fts
+ USING fts5(project_id, source, content);
+ "#,
+ )?;
+ Ok(())
+}
+
+#[tauri::command]
+fn get_app_paths(app: tauri::AppHandle) -> AppResult {
+ let dir = get_data_dir(&app).map_err(|e| e.to_string())?;
+ Ok(serde_json::json!({ "appDataDir": dir.to_string_lossy() }))
+}
+
+#[tauri::command]
+fn sqlite_status(db: State) -> AppResult {
+ Ok(serde_json::json!({
+ "dbPath": db.db_path.to_string_lossy(),
+ "ok": true
+ }))
+}
+
+#[derive(Debug, Deserialize)]
+struct StartDemoJobInput {
+ stage_count: Option,
+}
+
+#[tauri::command]
+async fn start_demo_job(
+ app: tauri::AppHandle,
+ jobs: State<'_, JobsState>,
+ input: StartDemoJobInput,
+) -> AppResult {
+ let job_id = Uuid::new_v4();
+ {
+ let mut active = jobs.active.lock().await;
+ active.push(job_id);
+ }
+
+ let stages = input.stage_count.unwrap_or(5).max(1);
+
+ tauri::async_runtime::spawn(async move {
+ for i in 0..stages {
+ let percent = (((i + 1) as f32 / stages as f32) * 100.0) as u8;
+ let event = JobProgressEvent {
+ job_id: job_id.to_string(),
+ stage: format!("stage_{}", i + 1),
+ percent,
+ message: format!("Completed stage {}/{}", i + 1, stages),
+ };
+ let _ = app.emit("job_progress", event);
+ tokio::time::sleep(std::time::Duration::from_millis(600)).await;
+ }
+ let _ = app.emit(
+ "job_done",
+ serde_json::json!({ "job_id": job_id.to_string() }),
+ );
+ // Remove the completed job from the active list
+ if let Ok(state) = app.try_state::() {
+ let mut active = state.active.lock().await;
+ active.retain(|id| *id != job_id);
+ }
+ });
+
+ Ok(job_id.to_string())
+}
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
- tauri::Builder::default()
- .setup(|app| {
- if cfg!(debug_assertions) {
- app.handle().plugin(
- tauri_plugin_log::Builder::default()
- .level(log::LevelFilter::Info)
- .build(),
- )?;
- }
- Ok(())
- })
- .run(tauri::generate_context!())
- .expect("error while running tauri application");
+ tauri::Builder::default()
+ .setup(|app| {
+ if cfg!(debug_assertions) {
+ app.handle().plugin(
+ tauri_plugin_log::Builder::default()
+ .level(log::LevelFilter::Info)
+ .build(),
+ )?;
+ }
+
+ let data_dir = get_data_dir(app.handle())
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
+ let db_path = data_dir.join("db").join("app.sqlite3");
+
+ ensure_sqlite_schema(&db_path)
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
+
+ app.manage(DbState { db_path });
+ app.manage(JobsState {
+ active: Arc::new(Mutex::new(vec![])),
+ });
+
+ Ok(())
+ })
+ .invoke_handler(tauri::generate_handler![
+ get_app_paths,
+ sqlite_status,
+ start_demo_job
+ ])
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
}
+
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 1947af4..d265cfb 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,8 +1,8 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
- "productName": "KDP E-Book Generator",
+ "productName": "Binary Books E-Crafter",
"version": "0.1.0",
- "identifier": "com.tauri.dev",
+ "identifier": "com.binarybooks.ecrafter",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
@@ -12,9 +12,9 @@
"app": {
"windows": [
{
- "title": "KDP E-Book Generator",
- "width": 800,
- "height": 600,
+ "title": "Binary Books E-Crafter",
+ "width": 1280,
+ "height": 800,
"resizable": true,
"fullscreen": false
}
@@ -25,13 +25,19 @@
},
"bundle": {
"active": true,
- "targets": "all",
+ "targets": ["msi"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
- ]
+ ],
+ "windows": {
+ "wix": {
+ "language": "en-US"
+ }
+ }
}
}
+
diff --git a/src/tauri/backend.ts b/src/tauri/backend.ts
new file mode 100644
index 0000000..7d655ad
--- /dev/null
+++ b/src/tauri/backend.ts
@@ -0,0 +1,29 @@
+import { invoke } from "@tauri-apps/api/core";
+import { listen } from "@tauri-apps/api/event";
+
+export type JobProgressEvent = {
+ job_id: string;
+ stage: string;
+ percent: number;
+ message: string;
+};
+
+export async function getAppPaths() {
+ return invoke<{ appDataDir: string }>("get_app_paths");
+}
+
+export async function sqliteStatus() {
+ return invoke<{ dbPath: string; ok: boolean }>("sqlite_status");
+}
+
+export async function startDemoJob(stageCount = 5) {
+ return invoke("start_demo_job", { input: { stage_count: stageCount } });
+}
+
+export function onJobProgress(cb: (e: JobProgressEvent) => void) {
+ return listen("job_progress", (event) => cb(event.payload));
+}
+
+export function onJobDone(cb: (payload: { job_id: string }) => void) {
+ return listen<{ job_id: string }>("job_done", (event) => cb(event.payload));
+}