skillok meeting
169ce64e1705fd64e48ee357a816bbdb90bab01d
Перейти к файлу
Код
meeting src-taurisrcshell.rs
anc Этап 1: запись встречи в Zoom, живая расшифровка и разбор - crates/audio: захват звука процесса Zoom и микрофона (WASAPI, process loopback), формат 16 кГц моно, нарезка на куски по паузам с отсевом тишины, распознавание начала и конца встречи по звуковой сессии Zoom. - Ядро: запись двух дорожек → Whisper через OpenRouter → расшифровка, живой конспект раз в три минуты, по окончании — разбор сам; звук удаляется после разбора, если не включено хранение. - Контроллер: слежу за Zoom → прошу предупредить участников → пишу; запись вручную, пауза, стоп; прерванная запись помечается при запуске. - Трей, уведомления, крестик прячет окно, запуск вместе с Windows. - Интерфейс: главный экран с карточкой состояния (задача #1), живая расшифровка и конспект на экране встречи, новые настройки. - reqwest на системном TLS: под Windows без C-кода ring. - 67 тестов; код под Windows проверен компиляцией и clippy с Mac, на Windows не запускался. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 169ce64 30 сентября 2026 г.
shell.rs
//! Оболочка Tauri: окно, трей, уведомления, автозапуск и события интерфейса.
//!
//! Программа живёт в трее: крестик прячет окно, а не выключает её — иначе
//! закрыл окно и встреча не записалась. Выключить — «Выйти» в меню трея.

use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_notification::NotificationExt;

use crate::controller::{FinishStep, Outlet, Status};
use crate::recorder;

const TRAY_ID: &str = "main";

pub struct TauriOutlet {
    pub app: AppHandle,
}

impl Outlet for TauriOutlet {
    fn status(&self, status: &Status) {
        let _ = self.app.emit("meet://status", status);
        if let Some(tray) = self.app.tray_by_id(TRAY_ID) {
            let _ = tray.set_tooltip(Some(tooltip(status)));
        }
    }

    fn recorder_event(&self, meeting_id: &str, event: &recorder::Event) {
        let _ = self.app.emit(
            "meet://recorder",
            serde_json::json!({ "meeting_id": meeting_id, "event": event }),
        );
    }

    fn meeting_changed(&self, meeting_id: &str) {
        let _ = self.app.emit("meet://meeting-changed", meeting_id);
    }

    fn notify(&self, title: &str, body: &str) {
        let _ = self.app.notification().builder().title(title).body(body).show();
    }

    fn show_window(&self) {
        show_main(&self.app);
    }
}

fn tooltip(status: &Status) -> &'static str {
    match status {
        Status::Unsupported => "Skillok Meet",
        Status::Watching => "Skillok Meet — жду встречу в Zoom",
        Status::AwaitingConsent => "Skillok Meet — предупредите участников",
        Status::Recording { paused: true, .. } => "Skillok Meet — запись на паузе",
        Status::Recording { .. } => "Skillok Meet — идёт запись",
        Status::Finishing {
            step: FinishStep::Transcribing,
            ..
        } => "Skillok Meet — дописываю расшифровку",
        Status::Finishing {
            step: FinishStep::Analyzing,
            ..
        } => "Skillok Meet — разбираю встречу",
    }
}

pub fn show_main(app: &AppHandle) {
    if let Some(w) = app.get_webview_window("main") {
        let _ = w.show();
        let _ = w.unminimize();
        let _ = w.set_focus();
    }
}

pub fn build_tray(app: &AppHandle) -> tauri::Result<()> {
    let open = MenuItem::with_id(app, "open", "Открыть Skillok Meet", true, None::<&str>)?;
    let quit = MenuItem::with_id(app, "quit", "Выйти", true, None::<&str>)?;
    let menu = Menu::with_items(app, &[&open, &quit])?;
    let mut tray = TrayIconBuilder::with_id(TRAY_ID)
        .tooltip("Skillok Meet")
        .menu(&menu)
        .show_menu_on_left_click(false)
        .on_menu_event(|app, event| match event.id.as_ref() {
            "open" => show_main(app),
            "quit" => app.exit(0),
            _ => {}
        })
        .on_tray_icon_event(|tray, event| {
            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                show_main(tray.app_handle());
            }
        });
    if let Some(icon) = app.default_window_icon() {
        tray = tray.icon(icon.clone());
    }
    tray.build(app)?;
    Ok(())
}

/// Запуск вместе с Windows. Вне Windows не трогаем: на Mac идёт разработка,
/// и регистрировать там автозапуск отладочной сборки незачем.
pub fn apply_autostart(_app: &AppHandle, _enabled: bool) {
    #[cfg(windows)]
    {
        use tauri_plugin_autostart::ManagerExt;
        let manager = _app.autolaunch();
        let _ = if _enabled {
            manager.enable()
        } else {
            manager.disable()
        };
    }
}