main window: added functionality to menu actions

- added ability to send toast messages
This commit is contained in:
Observer KRypt0n_ 2023-02-22 21:43:01 +02:00
parent 8199e0eac9
commit 14067c7bdf
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
3 changed files with 200 additions and 96 deletions

View file

@ -1,3 +1,7 @@
launcher-folder-opening-error = Failed to open launcher folder
game-folder-opening-error = Failed to open game folder
config-file-opening-error = Failed to open config file
config-flush-error = Failed to flush config config-flush-error = Failed to flush config
wine-prefix-update-failed = Failed to update wine prefix wine-prefix-update-failed = Failed to update wine prefix
dxvk-install-failed = Failed to install DXVK dxvk-install-failed = Failed to install DXVK

View file

@ -1,3 +1,7 @@
launcher-folder-opening-error = Не удалось открыть папку лаунчера
game-folder-opening-error = Не удалось открыть папку игры
config-file-opening-error = Не удалось открыть файл настроек
config-flush-error = Ошибка сохранения настроек config-flush-error = Ошибка сохранения настроек
wine-prefix-update-failed = Ошибка обновления префикса Wine wine-prefix-update-failed = Ошибка обновления префикса Wine
dxvk-install-failed = Ошибка установки DXVK dxvk-install-failed = Ошибка установки DXVK

View file

