Update book handling and improve offline/online sync logic

- Enhanced the `BookProps` struct with updated field mappings for better API compatibility.
- Improved offline/online sync workflows in `BookList` by adding `localBook` property handling and new item count methods for segmented tracking of local/online items.
- Updated click handlers in `BookList` to fetch data based on connectivity state and prioritize local data when offline.
- Refactored the decryption and vault handling logic in Rust to remove obsolete legacy methods and standardize debug behavior.
- Introduced `ScribeShell` layout component with foundational logic for book/chapter syncing and offline queue handling.
- Added `init_panic_hook` to improve crash reporting during Rust app initialization.
This commit is contained in:
natreex
2026-04-05 11:36:12 -04:00
parent b9bc024e91
commit 2b6d4cc48b
6 changed files with 585 additions and 50 deletions

View File

@@ -6,7 +6,6 @@ use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::{AppError, AppResult};
@@ -44,9 +43,6 @@ const KEYRING_USER: &str = "vault-key";
/// Falls back to the old derivation method if the keyring is unavailable,
/// and attempts to migrate the key into the keyring for next time.
fn get_vault_key() -> [u8; 32] {
if cfg!(debug_assertions) {
return derive_machine_key_legacy();
}
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
if let Ok(entry) = &entry {
if let Ok(stored) = entry.get_password() {
@@ -68,19 +64,6 @@ fn get_vault_key() -> [u8; 32] {
key
}
/// Legacy derivation for migrating vaults created before keyring support.
fn derive_machine_key_legacy() -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(SERVICE_NAME.as_bytes());
if let Ok(name) = hostname::get() {
hasher.update(name.as_encoded_bytes());
}
if let Some(dir) = dirs_next::home_dir() {
hasher.update(dir.to_string_lossy().as_bytes());
}
hasher.finalize().into()
}
fn encrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
let mut iv = [0u8; IV_LENGTH];
rand::rng().fill_bytes(&mut iv);
@@ -119,6 +102,12 @@ fn read_vault() -> AppResult<SecureVault> {
}
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
if cfg!(debug_assertions) {
return serde_json::from_str::<SecureVault>(&content)
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)));
}
let raw = BASE64.decode(content.trim())
.map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?;
@@ -129,34 +118,29 @@ fn read_vault() -> AppResult<SecureVault> {
}
}
let legacy_key = derive_machine_key_legacy();
if let Ok(decrypted) = decrypt_vault(&raw, &legacy_key) {
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
let _ = write_vault_with_key(&vault, &key);
return Ok(vault);
}
}
Ok(SecureVault::default())
Err(AppError::Keyring("Vault decryption failed — cannot read existing vault".into()))
}
fn write_vault_with_key(vault: &SecureVault, key: &[u8; 32]) -> AppResult<()> {
fn write_vault(vault: &SecureVault) -> AppResult<()> {
let path = vault_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create vault dir: {}", e)))?;
}
if cfg!(debug_assertions) {
let json = serde_json::to_string_pretty(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
return fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)));
}
let key = get_vault_key();
let json = serde_json::to_string(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
let encrypted = encrypt_vault(json.as_bytes(), key)?;
let encrypted = encrypt_vault(json.as_bytes(), &key)?;
let encoded = BASE64.encode(&encrypted);
fs::write(&path, encoded).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)))
}
fn write_vault(vault: &SecureVault) -> AppResult<()> {
let key = get_vault_key();
write_vault_with_key(vault, &key)
}
// ===== Public API (same signatures as before) =====
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {

View File

@@ -35,15 +35,19 @@ pub struct BookToolsSettings {
#[serde(rename_all = "camelCase")]
pub struct BookProps {
pub book_id: String,
#[serde(rename = "type")]
pub book_type: String,
pub author_id: String,
pub title: String,
pub sub_title: String,
pub summary: String,
#[serde(rename = "serie")]
pub serie_id: i64,
pub series_id: Option<String>,
#[serde(rename = "publicationDate")]
pub desired_release_date: String,
pub desired_word_count: i64,
#[serde(rename = "totalWordCount")]
pub word_count: i64,
pub cover_image: String,
pub book_meta: Option<String>,

View File

@@ -6,10 +6,13 @@ mod helpers;
mod shared;
use db::connection::create_db_manager;
use shared::crash_reporter::init_panic_hook;
use shared::session::create_session;
use std::path::PathBuf;
pub fn run() {
init_panic_hook();
let app_data_dir = dirs_next::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("com.eritors.scribe.desktop");

View File

@@ -1,2 +1,3 @@
pub mod crash_reporter;
pub mod session;
pub mod types;