use std::rc::Rc; use wgui::{ components::button::{ButtonClickEvent, ComponentButton}, globals::WguiGlobals, i18n::Translation, layout::WidgetID, log::LogErr, parser::{Fetchable, TemplateParams}, task::Tasks, widget::label::WidgetLabel, windowing::context_menu::{self}, }; use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind}; use crate::{ frontend::FrontendTasks, tab::settings::{ SettingType, SettingsMountParams, SettingsTab, TabNameEnum, Task as ParentTask, UpdateExtra, horiz_cell, macros::{MacroParams, options_category, options_checkbox, options_range_f32}, }, util::{ popup_manager::PopupHolder, whisper::{ WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name, whisper_model_path, }, }, views::{self, ViewUpdateParams}, }; #[derive(Clone)] enum Task { WhisperDownloadClosed, WhisperRemoveUnused, WhisperDownload(&'static WhisperModel), WhisperDownloadDone, CloseDialog, ReloadTab, } pub struct State { popup_download: PopupHolder, popup_dialog: PopupHolder, frontend_tasks: FrontendTasks, globals: WguiGlobals, tasks: Tasks, parent_tasks: Tasks, pending_download: Option<&'static WhisperModel>, } impl SettingsTab for State { fn update(&mut self, par: &mut ViewUpdateParams, _extra: &UpdateExtra) -> anyhow::Result<()> { self.popup_download.update(par)?; self.popup_dialog.update(par)?; for task in self.tasks.drain() { match task { Task::WhisperDownloadClosed => { if let Some(model) = self.pending_download.take() && !whisper_model_path(model.file_name).exists() { // download failed, set to selection to none par.general_config.whisper_model = "".into(); par.config_change_kind.replace(ConfigChangeKind::Other); // reload the tab self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); } } Task::WhisperRemoveUnused => { let _ = whisper_delete_all_models().log_err("could not remove whisper models"); } Task::WhisperDownload(model) => { self.pending_download = Some(model); self.show_whisper_download_dialog(model, par.executor.clone()); } Task::WhisperDownloadDone => { if let Some(model) = self.pending_download.take() { par.general_config.whisper_model = model.file_name.into(); par.config_change_kind.replace(ConfigChangeKind::Other); // reload tab so that the downloaded check marks get populated self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); } } Task::CloseDialog => { let close_dialog = self.popup_dialog.get_close_callback(par.layout); close_dialog(); } Task::ReloadTab => { self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); } } } Ok(()) } fn context_menu_custom( &mut self, action: Rc, config: &mut GeneralConfig, change_kind: &mut Option, layout: &mut wgui::layout::Layout, state: &mut wgui::parser::ParserState, ) -> anyhow::Result<()> { if let (Some(_), Some(id), Some(model_name)) = { let mut s = action.splitn(3, ';'); (s.next(), s.next(), s.next()) } { if !model_name.is_empty() && let Some(model) = whisper_model_from_name(model_name) { let mut common = layout.common(); let mut label = state.fetch_widget_as::(common.state, &format!("{id}_value"))?; label.set_text(&mut common, Translation::from_raw_text(model.display_name)); config.whisper_model = model.file_name.into(); if !whisper_model_path(model.file_name).exists() { self.show_whisper_model_dialog_box_download(model)?; } else { change_kind.replace(ConfigChangeKind::Other); } } else { config.whisper_model = "".into(); change_kind.replace(ConfigChangeKind::Other); // re-init the whole tab if whisper_any_models_downloaded().unwrap_or_default() { self.show_whisper_model_dialog_box_cleanup()?; } else { self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); } } } Ok(()) } } impl State { pub fn mount(par: SettingsMountParams) -> anyhow::Result { let tasks = Tasks::::new(); let popup_download = PopupHolder::::default(); let popup_dialog = PopupHolder::::default(); let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?; if par.feats.whisper { whisper_models_dropdown(par.mp, c)?; } options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?; options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?; options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?; if par.feats.xr_backend.is_open_vr() || par.feats.monado { // monado or openvr options_checkbox(par.mp, c, SettingType::BlockGameInput)?; options_checkbox(par.mp, c, SettingType::BlockGameInputIgnoreWatch)?; } if par.feats.monado { // monado-only options_checkbox(par.mp, c, SettingType::BlockPosesOnKbdInteraction)?; } options_range_f32( par.mp, c, SettingType::WatchViewAngleMin, SettingType::WatchViewAngleMax, 0.1, 1.0, 0.1, )?; Ok(State { tasks, parent_tasks: par.mp.tasks.clone(), popup_download, popup_dialog, frontend_tasks: par.frontend_tasks.clone(), globals: par.mp.doc_params.globals.clone(), pending_download: None, }) } fn show_whisper_download_dialog(&mut self, model: &WhisperModel, executor: AsyncExecutor) { views::download_file::mount_popup( self.popup_download.clone(), self.frontend_tasks.clone(), self.tasks.make_callback_box(Task::WhisperDownloadClosed), views::download_file::Params { globals: self.globals.clone(), executor, target_path: whisper_model_path(model.file_name), url: model.url.into(), on_downloaded: self.tasks.make_callback_box(Task::WhisperDownloadDone), }, ); } fn show_whisper_model_dialog_box_download(&mut self, model: &'static WhisperModel) -> anyhow::Result<()> { const ACTION_DOWNLOAD: &str = "download"; const ACTION_CANCEL: &str = "cancel"; self.pending_download = Some(model); let tasks = self.tasks.clone(); views::dialog_box::mount_popup( self.popup_dialog.clone(), self.frontend_tasks.clone(), views::dialog_box::Params { globals: self.globals.clone(), message: Translation::from_translation_key("APP_SETTINGS.WHISPER.NEED_TO_DOWNLOAD_MODEL"), entries: vec![ views::dialog_box::ButtonEntry { content: Translation::from_translation_key("APP_SETTINGS.CANCEL"), icon: "dashboard/close.svg", action: ACTION_CANCEL, }, views::dialog_box::ButtonEntry { content: Translation::from_translation_key("DOWNLOAD"), icon: "dashboard/download.svg", action: ACTION_DOWNLOAD, }, ], on_action_click: Box::new(move |action| match action { ACTION_DOWNLOAD => { tasks.push(Task::WhisperDownload(model)); } ACTION_CANCEL => { tasks.push(Task::CloseDialog); // treat as failed download tasks.push(Task::WhisperDownloadClosed); } _ => unreachable!(), }), }, ); Ok(()) } fn show_whisper_model_dialog_box_cleanup(&mut self) -> anyhow::Result<()> { const ACTION_REMOVE: &str = "remove"; const ACTION_CANCEL: &str = "cancel"; let tasks = self.tasks.clone(); views::dialog_box::mount_popup( self.popup_dialog.clone(), self.frontend_tasks.clone(), views::dialog_box::Params { globals: self.globals.clone(), message: Translation::from_translation_key("APP_SETTINGS.WHISPER.REMOVE_UNUSED_MODELS"), entries: vec![ views::dialog_box::ButtonEntry { content: Translation::from_translation_key("APP_SETTINGS.CANCEL"), icon: "dashboard/close.svg", action: ACTION_CANCEL, }, views::dialog_box::ButtonEntry { content: Translation::from_translation_key("REMOVE"), icon: "dashboard/trash.svg", action: ACTION_REMOVE, }, ], on_action_click: Box::new(move |action| match action { ACTION_REMOVE => { tasks.push(Task::WhisperRemoveUnused); tasks.push(Task::CloseDialog); tasks.push(Task::ReloadTab); } ACTION_CANCEL => { tasks.push(Task::CloseDialog); tasks.push(Task::ReloadTab); } _ => unreachable!(), }), }, ); Ok(()) } } fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { let id = mp.idx.to_string(); mp.idx += 1; let parent = horiz_cell(mp.layout, parent)?; let mut downloaded_models = vec![]; for (idx, m) in WHISPER_MODELS.iter().enumerate() { if whisper_model_path(m.file_name).exists() { downloaded_models.push(idx); } } let current_file = mp.config.whisper_model.as_ref(); let current_translation = whisper_model_from_name(current_file) .map_or(Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"), |x| { Translation::from_raw_text(x.display_name) }); let mut params = TemplateParams::new(); params.insert("id", &id); params.insert("translation", "APP_SETTINGS.WHISPER_MODEL"); params.insert("tooltip", "APP_SETTINGS.WHISPER_MODEL_HELP"); mp.parser_state .instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?; { let mut label = mp .parser_state .fetch_widget_as::(&mp.layout.state, &format!("{id}_value"))?; label.set_text_simple(&mut mp.layout.state.globals.get(), current_translation); } let btn = mp.parser_state.fetch_component_as::(&id)?; btn.on_click(Rc::new({ let parent_tasks = mp.tasks.clone(); move |_common, e: ButtonClickEvent| { let mut cells = WHISPER_MODELS .iter() .enumerate() .map(|(idx, item)| context_menu::Cell { action_name: Some(format!(";{id};{}", item.file_name).into()), title: Translation::from_raw_text_string(format!( "{}{}", item.display_name, if downloaded_models.contains(&idx) { " ✅" } else { "" } )), tooltip: None, attribs: vec![], }) .collect::>(); cells.insert( 0, context_menu::Cell { action_name: Some(format!(";{id};").into()), title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"), tooltip: None, attribs: vec![], }, ); parent_tasks.push(ParentTask::OpenContextMenu( e.mouse_pos_absolute.unwrap_or_default(), cells, )); Ok(()) } })); Ok(()) }