Remove unused assets, refactor migration handling, and add crash reporting

- Deleted obsolete icons, layout files, and SVG assets.
- Added `useAutoUpdate` hook for streamlined update logic.
- Introduced `auto_migrate_electron` and migration commands for Electron-to-Tauri data migration.
- Implemented `init_panic_hook` for improved crash reporting.
- Updated `.gitignore` for Tauri build output and IDE-specific files.
This commit is contained in:
natreex
2026-04-07 16:43:54 -04:00
parent 5c7e71ce9e
commit 19c8d0057c
26 changed files with 515 additions and 63 deletions

18
.gitignore vendored
View File

@@ -6,10 +6,6 @@ node_modules
# Testing # Testing
/coverage /coverage
# Next.js
/.next/
/out/
# Production # Production
/dist /dist
/build /build
@@ -32,7 +28,17 @@ yarn-error.log*
# TypeScript # TypeScript
*.tsbuildinfo *.tsbuildinfo
# Next.js
.next/
/out/
next-env.d.ts next-env.d.ts
# Electron # Electron build output
/release /release/
# Tauri build output
src-tauri/target/
# IDE
.idea/

View File

@@ -1,31 +0,0 @@
import type {Metadata} from "next";
import "./globals.css";
import {ReactNode} from "react";
import ScribeShell from "@/components/layout/ScribeShell";
export const metadata: Metadata = {
title: "ERitors Scribe",
description: "Les meilleurs livres sont ceux qui ont le meilleur plan.",
icons: {
icon: "/eritors-favicon-white.png"
},
robots: {
index: false,
follow: false,
},
};
export default function RootLayout(
{
children,
}: Readonly<{
children: ReactNode;
}>) {
return (
<html lang="fr">
<body>
<ScribeShell>{children}</ScribeShell>
</body>
</html>
);
}

View File

@@ -8,7 +8,7 @@ import * as tauri from '@/lib/tauri';
import PulseLoader from '@/components/ui/PulseLoader'; import PulseLoader from '@/components/ui/PulseLoader';
import {useTranslations} from '@/lib/i18n'; import {useTranslations} from '@/lib/i18n';
listen('auth-success', () => window.location.reload()); listen('auth-success', () => window.location.reload()).then();
type MigrationState = 'pending' | 'error' | 'done'; type MigrationState = 'pending' | 'error' | 'done';

View File

