feat: profile import/export; small refactor

This commit is contained in:
Gabriele Musco 2024-08-05 10:22:32 +02:00
parent 14689e5358
commit 93e6c3cf9e
2 changed files with 274 additions and 66 deletions

View file

@ -1,3 +1,6 @@
use std::fs::read_to_string;
use std::io::Write;
use super::alert::alert;
use super::devices_box::{DevicesBox, DevicesBoxMsg};
use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg};
@ -6,9 +9,11 @@ use super::steam_launch_options_box::{SteamLaunchOptionsBox, SteamLaunchOptionsB
use super::steamvr_calibration_box::SteamVrCalibrationBox;
use crate::config::Config;
use crate::dependencies::common::dep_pkexec;
use crate::file_utils::mount_has_nosuid;
use crate::file_utils::{get_writer, mount_has_nosuid};
use crate::gpu_profile::{get_amd_gpu_power_profile, GpuPowerProfile};
use crate::paths::{get_data_dir, get_home_dir};
use crate::profile::{LighthouseDriver, Profile, XRServiceType};
use crate::stateless_action;
use crate::steamvr_utils::chaperone_info_exists;
use crate::ui::app::{
AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction,
@ -18,9 +23,11 @@ use crate::ui::profile_editor::ProfileEditorInit;
use crate::ui::steamvr_calibration_box::SteamVrCalibrationBoxMsg;
use crate::ui::util::{limit_dropdown_width, warning_heading};
use crate::xr_devices::XRDevice;
use gtk::prelude::*;
use adw::prelude::*;
use gtk::glib::clone;
use relm4::actions::{ActionGroupName, RelmAction, RelmActionGroup};
use relm4::adw::{prelude::MessageDialogExt, ResponseAppearance};
use relm4::prelude::*;
use relm4::{new_action_group, new_stateless_action, prelude::*};
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
#[tracker::track]
@ -48,6 +55,10 @@ pub struct MainView {
steamvr_calibration_box: Controller<SteamVrCalibrationBox>,
#[tracker::do_not_track]
root_win: gtk::Window,
#[tracker::do_not_track]
profile_delete_action: gtk::gio::SimpleAction,
#[tracker::do_not_track]
profile_export_action: gtk::gio::SimpleAction,
xrservice_ready: bool,
}
@ -69,6 +80,9 @@ pub enum MainViewMsg {
SaveProfile(Profile),
UpdateDevices(Vec<XRDevice>),
UpdateXrServiceReady(bool),
ExportProfile,
ImportProfile,
OpenProfileEditor(Profile),
}
#[derive(Debug)]
@ -119,12 +133,23 @@ impl SimpleComponent for MainView {
},
section! {
"_About" => AboutAction,
}
},
},
profile_actions_menu: {
section! {
"_New profile" => ProfileMenuNewAction,
"_Edit profile" => ProfileMenuEditAction,
"Du_plicate profile" => ProfileMenuDuplicateAction,
"_Delete profile" => ProfileMenuDeleteAction,
},
section! {
"_Import profile" => ProfileMenuImportAction,
"E_xport profile" => ProfileMenuExportAction,
},
}
}
view! {
// TODO: refactor with adw.toolbarview
adw::ToolbarView {
set_top_bar_style: adw::ToolbarStyle::Flat,
set_bottom_bar_style: adw::ToolbarStyle::Flat,
@ -137,6 +162,7 @@ impl SimpleComponent for MainView {
set_vexpand: false,
pack_end: menu_btn = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic",
set_tooltip_text: Some("Menu"),
set_menu_model: Some(&app_menu),
},
},
@ -390,42 +416,14 @@ impl SimpleComponent for MainView {
},
connect_realize => move |dd| {
limit_dropdown_width(
dd, match model.enable_debug_view {
true => 18,
false => -1,
});
dd,
);
},
},
gtk::Button {
set_icon_name: "document-edit-symbolic",
set_tooltip_text: Some("Edit Profile"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::EditProfile);
},
},
gtk::Button {
set_icon_name: "edit-copy-symbolic",
set_tooltip_text: Some("Duplicate Profile"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::DuplicateProfile);
},
},
gtk::Button {
set_icon_name: "list-add-symbolic",
set_tooltip_text: Some("Create Profile"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::CreateProfile);
},
},
gtk::Button {
set_icon_name: "edit-delete-symbolic",
add_css_class: "destructive-action",
set_tooltip_text: Some("Delete Profile"),
#[track = "model.changed(Self::selected_profile())"]
set_sensitive: model.selected_profile.editable,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::DeleteProfile);
},
gtk::MenuButton {
set_icon_name: "view-more-symbolic",
set_tooltip_text: Some("Menu"),
set_menu_model: Some(&profile_actions_menu),
},
},
}
@ -469,16 +467,11 @@ impl SimpleComponent for MainView {
}
Self::Input::EnableDebugViewChanged(val) => {
self.set_enable_debug_view(val);
limit_dropdown_width(
self.profiles_dropdown.as_ref().unwrap(),
match val {
true => 18,
false => -1,
},
)
}
Self::Input::UpdateSelectedProfile(prof) => {
self.set_selected_profile(prof.clone());
self.profile_delete_action.set_enabled(prof.editable);
self.profile_export_action.set_enabled(prof.editable);
self.steamvr_calibration_box
.sender()
.emit(SteamVrCalibrationBoxMsg::SetVisible(
@ -511,7 +504,9 @@ impl SimpleComponent for MainView {
.unwrap()
.clone()
.set_selected(index);
self.set_selected_profile(self.profiles.get(index as usize).unwrap().clone());
sender.input(Self::Input::UpdateSelectedProfile(
self.profiles.get(index as usize).unwrap().clone(),
));
}
Self::Input::ProfileSelected(position) => {
sender
@ -522,22 +517,15 @@ impl SimpleComponent for MainView {
}
Self::Input::EditProfile => {
if self.selected_profile.editable {
self.create_profile_editor(sender, self.selected_profile.clone());
self.profile_editor
.as_ref()
.unwrap()
.emit(ProfileEditorMsg::Present);
sender.input(Self::Input::OpenProfileEditor(
self.selected_profile.clone(),
));
} else {
self.profile_not_editable_dialog.present();
}
}
Self::Input::CreateProfile => {
self.create_profile_editor(sender, Profile::default());
self.profile_editor
.as_ref()
.unwrap()
.sender()
.emit(ProfileEditorMsg::Present);
sender.input(Self::Input::OpenProfileEditor(Profile::default()));
}
Self::Input::DeleteProfile => {
self.profile_delete_confirm_dialog.present();
@ -549,12 +537,9 @@ impl SimpleComponent for MainView {
}
Self::Input::DuplicateProfile => {
if self.selected_profile.can_be_built {
self.create_profile_editor(sender, self.selected_profile.create_duplicate());
self.profile_editor
.as_ref()
.unwrap()
.sender()
.emit(ProfileEditorMsg::Present);
sender.input(Self::Input::OpenProfileEditor(
self.selected_profile.create_duplicate(),
));
} else {
alert(
"This profile cannot be duplicated",
@ -563,6 +548,112 @@ impl SimpleComponent for MainView {
);
}
}
Self::Input::ExportProfile => {
let prof = self.selected_profile.clone();
if !prof.editable {
return;
}
let chooser = gtk::FileDialog::builder()
.modal(true)
.title("Choose a location for the exported profile")
.initial_name(&format!("{}.json", &prof.name))
.accept_label("Export")
.build();
let root_win = self.root_win.clone();
chooser.save(
Some(&self.root_win),
gtk::gio::Cancellable::NONE,
move |res| {
if let Ok(file) = res {
if let Some(path) = file.path() {
if let Ok(mut writer) = get_writer(&path) {
if let Ok(s) = serde_json::to_string_pretty(&prof) {
let prof_id = prof.uuid;
let s = s
.replace(
get_data_dir().to_string_lossy().as_ref(),
"@DATADIR@",
)
.replace(
get_home_dir().to_string_lossy().as_ref(),
"@HOMEDIR@",
)
.replace(&prof_id, "@UUID@");
if writer.write_all(s.as_bytes()).is_ok() {
return;
}
}
}
}
alert("Failed to export profile", None, Some(&root_win));
}
},
);
}
Self::Input::ImportProfile => {
let confirm_dialog = adw::AlertDialog::builder()
.heading("Importing profiles is dangerous!")
.body_use_markup(true)
.body("Importing a profile from an <b>untrusted source</b> is dangerous and could lead to <b>arbitrary code execution</b>.\n\nInspect the profile JSON manually before importing it and make sure it doesn't contain anything suspicious, including references to untrusted forks.")
.build();
confirm_dialog.add_response("no", "_Cancel");
confirm_dialog.add_response("yes", "I understand the risk, continue");
confirm_dialog.set_response_appearance("yes", ResponseAppearance::Destructive);
let root_win = self.root_win.clone();
let fd_sender = sender.clone();
confirm_dialog.connect_response(None, move |_, res| {
if res != "yes" {
return;
}
let root_win = root_win.clone();
let fd_sender = fd_sender.clone();
let chooser = gtk::FileDialog::builder()
.modal(true)
.title("Choose a profile to import")
.accept_label("Import")
.filters(&{
let filter = gtk::gio::ListStore::builder()
.item_type(gtk::FileFilter::static_type())
.build();
filter.append(&{
let f = gtk::FileFilter::new();
f.add_mime_type("application/json");
f
});
filter
})
.build();
chooser.open(
Some(&root_win.clone()),
gtk::gio::Cancellable::NONE,
move |res| {
if let Ok(file) = res {
if let Some(path) = file.path() {
if let Ok(s) = read_to_string(path) {
let s = s
.replace(
"@DATADIR@",
get_data_dir().to_string_lossy().as_ref(),
)
.replace(
"@HOMEDIR@",
get_home_dir().to_string_lossy().as_ref(),
)
.replace("@UUID@", &Profile::new_uuid());
if let Ok(nprof) = serde_json::from_str::<Profile>(&s) {
fd_sender.input(Self::Input::OpenProfileEditor(nprof));
return;
}
}
}
alert("Failed to import profile", None, Some(&root_win));
}
},
);
});
confirm_dialog.present(Some(&self.root_win));
}
Self::Input::UpdateDevices(devs) => {
self.devices_box
.sender()
@ -571,6 +662,14 @@ impl SimpleComponent for MainView {
Self::Input::UpdateXrServiceReady(ready) => {
self.set_xrservice_ready(ready);
}
Self::Input::OpenProfileEditor(profile) => {
self.create_profile_editor(sender, profile);
self.profile_editor
.as_ref()
.unwrap()
.sender()
.emit(ProfileEditorMsg::Present);
}
}
}
@ -633,6 +732,35 @@ impl SimpleComponent for MainView {
init.selected_profile.lighthouse_driver == LighthouseDriver::SteamVR,
));
let mut actions = RelmActionGroup::<ProfileActionGroup>::new();
let profile_delete_action = {
let action = RelmAction::<ProfileMenuDeleteAction>::new_stateless(clone!(
#[strong]
sender,
move |_| {
sender.input(Self::Input::DeleteProfile);
}
));
let ret = action.gio_action().clone();
actions.add_action(action);
ret.set_enabled(false);
ret
};
let profile_export_action = {
let action = RelmAction::<ProfileMenuExportAction>::new_stateless(clone!(
#[strong]
sender,
move |_| {
sender.input(Self::Input::ExportProfile);
}
));
let ret = action.gio_action().clone();
actions.add_action(action);
ret.set_enabled(false);
ret
};
let mut model = Self {
xrservice_active: false,
enable_debug_view: init.config.debug_view_enabled,
@ -653,12 +781,93 @@ impl SimpleComponent for MainView {
steamvr_calibration_box,
profile_editor: None,
xrservice_ready: false,
profile_delete_action,
profile_export_action,
tracker: 0,
};
let widgets = view_output!();
model.profiles_dropdown = Some(widgets.profiles_dropdown.clone());
stateless_action!(
actions,
ProfileMenuNewAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Self::Input::CreateProfile);
}
)
);
stateless_action!(
actions,
ProfileMenuEditAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Self::Input::EditProfile);
}
)
);
stateless_action!(
actions,
ProfileMenuDuplicateAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Self::Input::DuplicateProfile);
}
)
);
stateless_action!(
actions,
ProfileMenuImportAction,
clone!(
#[strong]
sender,
move |_| {
sender.input(Self::Input::ImportProfile);
}
)
);
root.insert_action_group(ProfileActionGroup::NAME, Some(&actions.into_action_group()));
ComponentParts { model, widgets }
}
}
new_action_group!(ProfileActionGroup, "profile");
new_stateless_action!(
ProfileMenuNewAction,
ProfileActionGroup,
"profilemenunewaction"
);
new_stateless_action!(
ProfileMenuEditAction,
ProfileActionGroup,
"profilemenueditaction"
);
new_stateless_action!(
ProfileMenuDuplicateAction,
ProfileActionGroup,
"profilemenuduplicateaction"
);
new_stateless_action!(
ProfileMenuDeleteAction,
ProfileActionGroup,
"profilemenudeleteaction"
);
new_stateless_action!(
ProfileMenuImportAction,
ProfileActionGroup,
"profilemenuimportaction"
);
new_stateless_action!(
ProfileMenuExportAction,
ProfileActionGroup,
"profilemenuexportaction"
);

View file

@ -1,6 +1,6 @@
use gtk4::{gdk, gio, glib::clone, prelude::*};
pub fn limit_dropdown_width(dd: &gtk4::DropDown, chars: i32) {
pub fn limit_dropdown_width(dd: &gtk4::DropDown) {
let mut dd_child = dd
.first_child()
.unwrap()
@ -14,7 +14,6 @@ pub fn limit_dropdown_width(dd: &gtk4::DropDown, chars: i32) {
break;
}
if let Ok(label) = dd_child.clone().unwrap().downcast::<gtk4::Label>() {
label.set_max_width_chars(chars);
label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
}
let nc = dd_child.unwrap().first_child().clone();