@ -8,6 +8,8 @@ use relm4::{
use gtk::prelude::*; use gtk::prelude::*;
use adw::prelude::*; use adw::prelude::*;
use gtk::glib::clone;
use anime_launcher_sdk::config::launcher::LauncherStyle; use anime_launcher_sdk::config::launcher::LauncherStyle;
use crate::*; use crate::*;
@ -24,10 +26,13 @@ relm4::new_stateless_action!(ConfigFile, WindowActionGroup, "config_file");
relm4::new_stateless_action!(About, WindowActionGroup, "about"); relm4::new_stateless_action!(About, WindowActionGroup, "about");
static mut MAIN_WINDOW: Option<adw::Window> = None;
static mut PREFERENCES_WINDOW: Option<AsyncController<PreferencesApp>> = None; static mut PREFERENCES_WINDOW: Option<AsyncController<PreferencesApp>> = None;
static mut ABOUT_DIALOG: Option<Controller<AboutDialog>> = None; static mut ABOUT_DIALOG: Option<Controller<AboutDialog>> = None;
pub struct App { pub struct App {
toast_overlay: adw::ToastOverlay,
loading: Option<Option<String>>, loading: Option<Option<String>>,
style: LauncherStyle style: LauncherStyle
} }
@ -42,6 +47,11 @@ pub enum AppMsg {
/// was retrieved from remote repos /// was retrieved from remote repos
UpdatePatch(Option<Patch>), UpdatePatch(Option<Patch>),
Toast {
title: String,
description: Option<String>
},
UpdateLoadingStatus(Option<Option<String>>), UpdateLoadingStatus(Option<Option<String>>),
PerformAction, PerformAction,
OpenPreferences, OpenPreferences,
@ -103,114 +113,117 @@ impl SimpleComponent for App {
LauncherStyle::Classic => "" LauncherStyle::Classic => ""
}, },
gtk::Box { #[local_ref]
set_orientation: gtk::Orientation::Vertical, toast_overlay -> adw::ToastOverlay {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
adw::HeaderBar { adw::HeaderBar {
#[watch]
add_css_class: match model.style {
LauncherStyle::Modern => "",
LauncherStyle::Classic => "flat"
},
#[watch]
remove_css_class: match model.style {
LauncherStyle::Modern => "flat",
LauncherStyle::Classic => ""
},
pack_end = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic",
set_menu_model: Some(&main_menu)
}
},
adw::StatusPage {
set_title: "Loading data",
set_icon_name: Some("process-working"),
set_vexpand: true,
#[watch]
set_description: match &model.loading {
Some(Some(desc)) => Some(desc),
Some(None) | None => None
},
#[watch]
set_visible: model.loading.is_some()
},
adw::PreferencesPage {
#[watch]
set_visible: model.loading.is_none(),
add = &adw::PreferencesGroup {
#[watch] #[watch]
set_visible: model.style == LauncherStyle::Modern, add_css_class: match model.style {
LauncherStyle::Modern => "",
gtk::Image { LauncherStyle::Classic => "flat"
set_resource: Some("/org/app/images/icon.png"),
set_vexpand: true,
set_margin_top: 48
}, },
gtk::Label { #[watch]
set_label: "An Anime Game Launcher", remove_css_class: match model.style {
set_margin_top: 32, LauncherStyle::Modern => "flat",
add_css_class: "title-1" LauncherStyle::Classic => ""
},
pack_end = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic",
set_menu_model: Some(&main_menu)
} }
}, },
add = &adw::PreferencesGroup { adw::StatusPage {
#[watch] set_title: "Loading data",
set_valign: match model.style { set_icon_name: Some(APP_ID),
LauncherStyle::Modern => gtk::Align::Center,
LauncherStyle::Classic => gtk::Align::End
},
#[watch]
set_width_request: match model.style {
LauncherStyle::Modern => -1,
LauncherStyle::Classic => 800
},
set_vexpand: true, set_vexpand: true,
gtk::Box { #[watch]
set_description: match &model.loading {
Some(Some(desc)) => Some(desc),
Some(None) | None => None
},
#[watch]
set_visible: model.loading.is_some()
},
adw::PreferencesPage {
#[watch]
set_visible: model.loading.is_none(),
add = &adw::PreferencesGroup {
#[watch] #[watch]
set_halign: match model.style { set_visible: model.style == LauncherStyle::Modern,
gtk::Image {
set_resource: Some("/org/app/images/icon.png"),
set_vexpand: true,
set_margin_top: 48
},
gtk::Label {
set_label: "An Anime Game Launcher",
set_margin_top: 32,
add_css_class: "title-1"
}
},
add = &adw::PreferencesGroup {
#[watch]
set_valign: match model.style {
LauncherStyle::Modern => gtk::Align::Center, LauncherStyle::Modern => gtk::Align::Center,
LauncherStyle::Classic => gtk::Align::End LauncherStyle::Classic => gtk::Align::End
}, },
#[watch] #[watch]
set_height_request: match model.style { set_width_request: match model.style {
LauncherStyle::Modern => -1, LauncherStyle::Modern => -1,
LauncherStyle::Classic => 40 LauncherStyle::Classic => 800
}, },
set_margin_top: 64, set_vexpand: true,
set_spacing: 8,
gtk::Button { gtk::Box {
set_label: &tr("launch"),
set_hexpand: false,
set_width_request: 200,
add_css_class: "suggested-action",
connect_clicked => AppMsg::PerformAction
},
gtk::Button {
#[watch] #[watch]
set_width_request: match model.style { set_halign: match model.style {
LauncherStyle::Modern => gtk::Align::Center,
LauncherStyle::Classic => gtk::Align::End
},
#[watch]
set_height_request: match model.style {
LauncherStyle::Modern => -1, LauncherStyle::Modern => -1,
LauncherStyle::Classic => 40 LauncherStyle::Classic => 40
}, },
set_icon_name: "emblem-system-symbolic", set_margin_top: 64,
set_spacing: 8,
connect_clicked => AppMsg::OpenPreferences gtk::Button {
set_label: &tr("launch"),
set_hexpand: false,
set_width_request: 200,
add_css_class: "suggested-action",
connect_clicked => AppMsg::PerformAction
},
gtk::Button {
#[watch]
set_width_request: match model.style {
LauncherStyle::Modern => -1,
LauncherStyle::Classic => 40
},
set_icon_name: "emblem-system-symbolic",
connect_clicked => AppMsg::OpenPreferences
}
} }
} }
} }
@ -227,10 +240,13 @@ impl SimpleComponent for App {
tracing::info!("Initializing main window"); tracing::info!("Initializing main window");
let model = App { let model = App {
toast_overlay: adw::ToastOverlay::new(),
loading: Some(None), loading: Some(None),
style: CONFIG.launcher.style style: CONFIG.launcher.style
}; };
let toast_overlay = &model.toast_overlay;
let widgets = view_output!(); let widgets = view_output!();
if crate::APP_DEBUG { if crate::APP_DEBUG {
@ -240,6 +256,8 @@ impl SimpleComponent for App {
let about_dialog_broker: MessageBroker<AboutDialog> = MessageBroker::new(); let about_dialog_broker: MessageBroker<AboutDialog> = MessageBroker::new();
unsafe { unsafe {
MAIN_WINDOW = Some(widgets.main_window.clone());
PREFERENCES_WINDOW = Some(PreferencesApp::builder() PREFERENCES_WINDOW = Some(PreferencesApp::builder()
.launch(widgets.main_window.clone().into()) .launch(widgets.main_window.clone().into())
.forward(sender.input_sender(), std::convert::identity)); .forward(sender.input_sender(), std::convert::identity));
@ -252,18 +270,62 @@ impl SimpleComponent for App {
let group = RelmActionGroup::<WindowActionGroup>::new(); let group = RelmActionGroup::<WindowActionGroup>::new();
// TODO // TODO: reduce code somehow
group.add_action::<LauncherFolder>(&RelmAction::new_stateless(move |_| {
println!("Open launcher folder!");
}));
group.add_action::<GameFolder>(&RelmAction::new_stateless(move |_| { group.add_action::<LauncherFolder>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
println!("Open game folder!"); if let Some(dir) = anime_launcher_sdk::consts::launcher_dir() {
})); let child = std::process::Command::new("xdg-open")
.arg(dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
group.add_action::<ConfigFile>(&RelmAction::new_stateless(move |_| { if let Err(err) = child {
println!("Open config file!"); sender.input(AppMsg::Toast {
})); title: tr("launcher-folder-opening-error"),
description: Some(err.to_string())
});
tracing::error!("Failed to open launcher folder: {err}");
}
}
})));
group.add_action::<GameFolder>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
let child = std::process::Command::new("xdg-open")
.arg(&CONFIG.game.path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
if let Err(err) = child {
sender.input(AppMsg::Toast {
title: tr("game-folder-opening-error"),
description: Some(err.to_string())
});
tracing::error!("Failed to open game folder: {err}");
}
})));
group.add_action::<ConfigFile>(&RelmAction::new_stateless(clone!(@strong sender => move |_| {
if let Some(file) = anime_launcher_sdk::consts::config_file() {
let child = std::process::Command::new("xdg-open")
.arg(file)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
if let Err(err) = child {
sender.input(AppMsg::Toast {
title: tr("config-file-opening-error"),
description: Some(err.to_string())
});
tracing::error!("Failed to open config file: {err}");
}
}
})));
group.add_action::<About>(&RelmAction::new_stateless(move |_| { group.add_action::<About>(&RelmAction::new_stateless(move |_| {
about_dialog_broker.send(AboutDialogMsg::Show); about_dialog_broker.send(AboutDialogMsg::Show);
@ -328,16 +390,50 @@ impl SimpleComponent for App {
#[allow(unused_must_use)] #[allow(unused_must_use)]
AppMsg::UpdateGameDiff(diff) => unsafe { AppMsg::UpdateGameDiff(diff) => unsafe {
PREFERENCES_WINDOW.as_ref().unwrap_unchecked().sender().send(PreferencesAppMsg::UpdateGameDiff(diff)); PREFERENCES_WINDOW.as_ref().unwrap_unchecked().sender().send(PreferencesAppMsg::UpdateGameDiff(diff));
}, }
#[allow(unused_must_use)] #[allow(unused_must_use)]
AppMsg::UpdatePatch(patch) => unsafe { AppMsg::UpdatePatch(patch) => unsafe {
PREFERENCES_WINDOW.as_ref().unwrap_unchecked().sender().send(PreferencesAppMsg::UpdatePatch(patch)); PREFERENCES_WINDOW.as_ref().unwrap_unchecked().sender().send(PreferencesAppMsg::UpdatePatch(patch));
}, }
AppMsg::Toast { title, description } => unsafe {
let toast = adw::Toast::new(&title);
toast.set_timeout(5);
if let Some(description) = description {
toast.set_button_label(Some(&tr("details")));
let dialog = adw::MessageDialog::new(Some(MAIN_WINDOW.as_ref().unwrap_unchecked()), Some(&title), Some(&description));
dialog.add_response("close", &tr("close"));
dialog.add_response("save", &tr("save"));
dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested);
#[allow(unused_must_use)]
dialog.connect_response(Some("save"), |_, _| {
let result = std::process::Command::new("xdg-open")
.arg(crate::DEBUG_FILE.as_os_str())
.output();
if let Err(err) = result {
tracing::error!("Failed to open debug file: {}", err);
}
});
toast.connect_button_clicked(move |_| {
dialog.show();
});
}
self.toast_overlay.add_toast(&toast);
}
AppMsg::UpdateLoadingStatus(status) => { AppMsg::UpdateLoadingStatus(status) => {
self.loading = status; self.loading = status;
}, }
AppMsg::PerformAction => { AppMsg::PerformAction => {
anime_launcher_sdk::game::run().expect("Failed to run the game"); anime_launcher_sdk::game::run().expect("Failed to run the game");