@@ -1,6 +0,0 @@
'use client';
import BookList from '@/components/book/BookList';
export default function HomePage() {
return <BookList/>;
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

67
hooks/useAutoUpdate.ts Normal file
View File

@@ -0,0 +1,67 @@
import {useEffect, useState} from 'react';
import {check, Update} from '@tauri-apps/plugin-updater';
import {relaunch} from '@tauri-apps/plugin-process';
export interface UpdateState {
available: boolean;
version: string;
notes: string;
downloading: boolean;
progress: number;
install: () => Promise<void>;
dismiss: () => void;
}
export function useAutoUpdate(): UpdateState {
const [available, setAvailable] = useState(false);
const [version, setVersion] = useState('');
const [notes, setNotes] = useState('');
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const [update, setUpdate] = useState<Update | null>(null);
useEffect(() => {
let cancelled = false;
async function checkForUpdate() {
try {
const result = await check();
if (cancelled || !result) return;
setUpdate(result);
setAvailable(true);
setVersion(result.version);
setNotes(result.body ?? '');
} catch (_) {
// silently fail — no update or offline
}
}
checkForUpdate();
return () => { cancelled = true; };
}, []);
async function install() {
if (!update) return;
setDownloading(true);
try {
await update.downloadAndInstall((event) => {
if (event.event === 'Started' && event.data.contentLength) {
setProgress(0);
} else if (event.event === 'Progress') {
setProgress((prev) => prev + event.data.chunkLength);
} else if (event.event === 'Finished') {
setProgress(100);
}
});
await relaunch();
} catch (_) {
setDownloading(false);
}
}
function dismiss() {
setAvailable(false);
}
return {available, version, notes, downloading, progress, install, dismiss};
}

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,3 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000">
<path d="m577.3 0 577.4 1000H0z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 138 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,378 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::State;
use crate::crypto::key_manager;
use crate::db::connection::DbManager;
use crate::db::schema;
use crate::error::{AppError, AppResult};
use crate::shared::session::SessionState;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportMigrationData {
pub migration_file_path: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrationResult {
pub success: bool,
pub user_id: Option<String>,
pub error: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrationCheckResult {
pub found: bool,
pub user_id: Option<String>,
pub has_db: bool,
pub migration_path: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AutoMigrationResult {
pub migrated: bool,
pub user_id: Option<String>,
pub token_migrated: bool,
pub key_migrated: bool,
pub pin_migrated: bool,
pub db_migrated: bool,
pub error: Option<String>,
}
#[derive(Deserialize)]
struct ElectronMigrationFile {
version: u32,
user_id: String,
encryption_key: String,
pin_hash: Option<String>,
db_source: String,
}
// ─── Electron safeStorage vault format ───────────────────────────────────────
#[derive(Deserialize, Default)]
struct ElectronVault {
#[serde(default)]
token: Option<String>,
#[serde(flatten)]
extra: HashMap<String, serde_json::Value>,
}
// ─── Auto-migration from Electron ────────────────────────────────────────────
fn electron_data_paths() -> Vec<PathBuf> {
let home = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("."));
let mut paths = Vec::new();
#[cfg(target_os = "macos")]
{
let app_support = home.join("Library").join("Application Support");
paths.push(app_support.join("ERitors Scribe"));
paths.push(app_support.join("Electron"));
}
#[cfg(target_os = "windows")]
{
if let Some(appdata) = dirs_next::data_dir() {
paths.push(appdata.join("ERitors Scribe"));
paths.push(appdata.join("Electron"));
}
}
paths
}
fn find_electron_vault(data_path: &PathBuf) -> Option<PathBuf> {
let candidate = data_path.join("secure-config.json");
if candidate.exists() { return Some(candidate); }
None
}
fn find_electron_dbs(data_path: &PathBuf) -> Vec<(String, PathBuf)> {
let mut result = Vec::new();
if let Ok(entries) = std::fs::read_dir(data_path) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("eritors-local-") && name.ends_with(".db") {
let user_id = name
.trim_start_matches("eritors-local-")
.trim_end_matches(".db")
.to_string();
result.push((user_id, entry.path()));
}
}
}
result
}
#[cfg(target_os = "macos")]
fn decrypt_electron_value(encrypted_b64: &str) -> Option<String> {
use aes::Aes128;
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut as _, KeyIvInit};
use pbkdf2::pbkdf2_hmac;
use sha1::Sha1;
type Aes128CbcDec = cbc::Decryptor<Aes128>;
let b64 = if encrypted_b64.starts_with("encrypted:") {
&encrypted_b64[10..]
} else {
encrypted_b64
};
let data = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
b64,
).ok()?;
if !data.starts_with(b"v10") {
return None;
}
let ciphertext = &data[3..];
let app_names = ["ERitors Scribe", "Electron"];
for app_name in &app_names {
let service = format!("{} Safe Storage", app_name);
if let Ok(entry) = keyring::Entry::new(&service, app_name) {
if let Ok(password) = entry.get_password() {
let mut key = [0u8; 16];
pbkdf2_hmac::<Sha1>(
password.as_bytes(),
b"saltysalt",
1003,
&mut key,
);
let iv = [0x20u8; 16];
let mut buf = ciphertext.to_vec();
if let Ok(decryptor) = Aes128CbcDec::new_from_slices(&key, &iv) {
if let Ok(decrypted) = decryptor.decrypt_padded_mut::<Pkcs7>(&mut buf) {
if let Ok(s) = String::from_utf8(decrypted.to_vec()) {
return Some(s);
}
}
}
}
}
}
None
}
#[cfg(not(target_os = "macos"))]
fn decrypt_electron_value(_encrypted_b64: &str) -> Option<String> {
None
}
fn read_electron_vault(vault_path: &PathBuf) -> HashMap<String, String> {
let mut result = HashMap::new();
let Ok(content) = std::fs::read_to_string(vault_path) else { return result; };
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&content) else { return result; };
for (key, value) in &parsed {
if let Some(s) = value.as_str() {
if let Some(decrypted) = decrypt_electron_value(s) {
result.insert(key.clone(), decrypted);
} else if !s.starts_with("encrypted:") {
result.insert(key.clone(), s.to_string());
}
}
}
result
}
/// Called automatically at startup in production — detects and migrates Electron data.
#[tauri::command]
pub fn auto_migrate_electron(
db: State<DbManager>,
session: State<SessionState>,
) -> AppResult<AutoMigrationResult> {
let already_migrated = key_manager::get_last_user_id().ok().flatten();
if already_migrated.is_some() {
return Ok(AutoMigrationResult {
migrated: false, user_id: None,
token_migrated: false, key_migrated: false,
pin_migrated: false, db_migrated: false,
error: None,
});
}
for data_path in electron_data_paths() {
if !data_path.exists() { continue; }
let dbs = find_electron_dbs(&data_path);
if dbs.is_empty() { continue; }
let vault_data = if let Some(vault_path) = find_electron_vault(&data_path) {
read_electron_vault(&vault_path)
} else {
HashMap::new()
};
let token = vault_data.get("token").cloned();
let last_user_id = vault_data.get("lastUserId").cloned()
.or_else(|| dbs.first().map(|(uid, _)| uid.clone()));
let Some(user_id) = last_user_id else { continue; };
let encryption_key = vault_data.get(&format!("encryptionKey-{}", user_id)).cloned();
let pin_hash = vault_data.get(&format!("pinHash-{}", user_id)).cloned();
// Copy DB files
let mut db_migrated = false;
for (uid, db_path) in &dbs {
if uid != &user_id { continue; }
{
let mut db_manager = db.lock()
.map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
db_manager.initialize(uid)?;
let dest = db_manager.get_db_path(uid);
if let Some(parent) = dest.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::copy(db_path, &dest).is_ok() {
for ext in &["db-wal", "db-shm"] {
let src_extra = db_path.with_extension(ext);
if src_extra.exists() {
let dst_extra = dest.with_extension(ext);
let _ = std::fs::copy(&src_extra, &dst_extra);
}
}
db_migrated = true;
}
let conn = db_manager.get_connection(uid)?;
schema::initialize_schema(conn)?;
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
}
}
// Store secrets
let token_migrated = if let Some(ref t) = token {
key_manager::set_token(t).is_ok()
} else { false };
let key_migrated = if let Some(ref k) = encryption_key {
key_manager::set_user_encryption_key(&user_id, k).is_ok()
} else { false };
let pin_migrated = if let Some(ref p) = pin_hash {
key_manager::set_pin_hash(&user_id, p).is_ok()
} else { false };
key_manager::set_last_user_id(&user_id)?;
let mut session_guard = session.lock()
.map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
session_guard.user_id = Some(user_id.clone());
return Ok(AutoMigrationResult {
migrated: true,
user_id: Some(user_id),
token_migrated,
key_migrated,
pin_migrated,
db_migrated,
error: None,
});
}
Ok(AutoMigrationResult {
migrated: false, user_id: None,
token_migrated: false, key_migrated: false,
pin_migrated: false, db_migrated: false,
error: None,
})
}
/// Check if an Electron migration file + DB exist at the given path (legacy).
#[tauri::command]
pub fn check_electron_migration(migration_file_path: String) -> AppResult<MigrationCheckResult> {
let path = PathBuf::from(&migration_file_path);
if !path.exists() {
return Ok(MigrationCheckResult { found: false, user_id: None, has_db: false, migration_path: None });
}
let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
let db_name = format!("eritors-local-{}.db", migration.user_id);
let db_path = path.parent().unwrap_or(&path).join(&db_name);
Ok(MigrationCheckResult {
found: true,
user_id: Some(migration.user_id),
has_db: db_path.exists(),
migration_path: Some(migration_file_path),
})
}
/// Import from Electron export file (legacy).
#[tauri::command]
pub fn import_from_electron(
data: ImportMigrationData,
db: State<DbManager>,
session: State<SessionState>,
) -> AppResult<MigrationResult> {
let migration_path = PathBuf::from(&data.migration_file_path);
if !migration_path.exists() {
return Ok(MigrationResult { success: false, user_id: None, error: Some("Migration file not found".into()) });
}
let content = std::fs::read_to_string(&migration_path)?;
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
if migration.version != 1 {
return Ok(MigrationResult { success: false, user_id: None, error: Some(format!("Unsupported version: {}", migration.version)) });
}
let source_db = migration_path.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join(format!("eritors-local-{}.db", migration.user_id));
if !source_db.exists() {
let electron_db = PathBuf::from(&migration.db_source);
if !electron_db.exists() {
return Ok(MigrationResult { success: false, user_id: None, error: Some("Database file not found.".into()) });
}
copy_db_files(&electron_db, &migration.user_id, &db)?;
} else {
copy_db_files(&source_db, &migration.user_id, &db)?;
}
key_manager::set_user_encryption_key(&migration.user_id, &migration.encryption_key)?;
if let Some(ref pin_hash) = migration.pin_hash {
key_manager::set_pin_hash(&migration.user_id, pin_hash)?;
}
key_manager::set_last_user_id(&migration.user_id)?;
{
let mut db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
db_manager.initialize(&migration.user_id)?;
let conn = db_manager.get_connection(&migration.user_id)?;
schema::initialize_schema(conn)?;
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
}
let mut session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
session_guard.user_id = Some(migration.user_id.clone());
cleanup_migration_files(&migration_path, &migration.user_id);
Ok(MigrationResult { success: true, user_id: Some(migration.user_id), error: None })
}
fn cleanup_migration_files(migration_path: &PathBuf, user_id: &str) {
let _ = std::fs::remove_file(migration_path);
if let Some(parent) = migration_path.parent() {
let db_copy = parent.join(format!("eritors-local-{}.db", user_id));
let _ = std::fs::remove_file(&db_copy);
let _ = std::fs::remove_file(db_copy.with_extension("db-wal"));
let _ = std::fs::remove_file(db_copy.with_extension("db-shm"));
}
}
fn copy_db_files(source_db: &PathBuf, user_id: &str, db: &State<DbManager>) -> AppResult<()> {
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
let dest_db = db_manager.get_db_path(user_id);
if let Some(parent) = dest_db.parent() { std::fs::create_dir_all(parent)?; }
std::fs::copy(source_db, &dest_db)?;
for ext in &["db-wal", "db-shm"] {
let src = source_db.with_extension(ext);
if src.exists() {
let dst = dest_db.with_file_name(format!("eritors-local-{}.{}", user_id, ext));
let _ = std::fs::copy(&src, &dst);
}
}
Ok(())
}

