Phase 1 foundation: Tauri shell, Python sidecar, SQLite database

Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors

Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
  - Full schema: projects, media_files, speakers, segments, words,
    ai_outputs, annotations (with indexes)
  - Model structs with serde serialization
  - CRUD queries for projects, speakers, segments, words
  - Segment text editing preserves original text
  - Schema versioning for future migrations
  - 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management

Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean

Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 15:16:06 -08:00
parent c450ef3c0c
commit 503cc6c0cf
95 changed files with 9607 additions and 0 deletions

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5385
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "voice-to-notes"
version = "0.1.0"
description = "Voice to Notes — desktop transcription with speaker identification"
authors = ["Voice to Notes Contributors"]
license = "MIT"
edition = "2021"
[lib]
name = "voice_to_notes_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.31", features = ["bundled"] }
uuid = { version = "1", features = ["v4", "serde"] }
thiserror = "1"
chrono = { version = "0.4", features = ["serde"] }

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,2 @@
// AI provider commands — chat, summarize via Python sidecar
// TODO: Implement when AI provider service is built

View File

@@ -0,0 +1,2 @@
// Export commands — trigger caption/text export via Python sidecar
// TODO: Implement when export service is built

View File

@@ -0,0 +1,6 @@
pub mod ai;
pub mod export;
pub mod project;
pub mod settings;
pub mod system;
pub mod transcribe;

View File

@@ -0,0 +1,27 @@
use crate::db::models::Project;
#[tauri::command]
pub fn create_project(name: String) -> Result<Project, String> {
// TODO: Use actual database connection from app state
Ok(Project {
id: uuid::Uuid::new_v4().to_string(),
name,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
settings: None,
status: "active".to_string(),
})
}
#[tauri::command]
pub fn get_project(id: String) -> Result<Option<Project>, String> {
// TODO: Use actual database connection from app state
let _ = id;
Ok(None)
}
#[tauri::command]
pub fn list_projects() -> Result<Vec<Project>, String> {
// TODO: Use actual database connection from app state
Ok(vec![])
}

View File

@@ -0,0 +1,2 @@
// Settings commands — app preferences, model selection, AI provider config
// TODO: Implement when settings UI is built

View File

@@ -0,0 +1,2 @@
// System commands — hardware detection, llama-server lifecycle
// TODO: Implement hardware detection and llama-server management

View File

@@ -0,0 +1,2 @@
// Transcription commands — start/stop/monitor transcription via Python sidecar
// TODO: Implement when sidecar IPC is connected

View File

@@ -0,0 +1,22 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("SQLite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("Record not found: {0}")]
NotFound(String),
#[error("Invalid data: {0}")]
InvalidData(String),
}
impl serde::Serialize for DatabaseError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

49
src-tauri/src/db/mod.rs Normal file
View File

@@ -0,0 +1,49 @@
pub mod errors;
pub mod models;
pub mod queries;
pub mod schema;
use std::path::Path;
use rusqlite::Connection;
use errors::DatabaseError;
/// Open a SQLite database at the given path, creating tables if needed.
pub fn open_database(path: &Path) -> Result<Connection, DatabaseError> {
let conn = Connection::open(path)?;
// Enable WAL mode for concurrent reads
conn.pragma_update(None, "journal_mode", "WAL")?;
// Set busy timeout to 5 seconds
conn.pragma_update(None, "busy_timeout", 5000)?;
// Enable foreign key constraints
conn.pragma_update(None, "foreign_keys", "ON")?;
schema::create_tables(&conn)?;
Ok(conn)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_in_memory() {
let conn = Connection::open_in_memory().unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
schema::create_tables(&conn).unwrap();
// Verify tables exist by querying sqlite_master
let count: i32 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
[],
|row| row.get(0),
)
.unwrap();
// 8 tables: schema_version, projects, media_files, speakers, segments, words, ai_outputs, annotations
assert_eq!(count, 8);
}
}

View File

