use adw::prelude::*;
use ashpd::desktop::background::{BackgroundProxy, BackgroundRequestOptions};
use async_lock::Semaphore;
use chrono::Local;
use gettextrs::{gettext, pgettext};
use log::{debug, error, info, trace, warn};
#[cfg(all(target_os = "linux", feature = "mpris"))]
use mpris_server::PlaybackStatus;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde_json::json;
use std::cell::RefCell;
use std::error::Error;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use crate::core::http_task::http_task;
use crate::core::logging::Logging;
use crate::core::microphone_thread::microphone_thread;
use crate::core::processing_thread::processing_thread;
use crate::core::thread_messages::{GUIMessage::*, *};
use crate::gui::song_history_interface::FavoritesInterface;
use crate::gui::song_history_interface::{RecognitionHistoryInterface, SongRecordInterface};
#[cfg(target_os = "linux")]
use crate::plugins::ksni::SystrayInterface;
#[cfg(all(target_os = "linux", feature = "mpris"))]
use crate::plugins::mpris_player::{get_player, update_song};
use crate::utils::csv_song_history::SongHistoryRecord;
use crate::utils::filesystem_operations::{
clear_cache, obtain_favorites_csv_path, obtain_recognition_history_csv_path,
};
use crate::core::preferences::{Preferences, PreferencesInterface};
use crate::gui::context_menu::ContextMenuUtil;
use crate::gui::history_entry::HistoryEntry;
use crate::gui::listed_device::ListedDevice;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
pub fn gui_main(
log_object: Logging,
recording: bool,
input_file: Option<String>,
enable_mpris_cli: bool,
enable_pipewire_cli: bool,
) -> Result<(), Box<dyn Error>> {
let app = App::new(log_object);
app.run(recording, enable_mpris_cli, enable_pipewire_cli, input_file);
Ok(())
}
struct App {
builder: gtk::Builder,
preferences_interface: Arc<Mutex<PreferencesInterface>>,
song_history_interface: Rc<RefCell<RecognitionHistoryInterface>>,
favorites_interface: Rc<RefCell<FavoritesInterface>>,
old_preferences: Preferences,
ctx_selected_item: Rc<RefCell<Option<HistoryEntry>>>,
ctx_buffered_log: Rc<RefCell<String>>,
#[cfg(target_os = "linux")]
background_mutex: Rc<Semaphore>,
#[cfg(target_os = "linux")]
ctx_background_handle: Rc<RefCell<Option<BackgroundProxy>>>,
#[cfg(target_os = "linux")]
ctx_systray_handle: Rc<RefCell<Option<ksni::Handle<SystrayInterface>>>>,
ctx_logger_source_id: Rc<RefCell<Option<glib::source::SourceId>>>,
gui_tx: async_channel::Sender<GUIMessage>,
gui_rx: async_channel::Receiver<GUIMessage>,
microphone_tx: async_channel::Sender<MicrophoneMessage>,
microphone_rx: async_channel::Receiver<MicrophoneMessage>,
processing_tx: async_channel::Sender<ProcessingMessage>,
processing_rx: async_channel::Receiver<ProcessingMessage>,
http_tx: async_channel::Sender<HTTPMessage>,
http_rx: async_channel::Receiver<HTTPMessage>,
}
impl App {
fn new(log_object: Logging) -> App {
let (gui_tx, gui_rx) = async_channel::unbounded();
let (microphone_tx, microphone_rx) = async_channel::unbounded();
let (processing_tx, processing_rx) = async_channel::unbounded();
let (http_tx, http_rx) = async_channel::unbounded();
log_object.connect_to_gui_logger(gui_tx.clone());
Self::load_resources();
let ctx_selected_item: Rc<RefCell<Option<HistoryEntry>>> = Rc::new(RefCell::new(None));
let ctx_buffered_log: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
let ctx_logger_source_id: Rc<RefCell<Option<glib::source::SourceId>>> =
Rc::new(RefCell::new(None));
let history_list_store: gio::ListStore = gio::ListStore::new::<HistoryEntry>();
let song_history_interface = Rc::new(RefCell::new(
RecognitionHistoryInterface::new(
history_list_store.clone(),
obtain_recognition_history_csv_path,
)
.unwrap(),
));
let favorites_list_store = gio::ListStore::new::<HistoryEntry>();
let favorites_interface = Rc::new(RefCell::new(
FavoritesInterface::new(favorites_list_store.clone(), obtain_favorites_csv_path)
.unwrap(),
));
let builder = gtk::Builder::new();
let builder_scope = gtk::BuilderRustScope::new();
builder.set_scope(Some(&builder_scope));
let preferences_interface: PreferencesInterface = PreferencesInterface::new();
let old_preferences: Preferences = preferences_interface.preferences.clone();
let preferences_interface = Arc::new(Mutex::new(preferences_interface));
Self::setup_callbacks(
microphone_tx.clone(),
gui_tx.clone(),
builder.clone(),
builder_scope,
favorites_interface.clone(),
preferences_interface.clone(),
ctx_selected_item.clone(),
);
builder
.add_from_resource("/re/fossplant/songrec/interface-autogenerated.ui")
.unwrap();
let history_selection: gtk::SingleSelection = builder.object("history_selection").unwrap();
history_selection.set_model(Some(&history_list_store));
let favorites_selection: gtk::SingleSelection =
builder.object("favorites_selection").unwrap();
favorites_selection.set_model(Some(&favorites_list_store));
let request_interval_value: gtk::Adjustment = builder.object("interval_value").unwrap();
request_interval_value.set_value(old_preferences.request_interval_secs_v3.unwrap() as f64);
App {
builder,
song_history_interface,
favorites_interface,
preferences_interface,
old_preferences,
#[cfg(target_os = "linux")]
background_mutex: Rc::new(Semaphore::new(1)),
#[cfg(target_os = "linux")]
ctx_background_handle: Rc::new(RefCell::new(None)),
#[cfg(target_os = "linux")]
ctx_systray_handle: Rc::new(RefCell::new(None)),
ctx_selected_item,
ctx_buffered_log,
ctx_logger_source_id,
gui_tx,
gui_rx,
microphone_tx,
microphone_rx,
processing_tx,
processing_rx,
http_tx,
http_rx,
}
}
fn load_resources() {
gio::resources_register_include!("compiled.gresource")
.expect("Failed to register resources.");
gtk::init().unwrap();
if let Some(display) = gdk::Display::default() {
let icon_theme = gtk::IconTheme::for_display(&display);
icon_theme.add_resource_path("/re/fossplant/songrec/");
}
let css_theme = gtk::CssProvider::new();
css_theme.load_from_resource("/re/fossplant/songrec/style.css");
}
fn run(
self,
set_recording: bool,
enable_mpris_cli: bool,
enable_pipewire_cli: bool,
input_file: Option<String>,
) {
let application = adw::Application::new(
glib::prgname().as_deref(), gio::ApplicationFlags::HANDLES_OPEN,
);
let processing_tx = self.processing_tx.clone();
application.connect_open(move |_application, files, _hint| {
if !files.is_empty() {
if let Some(file_path) = files[0].path() {
let file_path_string = file_path.into_os_string().into_string().unwrap();
processing_tx
.try_send(ProcessingMessage::ProcessAudioFile(file_path_string))
.unwrap();
}
}
});
let start_minimized = self.old_preferences.start_minimized == Some(true);
let initial_activation: Rc<RefCell<bool>> = Rc::new(RefCell::new(true));
application.connect_activate(move |application| {
let main_window = &application.windows()[0];
let mut initial_activation = initial_activation.borrow_mut();
if !*initial_activation || !start_minimized {
main_window.present();
}
*initial_activation = false;
});
application.connect_startup(move |application| {
self.on_startup(
application,
set_recording,
enable_mpris_cli,
enable_pipewire_cli,
);
});
if let Some(input_file_string) = input_file {
application.run_with_args(&["songrec".to_string(), input_file_string]);
} else {
application.run_with_args(&["songrec".to_string()]);
}
}
fn notify_application_error(
preferences_interface: Arc<Mutex<PreferencesInterface>>,
label: &str,
application: &adw::Application,
) {
if preferences_interface
.lock()
.unwrap()
.preferences
.enable_notifications
== Some(true)
{
let notification = gio::Notification::new(&gettext("Application error"));
notification.set_body(Some(label));
notification.set_category(Some("network.error"));
application.send_notification(Some("application-error"), ¬ification);
}
}
fn notify_network_error(
preferences_interface: Arc<Mutex<PreferencesInterface>>,
label: &str,
application: &adw::Application,
always: bool,
) {
if always
|| preferences_interface
.lock()
.unwrap()
.preferences
.enable_notifications
== Some(true)
{
let notification = gio::Notification::new(&gettext("Network error"));
notification.set_body(Some(label));
notification.set_category(Some("network.error"));
application.send_notification(Some("network-error"), ¬ification);
}
}
fn update_website_search_text(&self) {
let search_row: adw::ActionRow = self.builder.object("search_youtube_row").unwrap();
let search_name_row: adw::EntryRow = self.builder.object("search_engine_name").unwrap();
let search_url_row: adw::EntryRow = self.builder.object("search_engine_url").unwrap();
let preferences = self
.preferences_interface
.lock()
.unwrap()
.preferences
.clone();
let label = preferences
.website_search_text
.unwrap_or(gettext("Search on YouTube"));
let url = preferences
.website_search_url
.unwrap_or("https://www.youtube.com/results?search_query=".to_string());
search_name_row.set_text(&label);
search_url_row.set_text(&url);
search_row.set_title(&label);
}
fn on_startup(
&self,
application: &adw::Application,
set_recording: bool,
enable_mpris_cli: bool,
enable_pipewire_cli: bool,
) {
clear_cache();
self.setup_intercom(
application,
set_recording,
enable_mpris_cli,
enable_pipewire_cli,
);
self.setup_actions(application, enable_mpris_cli);
self.setup_context_menus();
self.update_website_search_text();
let window: adw::ApplicationWindow = self.builder.object("main_window").unwrap();
window.set_application(Some(application));
#[cfg(target_os = "linux")]
{
let background_enabled = self.old_preferences.enable_background == Some(true);
let systray_enabled = self.old_preferences.enable_systray == Some(true);
let autostart_enabled = self.old_preferences.enable_autostart == Some(true);
let start_minimized = self.old_preferences.start_minimized == Some(true);
let background_setting: adw::SwitchRow =
self.builder.object("background_setting").unwrap();
let systray_setting: adw::SwitchRow = self.builder.object("systray_setting").unwrap();
let autostart_setting: adw::SwitchRow =
self.builder.object("autostart_setting").unwrap();
let start_minimized_setting: adw::SwitchRow =
self.builder.object("start_minimized_setting").unwrap();
Self::setup_background(
self.background_mutex.clone(),
true,
false,
start_minimized,
background_enabled,
systray_enabled,
autostart_enabled,
self.ctx_systray_handle.clone(),
self.ctx_background_handle.clone(),
window.clone(),
background_setting,
systray_setting,
autostart_setting,
start_minimized_setting,
self.gui_tx.clone(),
);
}
}
#[cfg(target_os = "linux")]
fn setup_background(
background_mutex: Rc<Semaphore>,
is_starting_app: bool,
update_autostart: bool,
start_minimized: bool,
background_enabled: bool,
systray_enabled: bool,
autostart_enabled: bool,
ctx_systray_handle: Rc<RefCell<Option<ksni::Handle<SystrayInterface>>>>,
ctx_background_handle: Rc<RefCell<Option<BackgroundProxy>>>,
window: adw::ApplicationWindow,
background_setting: adw::SwitchRow,
systray_setting: adw::SwitchRow,
autostart_setting: adw::SwitchRow,
start_minimized_setting: adw::SwitchRow,
gui_tx: async_channel::Sender<GUIMessage>,
) {
glib::spawn_future_local(async move {
let background_msg = pgettext(
"Status of the app displayed in the Background Apps section of GNOME system menu when using Flatpak",
"Recognizing songs",
);
let semaphore = background_mutex.acquire().await;
if ctx_background_handle.borrow().is_none() {
match BackgroundProxy::new().await {
Ok(background_proxy) => {
debug!(
"Background portal is reachable: version {}",
background_proxy.version()
);
let options = ashpd::desktop::background::SetStatusOptions::default()
.set_message(&background_msg);
if let Err(error) = background_proxy.set_status(options).await {
warn!("Error setting background status: {:?}", error);
} else {
background_setting.set_visible(true);
*ctx_background_handle.borrow_mut() = Some(background_proxy);
}
}
Err(error) => {
warn!("Background portal is not reachable: {:?}", error);
}
};
}
if let Some(ref background_proxy) = *ctx_background_handle.borrow() {
systray_setting.set_visible(background_enabled);
autostart_setting.set_visible(background_enabled);
if background_enabled || update_autostart {
let window_id =
ashpd::WindowIdentifier::from_native(&window.native().unwrap()).await;
info!("Window ID: {:?}", window_id);
match background_proxy
.request_background(
window_id.as_ref(),
BackgroundRequestOptions::default()
.set_reason(&*background_msg)
.set_auto_start(Some(autostart_enabled)),
)
.await
{
Ok(req) => match req.response() {
Ok(resp) => {
if resp.run_in_background() {
info!("Background request succeeded: {:?}", resp);
} else {
error!("Background request failed: {:?}", resp);
}
}
Err(error) => {
error!("Background request failed: {:?}", error);
}
},
Err(error) => {
error!("Background request failed: {:?}", error);
}
}
}
}
if systray_enabled && ctx_systray_handle.borrow().is_none() {
match SystrayInterface::try_enable(gui_tx).await {
Ok(handle) => {
*ctx_systray_handle.borrow_mut() = Some(handle);
}
Err(err) => {
error!(
"{}: {:?}",
gettext("Unable to enable notification icon"),
err
);
}
}
} else if !systray_enabled {
if let Some(handle) = ctx_systray_handle.take() {
*ctx_systray_handle.borrow_mut() = None;
SystrayInterface::disable(&handle).await;
}
}
let can_minimize = (systray_enabled && ctx_systray_handle.borrow().is_some())
|| (background_enabled && ctx_background_handle.borrow().is_some());
window.set_hide_on_close(can_minimize);
start_minimized_setting.set_visible(can_minimize);
if is_starting_app
&& background_setting.is_visible()
&& !background_enabled
&& systray_enabled
&& can_minimize
{
background_setting.set_active(true);
}
if is_starting_app && start_minimized && can_minimize {
window.close();
} else if is_starting_app {
window.present();
}
drop(semaphore);
});
}
fn setup_context_menus(&self) {
ContextMenuUtil::connect_menu_key_actions(
self.builder.clone(),
self.builder.object("history_view").unwrap(),
self.builder.object("history_context_menu").unwrap(),
self.ctx_selected_item.clone(),
self.favorites_interface.clone(),
);
ContextMenuUtil::connect_menu_key_actions(
self.builder.clone(),
self.builder.object("favorites_view").unwrap(),
self.builder.object("history_context_menu").unwrap(),
self.ctx_selected_item.clone(),
self.favorites_interface.clone(),
);
ContextMenuUtil::bind_actions(
self.builder.object("main_window").unwrap(),
self.builder.object("history_context_menu").unwrap(),
self.ctx_selected_item.clone(),
self.song_history_interface.clone(),
self.favorites_interface.clone(),
self.preferences_interface.clone(),
);
}
fn setup_callbacks(
microphone_tx_shared: async_channel::Sender<MicrophoneMessage>,
gui_tx_shared: async_channel::Sender<GUIMessage>,
builder_shared: gtk::Builder,
builder_scope: gtk::BuilderRustScope,
favorites: Rc<RefCell<FavoritesInterface>>,
preferences_shared: Arc<Mutex<PreferencesInterface>>,
ctx_selected_item: Rc<RefCell<Option<HistoryEntry>>>,
) {
let gui_tx = gui_tx_shared.clone();
let builder = builder_shared.clone();
let preferences = preferences_shared.clone();
builder_scope.add_callback("search_engine_action_changed", move |values| {
let entry_row = values[0].get::<adw::EntryRow>().unwrap();
let search_row: adw::ActionRow = builder.object("search_youtube_row").unwrap();
search_row.set_title(&entry_row.text());
let lock = preferences.lock().unwrap();
if lock.preferences.website_search_text != Some(entry_row.text().to_string()) {
let mut new_preference = Preferences::new();
new_preference.website_search_text = Some(entry_row.text().to_string());
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
}
None
});
let gui_tx = gui_tx_shared.clone();
let preferences = preferences_shared.clone();
builder_scope.add_callback("search_engine_url_changed", move |values| {
let entry_row = values[0].get::<adw::EntryRow>().unwrap();
let lock = preferences.lock().unwrap();
if lock.preferences.website_search_url != Some(entry_row.text().to_string()) {
let mut new_preference = Preferences::new();
new_preference.website_search_url = Some(entry_row.text().to_string());
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
}
None
});
let microphone_tx = microphone_tx_shared.clone();
let builder = builder_shared.clone();
builder_scope.add_callback("history_cell_setup_cb", move |values| {
let popover_menu: gtk::PopoverMenu = builder.object("history_context_menu").unwrap();
let builder = builder.clone();
let favorites = favorites.clone();
let ctx_selected_item = ctx_selected_item.clone();
let cell = values[1].get::<gtk::ColumnViewCell>().unwrap();
let label = gtk::Label::new(None);
label.set_xalign(0.0);
label.add_css_class("cell_label");
cell.set_child(Some(&label));
ContextMenuUtil::connect_menu_mouse_actions(
builder,
cell,
label,
popover_menu,
ctx_selected_item,
favorites,
);
None
});
builder_scope.add_callback("history_cell_bind_cb", move |values| {
let col = values[0].get::<gtk::ColumnViewColumn>().unwrap();
let cell = values[1].get::<gtk::ColumnViewCell>().unwrap();
let label = cell.child().unwrap().downcast::<gtk::Label>().unwrap();
let entry = cell.item().unwrap().downcast::<HistoryEntry>().unwrap();
let prop_name = col.id().unwrap();
let text = match prop_name.as_str() {
"song_name" => entry.song_name(),
"album" => entry.album().unwrap_or_default(),
"recognition_date" => entry.recognition_date(),
_ => unreachable!(),
};
label.set_text(&text);
None
});
let builder = builder_shared.clone();
builder_scope.add_callback("loopback_options_switched", move |_values| {
let loopback_switch: adw::SwitchRow = builder.object("loopback_switch").unwrap();
let microphone_switch: adw::SwitchRow = builder.object("microphone_switch").unwrap();
let device_section: adw::PreferencesGroup =
builder.object("input_device_section").unwrap();
let g_list_store: gio::ListStore = builder.object("audio_inputs_model").unwrap();
let volume_row: adw::PreferencesRow = builder.object("volume_row").unwrap();
let volume_gauge: gtk::ProgressBar = builder.object("volume_gauge").unwrap();
if loopback_switch.is_active() {
microphone_switch.set_active(false);
device_section.set_visible(true);
volume_row.set_visible(true);
let adw_combo_row: adw::ComboRow = builder.object("audio_inputs").unwrap();
if let Some(current_device) = adw_combo_row.selected_item() {
let current_device = current_device.downcast::<ListedDevice>().unwrap();
if !current_device.is_monitor() {
for position in 0..g_list_store.n_items() {
let other_device = g_list_store
.item(position)
.unwrap()
.downcast::<ListedDevice>()
.unwrap();
if other_device.is_monitor() {
adw_combo_row.set_selected(position);
break;
}
}
} else {
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStop)
.unwrap();
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStart(
current_device.inner_name().to_owned(),
))
.unwrap();
}
}
} else if !microphone_switch.is_active() && !loopback_switch.is_active() {
device_section.set_visible(false);
volume_row.set_visible(false);
volume_gauge.set_fraction(0.0);
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStop)
.unwrap();
}
None
});
let microphone_tx = microphone_tx_shared.clone();
let builder = builder_shared.clone();
builder_scope.add_callback("microphone_option_switched", move |_values| {
let microphone_switch: adw::SwitchRow = builder.object("microphone_switch").unwrap();
let loopback_switch: adw::SwitchRow = builder.object("loopback_switch").unwrap();
let device_section: adw::PreferencesGroup =
builder.object("input_device_section").unwrap();
let g_list_store: gio::ListStore = builder.object("audio_inputs_model").unwrap();
let volume_row: adw::PreferencesRow = builder.object("volume_row").unwrap();
let volume_gauge: gtk::ProgressBar = builder.object("volume_gauge").unwrap();
if microphone_switch.is_active() {
loopback_switch.set_active(false);
device_section.set_visible(true);
volume_row.set_visible(true);
let adw_combo_row: adw::ComboRow = builder.object("audio_inputs").unwrap();
if let Some(current_device) = adw_combo_row.selected_item() {
let current_device = current_device.downcast::<ListedDevice>().unwrap();
if current_device.is_monitor() {
for position in 0..g_list_store.n_items() {
let other_device = g_list_store
.item(position)
.unwrap()
.downcast::<ListedDevice>()
.unwrap();
if !other_device.is_monitor() {
adw_combo_row.set_selected(position);
break;
}
}
} else {
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStop)
.unwrap();
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStart(
current_device.inner_name().to_owned(),
))
.unwrap();
}
}
} else if !microphone_switch.is_active() && !loopback_switch.is_active() {
device_section.set_visible(false);
volume_row.set_visible(false);
volume_gauge.set_fraction(0.0);
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStop)
.unwrap();
}
None
});
let microphone_tx = microphone_tx_shared.clone();
let gui_tx = gui_tx_shared.clone();
let builder = builder_shared.clone();
builder_scope.add_callback("input_device_switched", move |values| {
let microphone_switch: adw::SwitchRow = builder.object("microphone_switch").unwrap();
let loopback_switch: adw::SwitchRow = builder.object("loopback_switch").unwrap();
let volume_gauge: gtk::ProgressBar = builder.object("volume_gauge").unwrap();
let combo_row = values[0].get::<adw::ComboRow>().unwrap();
if let Some(device) = combo_row.selected_item() {
let device = device.downcast::<ListedDevice>().unwrap();
let device_name = device.inner_name();
let is_monitor = device.is_monitor();
if microphone_switch.is_active() && is_monitor {
microphone_switch.set_active(false);
loopback_switch.set_active(true);
} else if loopback_switch.is_active() && !is_monitor {
loopback_switch.set_active(false);
microphone_switch.set_active(true);
}
let mut new_preference = Preferences::new();
new_preference.current_device_name = Some(device_name.to_string());
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
if microphone_switch.is_active() || loopback_switch.is_active() {
volume_gauge.set_fraction(0.0);
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStop)
.unwrap();
microphone_tx
.try_send(MicrophoneMessage::MicrophoneRecordStart(
device_name.to_owned(),
))
.unwrap();
}
}
None
});
let gui_tx = gui_tx_shared.clone();
builder_scope.add_callback("interval_changed", move |values| {
let adjustment = values[0].get::<gtk::Adjustment>().unwrap();
debug!("Request interval set to: {}", adjustment.value());
let mut new_preference = Preferences::new();
new_preference.request_interval_secs_v3 = Some(adjustment.value() as u64);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
None
});
let builder = builder_shared;
builder_scope.add_callback("about_dialog_closed", move |_values| {
let about_dialog: adw::AboutDialog = builder.object("about_dialog").unwrap();
about_dialog.set_visible(false);
None
});
}
fn setup_intercom(
&self,
application: &adw::Application,
set_recording: bool,
_enable_mpris_cli: bool,
enable_pipewire_cli: bool,
) {
let microphone_rx = self.microphone_rx.clone();
let microphone_tx = self.microphone_tx.clone();
let processing_tx = self.processing_tx.clone();
let gui_tx = self.gui_tx.clone();
let preferences_interface = self.preferences_interface.clone();
spawn_big_thread(move || {
microphone_thread(
microphone_rx,
microphone_tx,
processing_tx,
gui_tx,
preferences_interface,
enable_pipewire_cli,
);
});
let processing_rx = self.processing_rx.clone();
let http_tx = self.http_tx.clone();
let gui_tx = self.gui_tx.clone();
spawn_big_thread(move || {
processing_thread(processing_rx, http_tx, gui_tx);
});
let http_rx = self.http_rx.clone();
let gui_tx = self.gui_tx.clone();
let microphone_tx = self.microphone_tx.clone();
glib::spawn_future_local(http_task(http_rx, gui_tx, microphone_tx));
let gui_rx = self.gui_rx.clone();
let preferences_interface_ptr = self.preferences_interface.clone();
let old_device_name = self.old_preferences.current_device_name.clone();
#[cfg(target_os = "linux")]
let background_settings: adw::PreferencesGroup =
self.builder.object("background_settings").unwrap();
#[cfg(all(target_os = "linux", feature = "mpris"))]
let mpris_setting: adw::SwitchRow = self.builder.object("mpris_setting").unwrap();
let window: adw::ApplicationWindow = self.builder.object("main_window").unwrap();
let adw_combo_row: adw::ComboRow = self.builder.object("audio_inputs").unwrap();
let g_list_store: gio::ListStore = self.builder.object("audio_inputs_model").unwrap();
let microphone_switch: adw::SwitchRow = self.builder.object("microphone_switch").unwrap();
let recognize_file_row: adw::PreferencesRow =
self.builder.object("recognize_file_row").unwrap();
let spinner_row: adw::PreferencesRow = self.builder.object("spinner_row").unwrap();
let volume_row: adw::PreferencesRow = self.builder.object("volume_row").unwrap();
let volume_gauge: gtk::ProgressBar = self.builder.object("volume_gauge").unwrap();
let results_section: adw::PreferencesGroup =
self.builder.object("results_section").unwrap();
let no_network_message: gtk::Label = self.builder.object("no_network_message").unwrap();
let rate_limited_message: gtk::Label = self.builder.object("rate_limited_message").unwrap();
let results_image: gtk::Image = self.builder.object("results_image").unwrap();
let results_label: gtk::Label = self.builder.object("results_label").unwrap();
let loopback_switch: adw::SwitchRow = self.builder.object("loopback_switch").unwrap();
#[cfg(target_os = "linux")]
background_settings.set_visible(true);
#[cfg(all(target_os = "linux", feature = "mpris"))]
mpris_setting.set_visible(true);
microphone_switch.set_active(set_recording);
let song_history_interface = self.song_history_interface.clone();
let _old_preferences = self.old_preferences.clone();
let ctx_buffered_log = self.ctx_buffered_log.clone();
let application = application.clone();
glib::spawn_future_local(async move {
#[cfg(all(target_os = "linux", feature = "mpris"))]
let mut mpris_obj = {
let player = if _enable_mpris_cli && _old_preferences.enable_mpris_v2 != Some(false)
{
let player_maybe = get_player(true).await;
if let Some(ref player) = player_maybe {
let application = application.clone();
let window = window.clone();
player.connect_quit(move |_player| {
application.quit();
});
player.connect_raise(move |_player| {
window.present();
});
}
player_maybe
} else {
None
};
if _enable_mpris_cli
&& _old_preferences.enable_mpris_v2 != Some(false)
&& player.is_none()
{
println!("{}", gettext("Unable to enable MPRIS support"))
}
player
};
#[cfg(all(target_os = "linux", feature = "mpris"))]
let mut last_cover_path = None;
while let Ok(gui_message) = gui_rx.recv().await {
if let AppendToLog(log_string) = gui_message {
const MAX_LOG_SIZE: usize = 2 * 1024 * 1024;
{
let buffer_ptr: &mut String = &mut ctx_buffered_log.borrow_mut();
buffer_ptr.push_str(&log_string);
if buffer_ptr.len() > MAX_LOG_SIZE {
let to_cut: String = buffer_ptr
.chars()
.take(buffer_ptr.len() - MAX_LOG_SIZE)
.collect();
buffer_ptr.drain(..to_cut.len());
}
}
} else {
if let MicrophoneVolumePercent(_) = gui_message {
trace!("Received GUI message: {:?}", gui_message);
} else if let SongRecognized(ref msg) = gui_message {
debug!(
"Received GUI message: SongRecognized({})",
json!({
"artist_name": msg.artist_name,
"album_name": msg.album_name,
"song_name": msg.song_name,
"cover_image": msg.cover_image.as_ref().map(|data| format!("{:02x?}...", &data[..16])),
"track_key": msg.track_key,
"release_year": msg.release_year,
"genre": msg.genre,
"shazam_json": msg.shazam_json,
})
);
} else {
debug!("Received GUI message: {:?}", gui_message);
}
match gui_message {
ErrorMessage(_) | NetworkStatus(_) | SongRecognized(_) => {
recognize_file_row.set_sensitive(true);
spinner_row.set_visible(false);
}
_ => {}
}
match gui_message {
UpdatePreference(new_preference) => {
preferences_interface_ptr
.lock()
.unwrap()
.update(new_preference);
#[cfg(all(target_os = "linux", feature = "mpris"))]
if _enable_mpris_cli {
let mpris_enabled = preferences_interface_ptr
.lock()
.unwrap()
.preferences
.enable_mpris_v2
!= Some(false);
if mpris_enabled && mpris_obj.is_none() {
mpris_obj = {
let player_maybe = get_player(true).await;
if let Some(ref player) = player_maybe {
let application = application.clone();
let window = window.clone();
player.connect_quit(move |_player| {
application.quit();
});
player.connect_raise(move |_player| {
window.present();
});
} else {
println!(
"{}",
gettext("Unable to enable MPRIS support")
)
}
player_maybe
};
} else if let Some(ref player) = mpris_obj
&& mpris_enabled != player.can_play()
{
player.set_can_play(mpris_enabled).await.ok();
}
}
}
ErrorMessage(string) => {
if !(string == gettext("No match for this song")
&& (microphone_switch.is_active() || loopback_switch.is_active()))
{
error!("Displaying error: {}", string);
let dialog = adw::AlertDialog::builder()
.body(&string)
.close_response("ok")
.default_response("ok")
.build();
dialog.add_responses(&[("ok", &gettext("_Ok"))]);
glib::spawn_future_local(dialog.choose_future(Some(&window)));
if string != gettext("No match for this song") {
Self::notify_application_error(
preferences_interface_ptr.clone(),
&string,
&application,
);
}
}
}
RateLimitState(is_rate_limited) => {
if is_rate_limited && !rate_limited_message.is_visible() {
Self::notify_network_error(
preferences_interface_ptr.clone(),
&rate_limited_message.label(),
&application,
true,
);
}
rate_limited_message.set_visible(is_rate_limited);
}
NetworkStatus(network_is_reachable) => {
if !network_is_reachable && !no_network_message.is_visible() {
Self::notify_network_error(
preferences_interface_ptr.clone(),
&no_network_message.label(),
&application,
false,
);
}
no_network_message.set_visible(!network_is_reachable);
#[cfg(all(target_os = "linux", feature = "mpris"))]
{
let mpris_enabled = preferences_interface_ptr
.lock()
.unwrap()
.preferences
.enable_mpris_v2
!= Some(false);
if mpris_enabled {
let mpris_status = if network_is_reachable {
PlaybackStatus::Playing
} else {
PlaybackStatus::Paused
};
if let Some(ref player) = mpris_obj
&& let Err(error) =
player.set_playback_status(mpris_status).await
{
error!("Could not set MPRIS playback status: {:?}", error);
}
}
}
}
SongRecognized(message) => {
results_section.set_visible(true);
let song_name =
format!("{} - {}", message.artist_name, message.song_name);
if results_label.text().as_str() != song_name {
results_label.set_label(&song_name);
let notification =
gio::Notification::new(&gettext("Song recognized"));
notification.set_body(Some(&song_name));
if let Some(ref cover_image) = message.cover_image
&& let Ok(texture) =
gdk::Texture::from_bytes(&glib::Bytes::from(cover_image))
{
results_image.set_visible(true);
results_image.set_paintable(Some(&texture));
match message.album_name {
Some(ref value) => {
results_image.set_tooltip_text(Some(value))
}
None => results_image.set_tooltip_text(None),
};
notification.set_icon(&texture);
} else {
results_image.set_visible(false);
}
#[cfg(all(target_os = "linux", feature = "mpris"))]
if preferences_interface_ptr
.lock()
.unwrap()
.preferences
.enable_mpris_v2
!= Some(false)
&& let Some(ref player) = mpris_obj
{
update_song(player, &message, &mut last_cover_path).await;
}
if preferences_interface_ptr
.lock()
.unwrap()
.preferences
.enable_notifications
== Some(true)
{
application
.send_notification(Some("recognized-song"), ¬ification);
}
let new_entry = SongHistoryRecord {
song_name,
album: Some(message.album_name.unwrap_or_default()),
track_key: Some(message.track_key),
release_year: Some(message.release_year.unwrap_or_default()),
genre: Some(message.genre.unwrap_or_default()),
recognition_date: Local::now().format("%c").to_string(),
};
if preferences_interface_ptr
.lock()
.unwrap()
.preferences
.no_duplicates
== Some(true)
{
song_history_interface
.borrow_mut()
.remove(new_entry.clone());
}
song_history_interface
.borrow_mut()
.add_row_and_save(new_entry);
}
}
DevicesList(devices) => {
let mut initial_device_index: u32 = 0;
let mut initial_device: Option<ListedDevice> = None;
let mut found_monitor_device = false;
g_list_store.remove_all();
for (current_index, device) in devices.iter().enumerate() {
let listed_device = ListedDevice::new(
device.display_name.clone(),
device.inner_name.clone(),
device.is_monitor,
);
g_list_store.append(&listed_device);
if (old_device_name == Some(device.inner_name.to_string()))
|| (old_device_name.is_none()
&& device.is_monitor
&& !found_monitor_device)
{
initial_device_index = current_index as u32;
initial_device = Some(listed_device);
} else if current_index == 0 {
initial_device = Some(listed_device);
}
if device.is_monitor {
found_monitor_device = true;
}
}
if let Some(device) = initial_device {
adw_combo_row.set_selected(initial_device_index);
loopback_switch.set_visible(found_monitor_device);
debug!(
"Initially selected audio input device: {:?} / {:?}",
device.inner_name(),
device.display_name()
);
microphone_switch.set_visible(true);
volume_row.set_visible(
microphone_switch.is_active() || loopback_switch.is_active(),
);
}
}
MicrophoneRecording => {
volume_row.set_visible(
microphone_switch.is_active() || loopback_switch.is_active(),
);
volume_gauge.set_fraction(0.0);
}
MicrophoneVolumePercent(percent) => {
let clamped_percent = percent.clamp(0.0, 100.0);
volume_gauge.set_fraction((clamped_percent / 100.0) as f64);
}
WipeSongHistory => {
let dialog = adw::AlertDialog::builder()
.body(gettext("Are you sure you want to wipe history?"))
.default_response("yes")
.close_response("cancel")
.build();
dialog.add_responses(&[
("yes", &gettext("_Yes")),
("cancel", &gettext("_Cancel")),
]);
dialog.set_response_appearance(
"yes",
adw::ResponseAppearance::Destructive,
);
let song_history_interface = song_history_interface.clone();
dialog.choose(
Some(&window),
None::<&gio::Cancellable>,
move |result| {
if result == "yes" {
song_history_interface.borrow_mut().wipe_and_save();
}
},
);
}
ShowWindow => {
window.present();
}
QuitApplication => {
application.quit();
}
_ => {
debug!("(parsing unimplemented yet): {:?}", gui_message);
}
}
}
}
});
}
fn setup_actions(&self, application: &adw::Application, _enable_mpris_cli: bool) {
let window: adw::ApplicationWindow = self.builder.object("main_window").unwrap();
let file_picker: gtk::FileDialog = self.builder.object("file_picker").unwrap();
let about_dialog: adw::AboutDialog = self.builder.object("about_dialog").unwrap();
let results_label: gtk::Label = self.builder.object("results_label").unwrap();
let menu_button: gtk::MenuButton = self.builder.object("menu_button").unwrap();
let navigation_view: adw::NavigationView =
self.builder.object("main_window_pages").unwrap();
let recognize_file_row: adw::PreferencesRow =
self.builder.object("recognize_file_row").unwrap();
let spinner_row: adw::PreferencesRow = self.builder.object("spinner_row").unwrap();
let ctx_buffered_log = self.ctx_buffered_log.clone();
let ctx_logger_source_id = self.ctx_logger_source_id.clone();
let action_show_about = gio::ActionEntry::builder("show-about")
.activate(move |window, _, _| {
about_dialog.set_visible(true);
about_dialog.set_version(env!("CARGO_PKG_VERSION"));
about_dialog.present(Some(window));
about_dialog.set_debug_info(&ctx_buffered_log.borrow());
about_dialog.set_release_notes_version(
include_str!(
"../../packaging/freedesktop/re.fossplant.songrec.metainfo.xml.in"
)
.split("<release version=\"")
.collect::<Vec<_>>()[1]
.split("\"")
.collect::<Vec<_>>()[0],
);
about_dialog.set_release_notes(
include_str!(
"../../packaging/freedesktop/re.fossplant.songrec.metainfo.xml.in"
)
.split("<release ")
.collect::<Vec<_>>()[1]
.split("<description>")
.collect::<Vec<_>>()[1]
.split("</description>")
.collect::<Vec<_>>()[0],
);
let ctx_buffered_log = ctx_buffered_log.clone();
let ctx_logger_source_id_2 = ctx_logger_source_id.clone();
let about_dialog = about_dialog.clone();
if (*ctx_logger_source_id.borrow()).is_none() {
*ctx_logger_source_id.borrow_mut() =
Some(glib::source::timeout_add_seconds_local(1, move || {
if about_dialog.is_visible() {
about_dialog.set_debug_info(&ctx_buffered_log.borrow());
glib::ControlFlow::Continue
} else {
*ctx_logger_source_id_2.borrow_mut() = None;
glib::ControlFlow::Break
}
}));
}
})
.build();
let processing_tx = self.processing_tx.clone();
let action_recognize_file = gio::ActionEntry::builder("recognize-file")
.activate(move |window, _action, _obj| {
let processing_tx = processing_tx.clone();
let window: &adw::ApplicationWindow = window;
let recognize_file_row = recognize_file_row.clone();
let spinner_row = spinner_row.clone();
file_picker.open(
Some(window),
None::<&gio::Cancellable>,
move |file| match file {
Ok(gio_file) => {
info!("Picked file: {:?}", gio_file.path());
let path_str = gio_file.path().unwrap().to_string_lossy().into_owned();
recognize_file_row.set_sensitive(false);
spinner_row.set_visible(true);
processing_tx
.try_send(ProcessingMessage::ProcessAudioFile(path_str))
.unwrap();
}
Err(error) => {
error!("Error picking file: {:?}", error);
}
},
);
})
.build();
let preferences_interface_ptr = self.preferences_interface.clone();
let action_search_youtube = gio::ActionEntry::builder("search-youtube")
.activate(move |window: &adw::ApplicationWindow, _, _| {
let window = window.clone();
let results_label = results_label.text();
let mut encoded_search_term =
utf8_percent_encode(results_label.as_str(), NON_ALPHANUMERIC).to_string();
encoded_search_term = encoded_search_term.replace("%20", "+");
let website_search_url = {
let lock = preferences_interface_ptr.lock().unwrap();
lock.preferences.website_search_url.clone()
};
let mut search_url = website_search_url
.unwrap_or("https://www.youtube.com/results?search_query=".to_string());
search_url = format!("{}{}", search_url, encoded_search_term);
glib::spawn_future_local(async move {
info!("Launching URL: {}", search_url);
if let Err(err) = gtk::UriLauncher::new(&search_url)
.launch_future(Some(&window))
.await
{
error!("Could not launch URL {}: {:?}", search_url, err);
}
});
})
.build();
let action_export_to_csv = gio::ActionEntry::builder("export-to-csv")
.activate(move |_window: &adw::ApplicationWindow, _action, _obj| {
#[cfg(not(windows))]
{
let window = _window.clone();
glib::spawn_future_local(async move {
let launch_path = obtain_recognition_history_csv_path().unwrap();
info!("Launching file: {}", launch_path.display());
let launch_file = gio::File::for_path(&launch_path);
if let Err(err) = gtk::FileLauncher::new(Some(&launch_file))
.launch_future(Some(&window))
.await
{
error!("Could not launch file {}: {:?}", launch_path.display(), err);
}
});
}
#[cfg(windows)]
std::process::Command::new("cmd")
.args(&[
"/c",
&format!(
"start {}",
obtain_recognition_history_csv_path().unwrap().display()
),
])
.creation_flags(0x00000008) .output()
.ok();
})
.build();
let action_export_favorites_to_csv = gio::ActionEntry::builder("export-favorites-to-csv")
.activate(move |_window: &adw::ApplicationWindow, _action, _obj| {
#[cfg(not(windows))]
{
let window = _window.clone();
glib::spawn_future_local(async move {
let launch_path = obtain_favorites_csv_path().unwrap();
info!("Launching file: {}", launch_path.display());
let launch_file = gio::File::for_path(&launch_path);
if let Err(err) = gtk::FileLauncher::new(Some(&launch_file))
.launch_future(Some(&window))
.await
{
error!("Could not launch file {}: {:?}", launch_path.display(), err);
}
});
}
#[cfg(windows)]
std::process::Command::new("cmd")
.args(&[
"/c",
&format!("start {}", obtain_favorites_csv_path().unwrap().display()),
])
.creation_flags(0x00000008) .output()
.ok();
})
.build();
let gui_tx = self.gui_tx.clone();
let action_wipe_history = gio::ActionEntry::builder("wipe-history")
.activate(move |_window, _action, _obj| {
gui_tx.try_send(GUIMessage::WipeSongHistory).unwrap();
})
.build();
#[cfg(all(target_os = "linux", feature = "mpris"))]
let gui_tx = self.gui_tx.clone();
#[cfg(all(target_os = "linux", feature = "mpris"))]
let action_mpris_setting = gio::ActionEntry::builder("mpris-setting")
.state(self.old_preferences.enable_mpris_v2.unwrap().to_variant())
.activate(move |_, action, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let mut new_preference: Preferences = Preferences::new();
new_preference.enable_mpris_v2 = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
})
.build();
let gui_tx = self.gui_tx.clone();
let action_notification_setting = gio::ActionEntry::builder("notification-setting")
.state(
self.old_preferences
.enable_notifications
.unwrap()
.to_variant(),
)
.activate(move |_, action, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let mut new_preference: Preferences = Preferences::new();
new_preference.enable_notifications = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
})
.build();
#[cfg(target_os = "linux")]
let action_background_setting = {
let gui_tx = self.gui_tx.clone();
let background_mutex = self.background_mutex.clone();
let ctx_systray_handle = self.ctx_systray_handle.clone();
let ctx_background_handle = self.ctx_background_handle.clone();
let background_setting: adw::SwitchRow =
self.builder.object("background_setting").unwrap();
let systray_setting: adw::SwitchRow = self.builder.object("systray_setting").unwrap();
let autostart_setting: adw::SwitchRow =
self.builder.object("autostart_setting").unwrap();
let start_minimized_setting: adw::SwitchRow =
self.builder.object("start_minimized_setting").unwrap();
gio::ActionEntry::builder("background-setting")
.state(self.old_preferences.enable_background.unwrap().to_variant())
.activate(
move |window: &adw::ApplicationWindow, action: &gio::SimpleAction, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let background_mutex = background_mutex.clone();
let background_setting = background_setting.clone();
let systray_setting = systray_setting.clone();
let autostart_setting = autostart_setting.clone();
let start_minimized_setting = start_minimized_setting.clone();
let ctx_systray_handle = ctx_systray_handle.clone();
let ctx_background_handle = ctx_background_handle.clone();
let systray_enabled = systray_setting.is_active();
let autostart_enabled = autostart_setting.is_active();
let start_minimized = start_minimized_setting.is_active();
Self::setup_background(
background_mutex,
false,
false,
start_minimized,
new_state,
systray_enabled,
autostart_enabled,
ctx_systray_handle,
ctx_background_handle,
window.clone(),
background_setting,
systray_setting.clone(),
autostart_setting.clone(),
start_minimized_setting,
gui_tx.clone(),
);
let mut new_preference: Preferences = Preferences::new();
new_preference.enable_background = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
if new_state != systray_enabled {
systray_setting.set_active(new_state);
}
if !new_state && autostart_enabled {
autostart_setting.set_active(false);
}
},
)
.build()
};
#[cfg(target_os = "linux")]
let action_systray_setting = {
let gui_tx = self.gui_tx.clone();
let background_mutex = self.background_mutex.clone();
let ctx_systray_handle = self.ctx_systray_handle.clone();
let ctx_background_handle = self.ctx_background_handle.clone();
let background_setting: adw::SwitchRow =
self.builder.object("background_setting").unwrap();
let systray_setting: adw::SwitchRow = self.builder.object("systray_setting").unwrap();
let autostart_setting: adw::SwitchRow =
self.builder.object("autostart_setting").unwrap();
let start_minimized_setting: adw::SwitchRow =
self.builder.object("start_minimized_setting").unwrap();
gio::ActionEntry::builder("systray-setting")
.state(self.old_preferences.enable_systray.unwrap().to_variant())
.activate(
move |window: &adw::ApplicationWindow, action: &gio::SimpleAction, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let background_mutex = background_mutex.clone();
let background_setting = background_setting.clone();
let systray_setting = systray_setting.clone();
let autostart_setting = autostart_setting.clone();
let start_minimized_setting = start_minimized_setting.clone();
let ctx_systray_handle = ctx_systray_handle.clone();
let ctx_background_handle = ctx_background_handle.clone();
let background_enabled = background_setting.is_active();
let autostart_enabled = autostart_setting.is_active();
let start_minimized = start_minimized_setting.is_active();
Self::setup_background(
background_mutex,
false,
false,
start_minimized,
background_enabled,
new_state,
autostart_enabled,
ctx_systray_handle,
ctx_background_handle,
window.clone(),
background_setting,
systray_setting,
autostart_setting,
start_minimized_setting,
gui_tx.clone(),
);
let mut new_preference: Preferences = Preferences::new();
new_preference.enable_systray = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
},
)
.build()
};
#[cfg(target_os = "linux")]
let action_autostart_setting = {
let gui_tx = self.gui_tx.clone();
let background_mutex = self.background_mutex.clone();
let ctx_systray_handle = self.ctx_systray_handle.clone();
let ctx_background_handle = self.ctx_background_handle.clone();
let background_setting: adw::SwitchRow =
self.builder.object("background_setting").unwrap();
let systray_setting: adw::SwitchRow = self.builder.object("systray_setting").unwrap();
let autostart_setting: adw::SwitchRow =
self.builder.object("autostart_setting").unwrap();
let start_minimized_setting: adw::SwitchRow =
self.builder.object("start_minimized_setting").unwrap();
gio::ActionEntry::builder("autostart-setting")
.state(self.old_preferences.enable_autostart.unwrap().to_variant())
.activate(
move |window: &adw::ApplicationWindow, action: &gio::SimpleAction, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let background_mutex = background_mutex.clone();
let background_setting = background_setting.clone();
let systray_setting = systray_setting.clone();
let autostart_setting = autostart_setting.clone();
let start_minimized_setting = start_minimized_setting.clone();
let ctx_systray_handle = ctx_systray_handle.clone();
let ctx_background_handle = ctx_background_handle.clone();
let background_enabled = background_setting.is_active();
let systray_enabled = systray_setting.is_active();
let start_minimized = start_minimized_setting.is_active();
Self::setup_background(
background_mutex,
false,
true,
start_minimized,
background_enabled,
systray_enabled,
new_state,
ctx_systray_handle,
ctx_background_handle,
window.clone(),
background_setting,
systray_setting,
autostart_setting,
start_minimized_setting,
gui_tx.clone(),
);
let mut new_preference: Preferences = Preferences::new();
new_preference.enable_autostart = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
},
)
.build()
};
#[cfg(target_os = "linux")]
let action_start_minimized_setting = {
let gui_tx = self.gui_tx.clone();
gio::ActionEntry::builder("start-minimized-setting")
.state(self.old_preferences.start_minimized.unwrap().to_variant())
.activate(
move |_window: &adw::ApplicationWindow, action: &gio::SimpleAction, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let mut new_preference: Preferences = Preferences::new();
new_preference.start_minimized = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
},
)
.build()
};
let gui_tx = self.gui_tx.clone();
let action_no_dupes_setting = gio::ActionEntry::builder("no-dupes-setting")
.state(self.old_preferences.no_duplicates.unwrap().to_variant())
.activate(move |_, action, _| {
let state = action.state().unwrap();
let action_state: bool = state.get().unwrap();
let new_state = !action_state; action.set_state(&new_state.to_variant());
let mut new_preference: Preferences = Preferences::new();
new_preference.no_duplicates = Some(new_state);
gui_tx
.try_send(GUIMessage::UpdatePreference(new_preference))
.unwrap();
})
.build();
let action_close = gio::ActionEntry::builder("close")
.activate(move |window: &adw::ApplicationWindow, _, _| {
window.close();
})
.build();
let action_show_preferences = gio::ActionEntry::builder("show-preferences")
.activate(move |_, _, _| {
navigation_view.push_by_tag("settings_tag");
})
.build();
let microphone_tx = self.microphone_tx.clone();
let action_refresh_devices = gio::ActionEntry::builder("refresh-devices")
.activate(move |_, _, _| {
microphone_tx
.try_send(MicrophoneMessage::RefreshDevices)
.unwrap();
})
.build();
let action_show_menu = gio::ActionEntry::builder("show-menu")
.activate(move |_, _, _| {
menu_button.activate();
})
.build();
window.add_action_entries([
action_show_about,
action_recognize_file,
action_search_youtube,
action_export_to_csv,
action_export_favorites_to_csv,
action_wipe_history,
action_show_preferences,
action_notification_setting,
#[cfg(target_os = "linux")]
action_background_setting,
#[cfg(target_os = "linux")]
action_systray_setting,
#[cfg(target_os = "linux")]
action_autostart_setting,
#[cfg(target_os = "linux")]
action_start_minimized_setting,
action_no_dupes_setting,
action_refresh_devices,
action_close,
action_show_menu,
]);
#[cfg(all(target_os = "linux", feature = "mpris"))]
if _enable_mpris_cli {
window.add_action_entries([action_mpris_setting]);
}
application.set_accels_for_action("win.close", &["<Primary>Q", "<Primary>W"]);
application.set_accels_for_action("win.recognize-file", &["<Primary>O"]);
application
.set_accels_for_action("win.show-preferences", &["<Primary>comma", "<Primary>P"]);
application.set_accels_for_action("win.show-menu", &["F10"]);
}
}