View File

@@ -0,0 +1 @@
pub mod commands;

View File

@@ -0,0 +1,56 @@
use serde_json::json;
use std::panic;
const API_URL: &str = if cfg!(debug_assertions) { "http://localhost:3001/" } else { "https://api.eritors.com/" };
const APP_NAME: &str = "Eritors Scribe";
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
fn get_platform() -> &'static str {
if cfg!(target_os = "macos") { "desktop-macos" }
else if cfg!(target_os = "windows") { "desktop-windows" }
else if cfg!(target_os = "linux") { "desktop-linux" }
else { "desktop" }
}
fn get_os_version() -> String {
format!("{} {}", std::env::consts::OS, std::env::consts::ARCH)
}
fn send_crash_report(error_type: &str, error_message: &str, stack_trace: &str) {
let payload = json!({
"appName": APP_NAME,
"appVersion": APP_VERSION,
"platform": get_platform(),
"osVersion": get_os_version(),
"errorType": error_type,
"errorMessage": error_message,
"stackTrace": stack_trace,
});
let url = format!("{}crash-report", API_URL);
let _ = reqwest::blocking::Client::new()
.post(&url)
.json(&payload)
.timeout(std::time::Duration::from_secs(5))
.send();
}
pub fn init_panic_hook() {
let default_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
let message = if let Some(s) = info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
let location = info.location().map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())).unwrap_or_default();
let stack_trace = format!("panic at {}\n{}", location, std::backtrace::Backtrace::force_capture());
send_crash_report("RustPanic", &message, &stack_trace);
default_hook(info);
}));
}