use std::path::PathBuf;
use std::sync::Arc;
use chrono::Local;
use serde::Serialize;
use tauri::State;
use crate::controller::{Controller, Status};
use crate::meetings::{Meeting, MeetingDetail, Store};
use crate::openrouter::{Client, KeyInfo};
use crate::settings::{mask_key, Secrets, Settings, OPENROUTER_KEY};
pub struct AppState {
pub settings_path: PathBuf,
pub store: Arc<Store>,
pub secrets: Arc<dyn Secrets>,
pub controller: Arc<Controller>,
}
impl AppState {
fn settings(&self) -> Result<Settings, String> {
Settings::load(&self.settings_path).map_err(|e| e.to_string())
}
fn key(&self) -> Result<String, String> {
self.secrets
.get(OPENROUTER_KEY)
.map_err(|e| e.to_string())?
.ok_or_else(|| "Ключ OpenRouter не задан — добавьте его в настройках".into())
}
}
pub async fn analyze(store: &Store, settings: &Settings, key: &str, id: &str) -> Result<Meeting, String> {
let detail = store.get(id).map_err(|e| e.to_string())?;
if detail.transcript.segments.is_empty() {
return Err("В расшифровке нет ни одной реплики".into());
}
let model = &settings.analysis_model;
let (analysis, usage) = Client::new(key)
.analyze(model, &detail.meeting.title, &detail.transcript)
.await
.map_err(|e| e.to_string())?;
store
.save_analysis(id, &analysis, model, usage, Local::now())
.map_err(|e| e.to_string())
}
#[derive(Serialize)]
pub struct SettingsView {
settings: Settings,
openrouter_key: Option<String>,
meetings_dir: String,
capture_supported: bool,
}
#[tauri::command]
pub fn get_settings(state: State<'_, AppState>) -> Result<SettingsView, String> {
Ok(SettingsView {
settings: state.settings()?,
openrouter_key: state
.secrets
.get(OPENROUTER_KEY)
.map_err(|e| e.to_string())?
.map(|k| mask_key(&k)),
meetings_dir: state.store.root().display().to_string(),
capture_supported: state.controller.status() != Status::Unsupported,
})
}
#[tauri::command]
pub fn save_settings(
app: tauri::AppHandle,
state: State<'_, AppState>,
settings: Settings,
) -> Result<(), String> {
settings.save(&state.settings_path).map_err(|e| e.to_string())?;
crate::shell::apply_autostart(&app, settings.launch_at_login);
Ok(())
}
#[tauri::command]
pub async fn set_openrouter_key(state: State<'_, AppState>, key: String) -> Result<KeyInfo, String> {
let key = key.trim().to_string();
if key.is_empty() {
return Err("Вставьте ключ OpenRouter".into());
}
let info = Client::new(key.clone())
.key_info()
.await
.map_err(|e| e.to_string())?;
state
.secrets
.set(OPENROUTER_KEY, &key)
.map_err(|e| e.to_string())?;
Ok(info)
}
#[tauri::command]
pub async fn check_openrouter_key(state: State<'_, AppState>) -> Result<KeyInfo, String> {
Client::new(state.key()?)
.key_info()
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn delete_openrouter_key(state: State<'_, AppState>) -> Result<(), String> {
state.secrets.delete(OPENROUTER_KEY).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn list_meetings(state: State<'_, AppState>) -> Result<Vec<Meeting>, String> {
state.store.list().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_meeting(state: State<'_, AppState>, id: String) -> Result<MeetingDetail, String> {
state.store.get(&id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn import_meeting(state: State<'_, AppState>, title: String, text: String) -> Result<Meeting, String> {
state
.store
.import(&title, &text, Local::now())
.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn analyze_meeting(state: State<'_, AppState>, id: String) -> Result<Meeting, String> {
let settings = state.settings()?;
let key = state.key()?;
analyze(&state.store, &settings, &key, &id).await
}
#[tauri::command]
pub fn recorder_status(state: State<'_, AppState>) -> Status {
state.controller.status()
}
#[tauri::command]
pub fn recorder_consent(state: State<'_, AppState>, record: bool) -> Result<(), String> {
state.controller.consent(record)
}
#[tauri::command]
pub fn recorder_start(state: State<'_, AppState>) -> Result<Meeting, String> {
state.controller.start_manual()
}
#[tauri::command]
pub fn recorder_pause(state: State<'_, AppState>, paused: bool) -> Result<(), String> {
state.controller.pause(paused)
}
#[tauri::command]
pub fn recorder_stop(state: State<'_, AppState>) -> Result<(), String> {
if !matches!(state.controller.status(), Status::Recording { .. }) {
return Err("Запись не идёт".into());
}
let controller = state.controller.clone();
tauri::async_runtime::spawn(async move {
let _ = controller.stop().await;
});
Ok(())
}