@@ -0,0 +1,84 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
pub id: String,
pub name: String,
pub created_at: String,
pub updated_at: String,
pub settings: Option<String>,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaFile {
pub id: String,
pub project_id: String,
pub file_path: String,
pub file_hash: Option<String>,
pub duration_ms: Option<i64>,
pub sample_rate: Option<i32>,
pub channels: Option<i32>,
pub format: Option<String>,
pub file_size: Option<i64>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Speaker {
pub id: String,
pub project_id: String,
pub label: String,
pub display_name: Option<String>,
pub color: Option<String>,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Segment {
pub id: String,
pub project_id: String,
pub media_file_id: String,
pub speaker_id: Option<String>,
pub start_ms: i64,
pub end_ms: i64,
pub text: String,
pub original_text: Option<String>,
pub confidence: Option<f64>,
pub is_edited: bool,
pub edited_at: Option<String>,
pub segment_index: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Word {
pub id: String,
pub segment_id: String,
pub word: String,
pub start_ms: i64,
pub end_ms: i64,
pub confidence: Option<f64>,
pub word_index: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiOutput {
pub id: String,
pub project_id: String,
pub output_type: String,
pub prompt: Option<String>,
pub content: String,
pub provider: Option<String>,
pub created_at: String,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Annotation {
pub id: String,
pub project_id: String,
pub start_ms: i64,
pub end_ms: Option<i64>,
pub text: String,
pub annotation_type: String,
}

303
src-tauri/src/db/queries.rs Normal file
View File

@@ -0,0 +1,303 @@
use chrono::Utc;
use rusqlite::{params, Connection};
use uuid::Uuid;
use super::errors::DatabaseError;
use super::models::*;
// ── Projects ──────────────────────────────────────────────────────
pub fn create_project(conn: &Connection, name: &str) -> Result<Project, DatabaseError> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO projects (id, name, created_at, updated_at, status) VALUES (?1, ?2, ?3, ?4, 'active')",
params![id, name, now, now],
)?;
get_project(conn, &id)?.ok_or_else(|| DatabaseError::NotFound("project".into()))
}
pub fn get_project(conn: &Connection, id: &str) -> Result<Option<Project>, DatabaseError> {
let mut stmt = conn.prepare(
"SELECT id, name, created_at, updated_at, settings, status FROM projects WHERE id = ?1",
)?;
let mut rows = stmt.query_map(params![id], |row| {
Ok(Project {
id: row.get(0)?,
name: row.get(1)?,
created_at: row.get(2)?,
updated_at: row.get(3)?,
settings: row.get(4)?,
status: row.get(5)?,
})
})?;
match rows.next() {
Some(row) => Ok(Some(row?)),
None => Ok(None),
}
}
pub fn list_projects(conn: &Connection) -> Result<Vec<Project>, DatabaseError> {
let mut stmt = conn.prepare(
"SELECT id, name, created_at, updated_at, settings, status FROM projects WHERE status = 'active' ORDER BY updated_at DESC",
)?;
let rows = stmt.query_map([], |row| {
Ok(Project {
id: row.get(0)?,
name: row.get(1)?,
created_at: row.get(2)?,
updated_at: row.get(3)?,
settings: row.get(4)?,
status: row.get(5)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
pub fn update_project(
conn: &Connection,
id: &str,
name: Option<&str>,
settings: Option<&str>,
) -> Result<(), DatabaseError> {
let now = Utc::now().to_rfc3339();
if let Some(name) = name {
conn.execute(
"UPDATE projects SET name = ?1, updated_at = ?2 WHERE id = ?3",
params![name, now, id],
)?;
}
if let Some(settings) = settings {
conn.execute(
"UPDATE projects SET settings = ?1, updated_at = ?2 WHERE id = ?3",
params![settings, now, id],
)?;
}
Ok(())
}
pub fn delete_project(conn: &Connection, id: &str) -> Result<(), DatabaseError> {
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE projects SET status = 'deleted', updated_at = ?1 WHERE id = ?2",
params![now, id],
)?;
Ok(())
}
// ── Speakers ──────────────────────────────────────────────────────
pub fn create_speaker(
conn: &Connection,
project_id: &str,
label: &str,
color: Option<&str>,
) -> Result<Speaker, DatabaseError> {
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO speakers (id, project_id, label, color) VALUES (?1, ?2, ?3, ?4)",
params![id, project_id, label, color],
)?;
Ok(Speaker {
id,
project_id: project_id.to_string(),
label: label.to_string(),
display_name: None,
color: color.map(String::from),
metadata: None,
})
}
pub fn get_speakers_for_project(
conn: &Connection,
project_id: &str,
) -> Result<Vec<Speaker>, DatabaseError> {
let mut stmt = conn.prepare(
"SELECT id, project_id, label, display_name, color, metadata FROM speakers WHERE project_id = ?1",
)?;
let rows = stmt.query_map(params![project_id], |row| {
Ok(Speaker {
id: row.get(0)?,
project_id: row.get(1)?,
label: row.get(2)?,
display_name: row.get(3)?,
color: row.get(4)?,
metadata: row.get(5)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
pub fn rename_speaker(
conn: &Connection,
speaker_id: &str,
display_name: &str,
) -> Result<(), DatabaseError> {
conn.execute(
"UPDATE speakers SET display_name = ?1 WHERE id = ?2",
params![display_name, speaker_id],
)?;
Ok(())
}
// ── Segments ──────────────────────────────────────────────────────
pub fn get_segments_for_media(
conn: &Connection,
media_file_id: &str,
) -> Result<Vec<Segment>, DatabaseError> {
let mut stmt = conn.prepare(
"SELECT id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, original_text, confidence, is_edited, edited_at, segment_index FROM segments WHERE media_file_id = ?1 ORDER BY segment_index",
)?;
let rows = stmt.query_map(params![media_file_id], |row| {
Ok(Segment {
id: row.get(0)?,
project_id: row.get(1)?,
media_file_id: row.get(2)?,
speaker_id: row.get(3)?,
start_ms: row.get(4)?,
end_ms: row.get(5)?,
text: row.get(6)?,
original_text: row.get(7)?,
confidence: row.get(8)?,
is_edited: row.get(9)?,
edited_at: row.get(10)?,
segment_index: row.get(11)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
pub fn update_segment_text(
conn: &Connection,
segment_id: &str,
new_text: &str,
) -> Result<(), DatabaseError> {
let now = Utc::now().to_rfc3339();
// Preserve original text on first edit
conn.execute(
"UPDATE segments SET original_text = COALESCE(original_text, text), text = ?1, is_edited = 1, edited_at = ?2 WHERE id = ?3",
params![new_text, now, segment_id],
)?;
Ok(())
}
pub fn reassign_speaker(
conn: &Connection,
segment_id: &str,
new_speaker_id: &str,
) -> Result<(), DatabaseError> {
conn.execute(
"UPDATE segments SET speaker_id = ?1 WHERE id = ?2",
params![new_speaker_id, segment_id],
)?;
Ok(())
}
// ── Words ─────────────────────────────────────────────────────────
pub fn get_words_for_segment(
conn: &Connection,
segment_id: &str,
) -> Result<Vec<Word>, DatabaseError> {
let mut stmt = conn.prepare(
"SELECT id, segment_id, word, start_ms, end_ms, confidence, word_index FROM words WHERE segment_id = ?1 ORDER BY word_index",
)?;
let rows = stmt.query_map(params![segment_id], |row| {
Ok(Word {
id: row.get(0)?,
segment_id: row.get(1)?,
word: row.get(2)?,
start_ms: row.get(3)?,
end_ms: row.get(4)?,
confidence: row.get(5)?,
word_index: row.get(6)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::schema;
fn setup_db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
schema::create_tables(&conn).unwrap();
conn
}
#[test]
fn test_project_crud() {
let conn = setup_db();
// Create
let project = create_project(&conn, "Test Project").unwrap();
assert_eq!(project.name, "Test Project");
assert_eq!(project.status, "active");
// Read
let fetched = get_project(&conn, &project.id).unwrap().unwrap();
assert_eq!(fetched.name, "Test Project");
// List
let projects = list_projects(&conn).unwrap();
assert_eq!(projects.len(), 1);
// Update
update_project(&conn, &project.id, Some("Renamed"), None).unwrap();
let updated = get_project(&conn, &project.id).unwrap().unwrap();
assert_eq!(updated.name, "Renamed");
// Delete (soft)
delete_project(&conn, &project.id).unwrap();
let active = list_projects(&conn).unwrap();
assert_eq!(active.len(), 0);
}
#[test]
fn test_speaker_operations() {
let conn = setup_db();
let project = create_project(&conn, "Test").unwrap();
let speaker = create_speaker(&conn, &project.id, "Speaker 1", Some("#ff0000")).unwrap();
assert_eq!(speaker.label, "Speaker 1");
rename_speaker(&conn, &speaker.id, "Alice").unwrap();
let speakers = get_speakers_for_project(&conn, &project.id).unwrap();
assert_eq!(speakers[0].display_name.as_deref(), Some("Alice"));
}
#[test]
fn test_segment_text_editing() {
let conn = setup_db();
let project = create_project(&conn, "Test").unwrap();
// Insert a media file first
let media_id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO media_files (id, project_id, file_path, created_at) VALUES (?1, ?2, ?3, ?4)",
params![media_id, project.id, "test.wav", now],
)
.unwrap();
// Insert a segment
let seg_id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO segments (id, project_id, media_file_id, start_ms, end_ms, text, segment_index) VALUES (?1, ?2, ?3, 0, 1000, 'hello world', 0)",
params![seg_id, project.id, media_id],
)
.unwrap();
// Edit the text
update_segment_text(&conn, &seg_id, "hello everyone").unwrap();
let segments = get_segments_for_media(&conn, &media_id).unwrap();
assert_eq!(segments[0].text, "hello everyone");
assert_eq!(segments[0].original_text.as_deref(), Some("hello world"));
assert!(segments[0].is_edited);
}
}

136
src-tauri/src/db/schema.rs Normal file
View File

@@ -0,0 +1,136 @@
use rusqlite::Connection;
use super::errors::DatabaseError;
const CURRENT_SCHEMA_VERSION: i32 = 1;
pub fn create_tables(conn: &Connection) -> Result<(), DatabaseError> {
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
settings TEXT,
status TEXT NOT NULL DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS media_files (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
file_path TEXT NOT NULL,
file_hash TEXT,
duration_ms INTEGER,
sample_rate INTEGER,
channels INTEGER,
format TEXT,
file_size INTEGER,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS speakers (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
label TEXT NOT NULL,
display_name TEXT,
color TEXT,
metadata TEXT
);
CREATE TABLE IF NOT EXISTS segments (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
media_file_id TEXT NOT NULL REFERENCES media_files(id),
speaker_id TEXT REFERENCES speakers(id),
start_ms INTEGER NOT NULL,
end_ms INTEGER NOT NULL,
text TEXT NOT NULL,
original_text TEXT,
confidence REAL,
is_edited INTEGER NOT NULL DEFAULT 0,
edited_at TEXT,
segment_index INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS words (
id TEXT PRIMARY KEY,
segment_id TEXT NOT NULL REFERENCES segments(id),
word TEXT NOT NULL,
start_ms INTEGER NOT NULL,
end_ms INTEGER NOT NULL,
confidence REAL,
word_index INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS ai_outputs (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
output_type TEXT NOT NULL,
prompt TEXT,
content TEXT NOT NULL,
provider TEXT,
created_at TEXT NOT NULL,
metadata TEXT
);
CREATE TABLE IF NOT EXISTS annotations (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
start_ms INTEGER NOT NULL,
end_ms INTEGER,
text TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'bookmark'
);
CREATE INDEX IF NOT EXISTS idx_segments_project ON segments(project_id, segment_index);
CREATE INDEX IF NOT EXISTS idx_segments_time ON segments(media_file_id, start_ms);
CREATE INDEX IF NOT EXISTS idx_words_segment ON words(segment_id, word_index);
CREATE INDEX IF NOT EXISTS idx_words_time ON words(start_ms, end_ms);
CREATE INDEX IF NOT EXISTS idx_ai_outputs_project ON ai_outputs(project_id, output_type);
",
)?;
// Initialize schema version if empty
let count: i32 = conn.query_row(
"SELECT COUNT(*) FROM schema_version",
[],
|row| row.get(0),
)?;
if count == 0 {
conn.execute(
"INSERT INTO schema_version (version) VALUES (?1)",
[CURRENT_SCHEMA_VERSION],
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_tables_idempotent() {
let conn = Connection::open_in_memory().unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
create_tables(&conn).unwrap();
// Running again should be fine (IF NOT EXISTS)
create_tables(&conn).unwrap();
}
#[test]
fn test_schema_version() {
let conn = Connection::open_in_memory().unwrap();
create_tables(&conn).unwrap();
let version: i32 = conn
.query_row("SELECT version FROM schema_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, CURRENT_SCHEMA_VERSION);
}
}

18
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,18 @@
pub mod commands;
pub mod db;
pub mod state;
use commands::project::{create_project, get_project, list_projects};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
create_project,
get_project,
list_projects,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
voice_to_notes_lib::run()
}

19
src-tauri/src/state.rs Normal file
View File

@@ -0,0 +1,19 @@
use std::path::PathBuf;
use std::sync::Mutex;
use rusqlite::Connection;
/// Shared application state managed by Tauri.
pub struct AppState {
pub db: Mutex<Option<Connection>>,
pub data_dir: PathBuf,
}
impl AppState {
pub fn new(data_dir: PathBuf) -> Self {
Self {
db: Mutex::new(None),
data_dir,
}
}
}

37
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Voice to Notes",
"version": "0.1.0",
"identifier": "com.voicetonotes.app",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "Voice to Notes",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}