Initial commit: Voiced invoice desktop app
Offline-first invoicing app for Arun P, built by Bongbetic. - Tauri v2 (Rust) + React/TypeScript + IBM Carbon Design System UI - SQLite (rusqlite) storage with a persistent invoice-number series - Guided first-run setup, pre-filled vendor/bank details - Fixed-amount or rate x quantity line items (per sec/min/hour/session/unit) - GST as CGST+SGST or IGST, Indian amount-in-words - Optional signature, with a computer-generated note as fallback - Print-ready PDF export via @react-pdf/renderer - Windows cross-compile via cargo-xwin (NSIS installer) Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
use crate::AppState;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::State;
|
||||
|
||||
fn safe_kind(kind: &str) -> String {
|
||||
kind.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
|
||||
.collect::<String>()
|
||||
.to_lowercase()
|
||||
}
|
||||
|
||||
fn sanitize_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '-' || *c == '_')
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extension_of(path: &str) -> String {
|
||||
Path::new(path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.to_lowercase())
|
||||
.unwrap_or_else(|| "png".to_string())
|
||||
}
|
||||
|
||||
fn mime_for(ext: &str) -> &'static str {
|
||||
match ext {
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"svg" => "image/svg+xml",
|
||||
"webp" => "image/webp",
|
||||
"gif" => "image/gif",
|
||||
_ => "image/png",
|
||||
}
|
||||
}
|
||||
|
||||
fn assets_dir(state: &State<AppState>) -> Result<PathBuf, String> {
|
||||
let dir = state.data_dir.join("assets");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn write_asset(state: &State<AppState>, kind: &str, ext: &str, bytes: &[u8]) -> Result<String, String> {
|
||||
let dir = assets_dir(state)?;
|
||||
let file = format!(
|
||||
"{}-{}.{}",
|
||||
safe_kind(kind),
|
||||
uuid::Uuid::new_v4().simple(),
|
||||
ext
|
||||
);
|
||||
let path = dir.join(file);
|
||||
std::fs::write(&path, bytes).map_err(|e| e.to_string())?;
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Copy a user-picked file into the app's asset store and return the stored path.
|
||||
#[tauri::command]
|
||||
pub fn import_asset(
|
||||
state: State<AppState>,
|
||||
source_path: String,
|
||||
kind: String,
|
||||
) -> Result<String, String> {
|
||||
let bytes = std::fs::read(&source_path).map_err(|e| e.to_string())?;
|
||||
let ext = extension_of(&source_path);
|
||||
write_asset(&state, &kind, &ext, &bytes)
|
||||
}
|
||||
|
||||
/// Used when the frontend already holds the bytes (base64) instead of a path.
|
||||
#[tauri::command]
|
||||
pub fn save_asset_bytes(
|
||||
state: State<AppState>,
|
||||
kind: String,
|
||||
file_name: String,
|
||||
data_base64: String,
|
||||
) -> Result<String, String> {
|
||||
let bytes = STANDARD
|
||||
.decode(data_base64.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
let ext = extension_of(&sanitize_name(&file_name));
|
||||
write_asset(&state, &kind, &ext, &bytes)
|
||||
}
|
||||
|
||||
/// Read a stored asset back as a data URI so it can be embedded in the PDF.
|
||||
#[tauri::command]
|
||||
pub fn read_asset_data_uri(path: String) -> Result<String, String> {
|
||||
let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
|
||||
let ext = extension_of(&path);
|
||||
Ok(format!(
|
||||
"data:{};base64,{}",
|
||||
mime_for(&ext),
|
||||
STANDARD.encode(bytes)
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_asset(state: State<AppState>, path: String) -> Result<(), String> {
|
||||
let dir = assets_dir(&state)?;
|
||||
let target = PathBuf::from(&path);
|
||||
// Only allow deleting files inside the managed assets directory.
|
||||
if target.starts_with(&dir) && target.exists() {
|
||||
std::fs::remove_file(&target).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a base64 payload (e.g. a generated PDF) to a user-chosen path.
|
||||
#[tauri::command]
|
||||
pub fn save_binary_file(path: String, data_base64: String) -> Result<(), String> {
|
||||
let bytes = STANDARD
|
||||
.decode(data_base64.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
if let Some(parent) = Path::new(&path).parent() {
|
||||
std::fs::create_dir_all(parent).ok();
|
||||
}
|
||||
std::fs::write(&path, bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user