From 33a83b77b8b43457b70bfe98d30b84297d516763 Mon Sep 17 00:00:00 2001 From: antoniohauren Date: Thu, 3 Sep 2026 11:20:57 -0300 Subject: [PATCH 1/7] Add achievement status filters --- po/samrewritten.pot | 110 +++++++++--------- .../achievement_manual_view/mod.rs | 4 +- .../achievement_manual_view/row_factory.rs | 10 +- src/gui_frontend/achievement_view.rs | 71 ++++++++++- src/gui_frontend/app_list_view/mod.rs | 21 ++++ src/gui_frontend/app_view.rs | 3 + 6 files changed, 158 insertions(+), 61 deletions(-) diff --git a/po/samrewritten.pot b/po/samrewritten.pot index 9778208..76a39fb 100644 --- a/po/samrewritten.pot +++ b/po/samrewritten.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: SamRewritten\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 21:40+0200\n" +"POT-Creation-Date: 2026-09-03 10:59-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,8 +17,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/gui_frontend/ui_components.rs:159 src/gui_frontend/app_view.rs:62 -#: src/gui_frontend/app_list_view/mod.rs:421 +#: src/gui_frontend/ui_components.rs:159 src/gui_frontend/app_view.rs:63 +#: src/gui_frontend/app_list_view/mod.rs:422 msgid "Loading..." msgstr "" @@ -147,9 +147,9 @@ msgstr "" #: src/gui_frontend/dialogs.rs:290 msgid "" "If you've installed Steam in a custom location, you can point SamRewritten " -"to it using environment variables. Please check the GitHub page for instructions, or to report " -"your issue." +"to it using environment variables. Please check the GitHub page for instructions, or to " +"report your issue." msgstr "" #: src/gui_frontend/dialogs.rs:295 @@ -234,43 +234,43 @@ msgstr "" msgid "Select your Steam folder" msgstr "" -#: src/gui_frontend/app_view.rs:68 +#: src/gui_frontend/app_view.rs:69 msgid "Achievements:" msgstr "" -#: src/gui_frontend/app_view.rs:82 +#: src/gui_frontend/app_view.rs:83 msgid "Stats:" msgstr "" -#: src/gui_frontend/app_view.rs:96 +#: src/gui_frontend/app_view.rs:97 msgid "Last playtime:" msgstr "" -#: src/gui_frontend/app_view.rs:110 +#: src/gui_frontend/app_view.rs:111 msgid "Type:" msgstr "" -#: src/gui_frontend/app_view.rs:124 +#: src/gui_frontend/app_view.rs:125 msgid "Developer:" msgstr "" -#: src/gui_frontend/app_view.rs:141 +#: src/gui_frontend/app_view.rs:142 msgid "Metacritic:" msgstr "" -#: src/gui_frontend/app_view.rs:155 +#: src/gui_frontend/app_view.rs:156 msgid "Failed to load app." msgstr "" -#: src/gui_frontend/app_view.rs:161 +#: src/gui_frontend/app_view.rs:162 msgid "No entries found." msgstr "" -#: src/gui_frontend/app_view.rs:178 +#: src/gui_frontend/app_view.rs:179 msgid "Achievements" msgstr "" -#: src/gui_frontend/app_view.rs:181 +#: src/gui_frontend/app_view.rs:182 msgid "Stats" msgstr "" @@ -421,150 +421,150 @@ msgstr "" msgid "Clear selected user" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:442 +#: src/gui_frontend/app_list_view/mod.rs:443 msgid "SamRewritten could not connect to Steam. Is it running?" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:447 +#: src/gui_frontend/app_list_view/mod.rs:448 msgid "Try again" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:464 -#: src/gui_frontend/app_list_view/mod.rs:1752 +#: src/gui_frontend/app_list_view/mod.rs:465 +#: src/gui_frontend/app_list_view/mod.rs:1771 msgid "Name or AppId (Ctrl+K)" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:480 +#: src/gui_frontend/app_list_view/mod.rs:481 msgid "Show or hide the sidebar" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1747 +#: src/gui_frontend/app_list_view/mod.rs:1766 msgid "Achievement or stat..." msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1866 -#: src/gui_frontend/app_list_view/settings_bindings.rs:333 +#: src/gui_frontend/app_list_view/mod.rs:1885 +#: src/gui_frontend/app_list_view/settings_bindings.rs:328 msgid "Could not change the in-game setting" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1868 -#: src/gui_frontend/app_list_view/settings_bindings.rs:334 +#: src/gui_frontend/app_list_view/mod.rs:1887 +#: src/gui_frontend/app_list_view/settings_bindings.rs:329 msgid "The change could not be applied. Restart SamRewritten and try again." msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:58 +#: src/gui_frontend/app_list_view/sidebar.rs:57 msgid "Hide with no achievements" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:64 +#: src/gui_frontend/app_list_view/sidebar.rs:63 msgid "Hide at 100%" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:70 +#: src/gui_frontend/app_list_view/sidebar.rs:69 msgid "Hide at 0%" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:76 +#: src/gui_frontend/app_list_view/sidebar.rs:75 msgid "Hide never launched" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:82 +#: src/gui_frontend/app_list_view/sidebar.rs:81 msgid "Only currently idling" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:88 +#: src/gui_frontend/app_list_view/sidebar.rs:87 msgid "Hide hidden in Steam" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:94 +#: src/gui_frontend/app_list_view/sidebar.rs:93 msgid "Show junk" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:108 +#: src/gui_frontend/app_list_view/sidebar.rs:107 msgid "App ID" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:113 +#: src/gui_frontend/app_list_view/sidebar.rs:112 msgid "Name" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:118 +#: src/gui_frontend/app_list_view/sidebar.rs:117 msgid "Last played" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:123 +#: src/gui_frontend/app_list_view/sidebar.rs:122 msgid "Playtime" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:128 +#: src/gui_frontend/app_list_view/sidebar.rs:127 msgid "Completion" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:133 +#: src/gui_frontend/app_list_view/sidebar.rs:132 msgid "Achievements left" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:183 +#: src/gui_frontend/app_list_view/sidebar.rs:180 msgid "Filters on a search term, which SamRewritten cannot reproduce exactly." msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:186 +#: src/gui_frontend/app_list_view/sidebar.rs:183 msgid "Uses a Steam filter SamRewritten cannot reproduce exactly." msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:189 +#: src/gui_frontend/app_list_view/sidebar.rs:186 msgid "" "Needs information from Steam that could not be read. Refresh to try again." msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:192 +#: src/gui_frontend/app_list_view/sidebar.rs:189 msgid "" "Needs your friends' games, which Steam can only tell us when it is online." msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:201 +#: src/gui_frontend/app_list_view/sidebar.rs:198 msgid "Favorites" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:228 +#: src/gui_frontend/app_list_view/sidebar.rs:225 msgid "View profile" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:283 +#: src/gui_frontend/app_list_view/sidebar.rs:280 #: src/gui_frontend/profile_view/mod.rs:484 msgid "Steam user" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:305 +#: src/gui_frontend/app_list_view/sidebar.rs:300 msgid "Steam is offline. What needs its servers is turned off." msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:316 +#: src/gui_frontend/app_list_view/sidebar.rs:311 msgid "Fetching completion…" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:322 +#: src/gui_frontend/app_list_view/sidebar.rs:317 msgid "Click to cancel" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:353 +#: src/gui_frontend/app_list_view/sidebar.rs:348 msgid "Filters" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:364 +#: src/gui_frontend/app_list_view/sidebar.rs:359 msgid "Steam collection" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:365 -#: src/gui_frontend/app_list_view/sidebar.rs:588 +#: src/gui_frontend/app_list_view/sidebar.rs:360 +#: src/gui_frontend/app_list_view/sidebar.rs:580 msgid "All games" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:448 +#: src/gui_frontend/app_list_view/sidebar.rs:442 msgid "Sort by" msgstr "" -#: src/gui_frontend/app_list_view/sidebar.rs:513 +#: src/gui_frontend/app_list_view/sidebar.rs:505 msgid "Reset filters" msgstr "" diff --git a/src/gui_frontend/achievement_manual_view/mod.rs b/src/gui_frontend/achievement_manual_view/mod.rs index 6fa4646..28fd65f 100644 --- a/src/gui_frontend/achievement_manual_view/mod.rs +++ b/src/gui_frontend/achievement_manual_view/mod.rs @@ -41,7 +41,7 @@ use gtk::gio::{ListStore, SimpleAction}; use gtk::glib::{MainContext, clone}; use gtk::prelude::*; use gtk::{ - Box, Button, Frame, Label, ListView, NoSelection, Orientation, ScrolledWindow, + Box, Button, CustomFilter, Frame, Label, ListView, NoSelection, Orientation, ScrolledWindow, SignalListItemFactory, SpinButton, Stack, ToggleButton, glib, }; use header::create_header; @@ -111,6 +111,7 @@ pub fn create_achievements_manual_view( app_id: &Rc>>, app_unlocked_achievements_count: &Rc>, filtered_model: &NoSelection, + status_filter: &CustomFilter, raw_model: &ListStore, timed_raw_model: &ListStore, achievement_views_stack: &Stack, @@ -430,6 +431,7 @@ pub fn create_achievements_manual_view( &header.queue_label, &cancelled_task, &update_autofill, + status_filter, ); let app_achievements_list_view = ListView::builder() diff --git a/src/gui_frontend/achievement_manual_view/row_factory.rs b/src/gui_frontend/achievement_manual_view/row_factory.rs index ddd649b..936243a 100644 --- a/src/gui_frontend/achievement_manual_view/row_factory.rs +++ b/src/gui_frontend/achievement_manual_view/row_factory.rs @@ -25,7 +25,7 @@ use crate::utils::format::format_achievement_progress; use gtk::gio::{ListStore, spawn_blocking}; use gtk::glib::{self, MainContext, clone}; use gtk::prelude::*; -use gtk::{Button, Label, ListItem, SignalListItemFactory}; +use gtk::{Button, CustomFilter, FilterChange, Label, ListItem, SignalListItemFactory}; use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; @@ -44,6 +44,7 @@ pub(super) fn install_row_factory( queue_label: &Label, cancelled_task: &Arc, update_autofill: &Rc, + status_filter: &CustomFilter, ) { factory.connect_setup(clone!( #[strong] @@ -59,6 +60,8 @@ pub(super) fn install_row_factory( #[strong] update_autofill, #[weak] + status_filter, + #[weak] raw_model, #[weak] app_achievement_count_value, @@ -93,6 +96,8 @@ pub(super) fn install_row_factory( raw_model, #[weak] start_button, + #[weak] + status_filter, move |switch| { let Some(achievement_object) = list_item.item().and_downcast::() @@ -143,6 +148,8 @@ pub(super) fn install_row_factory( achievement_object, #[weak] start_button, + #[weak] + status_filter, async move { let result = handle.await.expect("spawn_blocking task panicked"); // Steam accepting the call and then failing to @@ -169,6 +176,7 @@ pub(super) fn install_row_factory( start_button .set_sensitive(new_unlocked != raw_model_len as usize); update_autofill(); + status_filter.changed(FilterChange::Different); } Ok(false) => { eprintln!("[CLIENT] Steam did not store the achievement"); diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index 2c074ad..395f0db 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -17,11 +17,14 @@ use crate::gui_frontend::MainApplication; use crate::gui_frontend::achievement_automatic_view::create_achievements_automatic_view; use crate::gui_frontend::achievement_manual_view::create_achievements_manual_view; use crate::gui_frontend::gobjects::achievement::GAchievementObject; +use crate::gui_frontend::i18n::tr; use gtk::gio::ListStore; +use gtk::glib; use gtk::prelude::*; use gtk::{ - CustomSorter, FilterListModel, Label, NoSelection, SortListModel, Stack, StackTransitionType, - StringFilter, StringFilterMatchMode, + Align, Box, CustomFilter, CustomSorter, FilterChange, FilterListModel, Label, NoSelection, + Orientation, SortListModel, Stack, StackTransitionType, StringFilter, StringFilterMatchMode, + ToggleButton, }; use std::cell::Cell; use std::cmp::Ordering; @@ -34,7 +37,7 @@ pub fn create_achievements_view( app_unlocked_achievements_count: Rc>, application: &MainApplication, app_achievement_count_value: &Label, -) -> (Stack, ListStore, StringFilter, Arc) { +) -> (Stack, ListStore, StringFilter, Arc, Box) { let app_achievements_model = ListStore::new::(); let app_timed_achievements_model = ListStore::new::(); @@ -47,6 +50,64 @@ pub fn create_achievements_view( .model(&app_achievements_model) .filter(&app_achievement_string_filter) .build(); + // None shows all achievements; Some(false) and Some(true) show only + // locked and unlocked achievements respectively. + let achievement_status = Rc::new(Cell::new(None)); + let achievement_status_filter = CustomFilter::new({ + let achievement_status = Rc::clone(&achievement_status); + move |object| { + let achievement = object.downcast_ref::().unwrap(); + achievement_status + .get() + .is_none_or(|unlocked| achievement.is_achieved() == unlocked) + } + }); + let app_achievement_status_filter_model = FilterListModel::builder() + .model(&app_achievement_filter_model) + .filter(&achievement_status_filter) + .build(); + let all_toggle = ToggleButton::builder() + .icon_name("object-select-symbolic") + .tooltip_text(tr("All").as_str()) + .build(); + let locked_toggle = ToggleButton::builder() + .icon_name("changes-prevent-symbolic") + .tooltip_text(tr("Locked").as_str()) + .group(&all_toggle) + .build(); + let unlocked_toggle = ToggleButton::builder() + .icon_name("changes-allow-symbolic") + .tooltip_text(tr("Unlocked").as_str()) + .group(&all_toggle) + .build(); + all_toggle.set_active(true); + let status_filter_box = Box::builder() + .orientation(Orientation::Horizontal) + .css_classes(["linked"]) + .valign(Align::Center) + .visible(false) + .build(); + status_filter_box.append(&all_toggle); + status_filter_box.append(&locked_toggle); + status_filter_box.append(&unlocked_toggle); + for (button, state) in [ + (&all_toggle, None), + (&locked_toggle, Some(false)), + (&unlocked_toggle, Some(true)), + ] { + button.connect_toggled(glib::clone!( + #[strong] + achievement_status, + #[weak] + achievement_status_filter, + move |button| { + if button.is_active() { + achievement_status.set(state); + achievement_status_filter.changed(FilterChange::Different); + } + } + )); + } let app_achievement_timed_filter_model = FilterListModel::builder() .model(&app_timed_achievements_model) .filter(&app_achievement_string_filter) @@ -65,7 +126,7 @@ pub fn create_achievements_view( .into() }); let app_achievement_sort_model = SortListModel::builder() - .model(&app_achievement_filter_model) + .model(&app_achievement_status_filter_model) .sorter(&global_achieved_percent_sorter) .build(); @@ -81,6 +142,7 @@ pub fn create_achievements_view( &app_id, &app_unlocked_achievements_count, &app_achievement_selection_model, + &achievement_status_filter, &app_achievements_model, &app_timed_achievements_model, &achievement_views_stack, @@ -98,5 +160,6 @@ pub fn create_achievements_view( app_achievements_model, app_achievement_string_filter, cancel_timed_unlock, + status_filter_box, ) } diff --git a/src/gui_frontend/app_list_view/mod.rs b/src/gui_frontend/app_list_view/mod.rs index 581a971..f3728a4 100644 --- a/src/gui_frontend/app_list_view/mod.rs +++ b/src/gui_frontend/app_list_view/mod.rs @@ -410,6 +410,7 @@ pub fn create_main_ui( app_pane, cancel_timed_unlock, app_achievements_stack, + achievement_status_filter_box, ) = create_app_view( app_id.clone(), app_unlocked_achievements_count.clone(), @@ -484,6 +485,7 @@ pub fn create_main_ui( header_bar.pack_start(&search_entry); header_bar.pack_end(&context_menu_button); header_bar.pack_end(&context_menu_button_loading); + header_bar.pack_end(&achievement_status_filter_box); let list_scrolled_window = ScrolledWindow::builder() .hscrollbar_policy(PolicyType::Never) @@ -1708,6 +1710,19 @@ pub fn create_main_ui( &app_stack, ); + app_stack.connect_visible_child_name_notify(clone!( + #[weak] + list_stack, + #[weak] + achievement_status_filter_box, + move |stack| { + achievement_status_filter_box.set_visible( + list_stack.visible_child_name().as_deref() == Some("app") + && stack.visible_child_name().as_deref() == Some("achievements"), + ); + } + )); + list_stack.connect_visible_child_notify(clone!( #[weak] back_button, @@ -1721,6 +1736,8 @@ pub fn create_main_ui( search_entry, #[weak] action_refresh_app_list, + #[weak] + achievement_status_filter_box, #[strong] prefetched_progress, #[strong] @@ -1729,6 +1746,10 @@ pub fn create_main_ui( let page = stack.visible_child_name(); let page = page.as_deref(); let on_own_page = page == Some("app") || page == Some("profile"); + achievement_status_filter_box.set_visible( + page == Some("app") + && app_stack.visible_child_name().as_deref() == Some("achievements"), + ); sidebar_button.set_visible(!on_own_page); sidebar_button.set_sensitive(page == Some("list")); search_entry.set_sensitive(page != Some("profile")); diff --git a/src/gui_frontend/app_view.rs b/src/gui_frontend/app_view.rs index 1315f3d..f1656ef 100644 --- a/src/gui_frontend/app_view.rs +++ b/src/gui_frontend/app_view.rs @@ -57,6 +57,7 @@ pub fn create_app_view( Box, Arc, Stack, + Box, ) { let app_spinner = Spinner::builder().spinning(true).margin_end(5).build(); let app_spinner_label = Label::builder().label(tr("Loading...").as_str()).build(); @@ -226,6 +227,7 @@ pub fn create_app_view( app_achievements_model, app_achievement_string_filter, cancel_timed_unlock, + achievement_status_filter_box, ) = create_achievements_view( app_id.clone(), app_unlocked_achievements_count, @@ -337,5 +339,6 @@ pub fn create_app_view( app_pane, cancel_timed_unlock, app_achievements_stack, + achievement_status_filter_box, ) } From 35bc06af8284871a7d424d29c282de216f03d4ed Mon Sep 17 00:00:00 2001 From: antoniohauren Date: Thu, 3 Sep 2026 11:46:47 -0300 Subject: [PATCH 2/7] Add achievement ordering menu --- po/POTFILES.in | 1 + po/samrewritten.pot | 74 ++++++++++----- src/gui_frontend/achievement_view.rs | 112 +++++++++++++++++++---- src/gui_frontend/app_list_view/mod.rs | 23 +++-- src/gui_frontend/app_view.rs | 7 +- src/gui_frontend/gobjects/achievement.rs | 9 ++ 6 files changed, 179 insertions(+), 47 deletions(-) diff --git a/po/POTFILES.in b/po/POTFILES.in index 3161975..bfc3f1e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,6 +5,7 @@ src/gui_frontend/dialogs.rs src/gui_frontend/app_view.rs src/gui_frontend/stat_view.rs src/gui_frontend/achievement_automatic_view.rs +src/gui_frontend/achievement_view.rs src/gui_frontend/achievement_manual_view/mod.rs src/gui_frontend/achievement_manual_view/header.rs src/gui_frontend/achievement_manual_view/config_popover.rs diff --git a/po/samrewritten.pot b/po/samrewritten.pot index 76a39fb..42b3dfc 100644 --- a/po/samrewritten.pot +++ b/po/samrewritten.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: SamRewritten\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:59-0300\n" +"POT-Creation-Date: 2026-09-03 11:39-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,8 +17,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/gui_frontend/ui_components.rs:159 src/gui_frontend/app_view.rs:63 -#: src/gui_frontend/app_list_view/mod.rs:422 +#: src/gui_frontend/ui_components.rs:159 src/gui_frontend/app_view.rs:64 +#: src/gui_frontend/app_list_view/mod.rs:423 msgid "Loading..." msgstr "" @@ -234,43 +234,43 @@ msgstr "" msgid "Select your Steam folder" msgstr "" -#: src/gui_frontend/app_view.rs:69 +#: src/gui_frontend/app_view.rs:70 msgid "Achievements:" msgstr "" -#: src/gui_frontend/app_view.rs:83 +#: src/gui_frontend/app_view.rs:84 msgid "Stats:" msgstr "" -#: src/gui_frontend/app_view.rs:97 +#: src/gui_frontend/app_view.rs:98 msgid "Last playtime:" msgstr "" -#: src/gui_frontend/app_view.rs:111 +#: src/gui_frontend/app_view.rs:112 msgid "Type:" msgstr "" -#: src/gui_frontend/app_view.rs:125 +#: src/gui_frontend/app_view.rs:126 msgid "Developer:" msgstr "" -#: src/gui_frontend/app_view.rs:142 +#: src/gui_frontend/app_view.rs:143 msgid "Metacritic:" msgstr "" -#: src/gui_frontend/app_view.rs:156 +#: src/gui_frontend/app_view.rs:157 msgid "Failed to load app." msgstr "" -#: src/gui_frontend/app_view.rs:162 +#: src/gui_frontend/app_view.rs:163 msgid "No entries found." msgstr "" -#: src/gui_frontend/app_view.rs:179 +#: src/gui_frontend/app_view.rs:180 msgid "Achievements" msgstr "" -#: src/gui_frontend/app_view.rs:182 +#: src/gui_frontend/app_view.rs:183 msgid "Stats" msgstr "" @@ -286,6 +286,38 @@ msgstr "" msgid "Stop and go back" msgstr "" +#: src/gui_frontend/achievement_view.rs:78 +msgid "All" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:82 +msgid "Locked" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:87 +msgid "Unlocked" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:163 +msgid "Steam default" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:166 +msgid "Alphabetically" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:170 +msgid "Global percentage" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:174 +msgid "Unlock date" +msgstr "" + +#: src/gui_frontend/achievement_view.rs:185 +msgid "Order achievements" +msgstr "" + #: src/gui_frontend/achievement_manual_view/mod.rs:57 msgid "No achievements staged" msgstr "" @@ -421,33 +453,33 @@ msgstr "" msgid "Clear selected user" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:443 +#: src/gui_frontend/app_list_view/mod.rs:444 msgid "SamRewritten could not connect to Steam. Is it running?" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:448 +#: src/gui_frontend/app_list_view/mod.rs:449 msgid "Try again" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:465 -#: src/gui_frontend/app_list_view/mod.rs:1771 +#: src/gui_frontend/app_list_view/mod.rs:466 +#: src/gui_frontend/app_list_view/mod.rs:1778 msgid "Name or AppId (Ctrl+K)" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:481 +#: src/gui_frontend/app_list_view/mod.rs:482 msgid "Show or hide the sidebar" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1766 +#: src/gui_frontend/app_list_view/mod.rs:1773 msgid "Achievement or stat..." msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1885 +#: src/gui_frontend/app_list_view/mod.rs:1892 #: src/gui_frontend/app_list_view/settings_bindings.rs:328 msgid "Could not change the in-game setting" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1887 +#: src/gui_frontend/app_list_view/mod.rs:1894 #: src/gui_frontend/app_list_view/settings_bindings.rs:329 msgid "The change could not be applied. Restart SamRewritten and try again." msgstr "" diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index 395f0db..f4f1337 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -22,9 +22,9 @@ use gtk::gio::ListStore; use gtk::glib; use gtk::prelude::*; use gtk::{ - Align, Box, CustomFilter, CustomSorter, FilterChange, FilterListModel, Label, NoSelection, - Orientation, SortListModel, Stack, StackTransitionType, StringFilter, StringFilterMatchMode, - ToggleButton, + Align, Box, CheckButton, CustomFilter, CustomSorter, FilterChange, FilterListModel, Label, + MenuButton, NoSelection, Orientation, Popover, SortListModel, SorterChange, Stack, + StackTransitionType, StringFilter, StringFilterMatchMode, ToggleButton, }; use std::cell::Cell; use std::cmp::Ordering; @@ -37,7 +37,14 @@ pub fn create_achievements_view( app_unlocked_achievements_count: Rc>, application: &MainApplication, app_achievement_count_value: &Label, -) -> (Stack, ListStore, StringFilter, Arc, Box) { +) -> ( + Stack, + ListStore, + StringFilter, + Arc, + Box, + MenuButton, +) { let app_achievements_model = ListStore::new::(); let app_timed_achievements_model = ListStore::new::(); @@ -113,22 +120,94 @@ pub fn create_achievements_view( .filter(&app_achievement_string_filter) .build(); - let global_achieved_percent_sorter = CustomSorter::new(move |obj1, obj2| { - let achievement1 = obj1.downcast_ref::().unwrap(); - let achievement2 = obj2.downcast_ref::().unwrap(); - - let percent1 = achievement1.global_achieved_percent(); - let percent2 = achievement2.global_achieved_percent(); - - percent2 - .partial_cmp(&percent1) - .unwrap_or(Ordering::Equal) - .into() + // 0 = Steam's source order, 1 = name, 2 = global percentage, + // 3 = most recently unlocked. + let achievement_order = Rc::new(Cell::new(2u8)); + let achievement_sorter = CustomSorter::new({ + let achievement_order = Rc::clone(&achievement_order); + move |obj1, obj2| { + let achievement1 = obj1.downcast_ref::().unwrap(); + let achievement2 = obj2.downcast_ref::().unwrap(); + match achievement_order.get() { + 0 => Ordering::Equal.into(), + 1 => achievement1 + .name() + .to_lowercase() + .cmp(&achievement2.name().to_lowercase()) + .into(), + 3 => achievement2 + .unlock_time_seconds() + .cmp(&achievement1.unlock_time_seconds()) + .into(), + _ => achievement2 + .global_achieved_percent() + .partial_cmp(&achievement1.global_achieved_percent()) + .unwrap_or(Ordering::Equal) + .into(), + } + } }); let app_achievement_sort_model = SortListModel::builder() .model(&app_achievement_status_filter_model) - .sorter(&global_achieved_percent_sorter) + .sorter(&achievement_sorter) + .build(); + + let order_popover_box = Box::builder() + .orientation(Orientation::Vertical) + .margin_start(8) + .margin_end(8) + .margin_top(8) + .margin_bottom(8) + .build(); + let steam_order = CheckButton::builder() + .label(tr("Steam default").as_str()) + .build(); + let alphabetical_order = CheckButton::builder() + .label(tr("Alphabetically").as_str()) + .group(&steam_order) + .build(); + let percentage_order = CheckButton::builder() + .label(tr("Global percentage").as_str()) + .group(&steam_order) + .build(); + let unlock_date_order = CheckButton::builder() + .label(tr("Unlock date").as_str()) + .group(&steam_order) + .build(); + percentage_order.set_active(true); + order_popover_box.append(&steam_order); + order_popover_box.append(&alphabetical_order); + order_popover_box.append(&percentage_order); + order_popover_box.append(&unlock_date_order); + let order_popover = Popover::builder().child(&order_popover_box).build(); + let order_button = MenuButton::builder() + .icon_name("view-sort-descending-symbolic") + .tooltip_text(tr("Order achievements").as_str()) + .popover(&order_popover) + .visible(false) .build(); + for (button, order) in [ + (&steam_order, 0), + (&alphabetical_order, 1), + (&percentage_order, 2), + (&unlock_date_order, 3), + ] { + button.connect_toggled(glib::clone!( + #[strong] + achievement_order, + #[weak] + achievement_sorter, + #[weak] + order_popover, + move |button| { + if button.is_active() { + achievement_order.set(order); + achievement_sorter.changed(SorterChange::Different); + order_popover.popdown(); + } + } + )); + } let app_achievement_selection_model = NoSelection::new(Option::::None); app_achievement_selection_model.set_model(Some(&app_achievement_sort_model)); @@ -161,5 +240,6 @@ pub fn create_achievements_view( app_achievement_string_filter, cancel_timed_unlock, status_filter_box, + order_button, ) } diff --git a/src/gui_frontend/app_list_view/mod.rs b/src/gui_frontend/app_list_view/mod.rs index f3728a4..696b053 100644 --- a/src/gui_frontend/app_list_view/mod.rs +++ b/src/gui_frontend/app_list_view/mod.rs @@ -411,6 +411,7 @@ pub fn create_main_ui( cancel_timed_unlock, app_achievements_stack, achievement_status_filter_box, + achievement_order_button, ) = create_app_view( app_id.clone(), app_unlocked_achievements_count.clone(), @@ -483,6 +484,7 @@ pub fn create_main_ui( header_bar.pack_start(&back_button); header_bar.pack_start(&sidebar_button); header_bar.pack_start(&search_entry); + header_bar.pack_start(&achievement_order_button); header_bar.pack_end(&context_menu_button); header_bar.pack_end(&context_menu_button_loading); header_bar.pack_end(&achievement_status_filter_box); @@ -1715,11 +1717,14 @@ pub fn create_main_ui( list_stack, #[weak] achievement_status_filter_box, + #[weak] + achievement_order_button, move |stack| { - achievement_status_filter_box.set_visible( - list_stack.visible_child_name().as_deref() == Some("app") - && stack.visible_child_name().as_deref() == Some("achievements"), - ); + let show_achievement_controls = list_stack.visible_child_name().as_deref() + == Some("app") + && stack.visible_child_name().as_deref() == Some("achievements"); + achievement_status_filter_box.set_visible(show_achievement_controls); + achievement_order_button.set_visible(show_achievement_controls); } )); @@ -1738,6 +1743,8 @@ pub fn create_main_ui( action_refresh_app_list, #[weak] achievement_status_filter_box, + #[weak] + achievement_order_button, #[strong] prefetched_progress, #[strong] @@ -1746,10 +1753,10 @@ pub fn create_main_ui( let page = stack.visible_child_name(); let page = page.as_deref(); let on_own_page = page == Some("app") || page == Some("profile"); - achievement_status_filter_box.set_visible( - page == Some("app") - && app_stack.visible_child_name().as_deref() == Some("achievements"), - ); + let show_achievement_controls = page == Some("app") + && app_stack.visible_child_name().as_deref() == Some("achievements"); + achievement_status_filter_box.set_visible(show_achievement_controls); + achievement_order_button.set_visible(show_achievement_controls); sidebar_button.set_visible(!on_own_page); sidebar_button.set_sensitive(page == Some("list")); search_entry.set_sensitive(page != Some("profile")); diff --git a/src/gui_frontend/app_view.rs b/src/gui_frontend/app_view.rs index f1656ef..ef4657f 100644 --- a/src/gui_frontend/app_view.rs +++ b/src/gui_frontend/app_view.rs @@ -24,8 +24,8 @@ use gtk::glib::clone; use gtk::pango::{EllipsizeMode, WrapMode}; use gtk::prelude::*; use gtk::{ - Align, Box, Label, Orientation, Separator, Spinner, Stack, StackTransitionType, StringFilter, - ToggleButton, + Align, Box, Label, MenuButton, Orientation, Separator, Spinner, Stack, StackTransitionType, + StringFilter, ToggleButton, }; use std::cell::Cell; use std::rc::Rc; @@ -58,6 +58,7 @@ pub fn create_app_view( Arc, Stack, Box, + MenuButton, ) { let app_spinner = Spinner::builder().spinning(true).margin_end(5).build(); let app_spinner_label = Label::builder().label(tr("Loading...").as_str()).build(); @@ -228,6 +229,7 @@ pub fn create_app_view( app_achievement_string_filter, cancel_timed_unlock, achievement_status_filter_box, + achievement_order_button, ) = create_achievements_view( app_id.clone(), app_unlocked_achievements_count, @@ -340,5 +342,6 @@ pub fn create_app_view( cancel_timed_unlock, app_achievements_stack, achievement_status_filter_box, + achievement_order_button, ) } diff --git a/src/gui_frontend/gobjects/achievement.rs b/src/gui_frontend/gobjects/achievement.rs index f148b91..aa2ef4b 100644 --- a/src/gui_frontend/gobjects/achievement.rs +++ b/src/gui_frontend/gobjects/achievement.rs @@ -15,6 +15,7 @@ use glib::Object; use gtk::glib; +use std::time::UNIX_EPOCH; use crate::backend::stat_definitions::AchievementInfo; @@ -26,6 +27,10 @@ impl GAchievementObject { pub fn new(info: AchievementInfo) -> Self { let global_achieved_percent = info.global_achieved_percent.unwrap_or(0.0); let global_achieved_percent_ok = info.global_achieved_percent.is_some(); + let unlock_time_seconds = info + .unlock_time + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map_or(0, |duration| duration.as_secs()); Object::builder() .property("search-text", format!("{} {}", info.name, info.description)) @@ -37,6 +42,7 @@ impl GAchievementObject { "unlock-time", info.unlock_time.map(|time| format!("{time:#?}")), ) + .property("unlock-time-seconds", unlock_time_seconds) .property("icon-normal", info.icon_normal) .property("icon-locked", info.icon_locked) .property("permission", info.permission) @@ -76,6 +82,9 @@ mod imp { #[property(get, set)] unlock_time: RefCell>, + #[property(get, set)] + unlock_time_seconds: Cell, + #[property(get, set)] icon_normal: RefCell, From 1033815ffc1e178e2f7ece8e647e495e5b48e224 Mon Sep 17 00:00:00 2001 From: antoniohauren Date: Thu, 3 Sep 2026 14:55:43 -0300 Subject: [PATCH 3/7] Add tab-specific achievement and stat ordering --- po/samrewritten.pot | 164 +++++++++++++------------ src/gui_frontend/achievement_view.rs | 169 +++++++++----------------- src/gui_frontend/app_list_view/mod.rs | 55 ++++----- src/gui_frontend/app_view.rs | 13 +- src/gui_frontend/stat_view.rs | 64 ++++++++-- src/gui_frontend/ui_components.rs | 51 +++++++- 6 files changed, 276 insertions(+), 240 deletions(-) diff --git a/po/samrewritten.pot b/po/samrewritten.pot index 42b3dfc..867588a 100644 --- a/po/samrewritten.pot +++ b/po/samrewritten.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: SamRewritten\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 11:39-0300\n" +"POT-Creation-Date: 2026-09-03 14:52-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,104 +17,144 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/gui_frontend/ui_components.rs:159 src/gui_frontend/app_view.rs:64 -#: src/gui_frontend/app_list_view/mod.rs:423 +#: src/gui_frontend/ui_components.rs:160 src/gui_frontend/app_view.rs:62 +#: src/gui_frontend/app_list_view/mod.rs:421 msgid "Loading..." msgstr "" -#: src/gui_frontend/ui_components.rs:190 +#: src/gui_frontend/ui_components.rs:191 msgid "Select all visible apps" msgstr "" -#: src/gui_frontend/ui_components.rs:194 +#: src/gui_frontend/ui_components.rs:195 msgid "Deselect all apps" msgstr "" -#: src/gui_frontend/ui_components.rs:198 +#: src/gui_frontend/ui_components.rs:199 msgid "Unlock all in selection" msgstr "" -#: src/gui_frontend/ui_components.rs:202 +#: src/gui_frontend/ui_components.rs:203 msgid "Reset all in selection" msgstr "" -#: src/gui_frontend/ui_components.rs:206 +#: src/gui_frontend/ui_components.rs:207 #: src/gui_frontend/app_list_view/progress_actions.rs:116 msgid "Export selected apps progress" msgstr "" -#: src/gui_frontend/ui_components.rs:210 +#: src/gui_frontend/ui_components.rs:211 msgid "Import progress..." msgstr "" -#: src/gui_frontend/ui_components.rs:216 +#: src/gui_frontend/ui_components.rs:217 msgid "Appear in-game while managing" msgstr "" -#: src/gui_frontend/ui_components.rs:222 +#: src/gui_frontend/ui_components.rs:223 msgid "Refresh app list" msgstr "" -#: src/gui_frontend/ui_components.rs:226 +#: src/gui_frontend/ui_components.rs:227 msgid "Refresh achievement counts" msgstr "" -#: src/gui_frontend/ui_components.rs:229 src/gui_frontend/ui_components.rs:413 +#: src/gui_frontend/ui_components.rs:230 src/gui_frontend/ui_components.rs:414 msgid "About" msgstr "" -#: src/gui_frontend/ui_components.rs:233 +#: src/gui_frontend/ui_components.rs:234 msgid "Change Steam folder…" msgstr "" -#: src/gui_frontend/ui_components.rs:240 +#: src/gui_frontend/ui_components.rs:241 msgid "Bulk process" msgstr "" -#: src/gui_frontend/ui_components.rs:246 +#: src/gui_frontend/ui_components.rs:247 msgid "System" msgstr "" -#: src/gui_frontend/ui_components.rs:247 +#: src/gui_frontend/ui_components.rs:248 msgid "Light" msgstr "" -#: src/gui_frontend/ui_components.rs:248 +#: src/gui_frontend/ui_components.rs:249 msgid "Dark" msgstr "" -#: src/gui_frontend/ui_components.rs:257 +#: src/gui_frontend/ui_components.rs:258 msgid "Disable animations" msgstr "" -#: src/gui_frontend/ui_components.rs:261 +#: src/gui_frontend/ui_components.rs:262 msgid "Appearance" msgstr "" -#: src/gui_frontend/ui_components.rs:267 +#: src/gui_frontend/ui_components.rs:268 msgid "System default" msgstr "" -#: src/gui_frontend/ui_components.rs:270 +#: src/gui_frontend/ui_components.rs:271 msgid "Language" msgstr "" -#: src/gui_frontend/ui_components.rs:387 +#: src/gui_frontend/ui_components.rs:388 msgid "Game default" msgstr "" -#: src/gui_frontend/ui_components.rs:406 +#: src/gui_frontend/ui_components.rs:407 msgid "Refresh achievements & stats" msgstr "" -#: src/gui_frontend/ui_components.rs:410 +#: src/gui_frontend/ui_components.rs:411 msgid "Reset everything" msgstr "" -#: src/gui_frontend/ui_components.rs:418 +#: src/gui_frontend/ui_components.rs:432 src/gui_frontend/ui_components.rs:449 +msgid "Steam default" +msgstr "" + +#: src/gui_frontend/ui_components.rs:433 src/gui_frontend/ui_components.rs:450 +msgid "Alphabetically" +msgstr "" + +#: src/gui_frontend/ui_components.rs:439 +msgid "Stat order" +msgstr "" + +#: src/gui_frontend/ui_components.rs:444 msgid "Achievement language" msgstr "" +#: src/gui_frontend/ui_components.rs:451 +msgid "Global percentage" +msgstr "" + +#: src/gui_frontend/ui_components.rs:452 +msgid "Unlock date" +msgstr "" + +#: src/gui_frontend/ui_components.rs:458 +msgid "Achievement order" +msgstr "" + +#: src/gui_frontend/ui_components.rs:462 +msgid "All" +msgstr "" + +#: src/gui_frontend/ui_components.rs:463 +msgid "Locked" +msgstr "" + +#: src/gui_frontend/ui_components.rs:464 +msgid "Unlocked" +msgstr "" + +#: src/gui_frontend/ui_components.rs:470 +msgid "Achievement state" +msgstr "" + #: src/gui_frontend/dialogs.rs:89 src/gui_frontend/dialogs.rs:162 #: src/gui_frontend/dialogs.rs:185 src/gui_frontend/dialogs.rs:233 #: src/gui_frontend/unlock_scheduler.rs:185 @@ -234,51 +274,51 @@ msgstr "" msgid "Select your Steam folder" msgstr "" -#: src/gui_frontend/app_view.rs:70 +#: src/gui_frontend/app_view.rs:68 msgid "Achievements:" msgstr "" -#: src/gui_frontend/app_view.rs:84 +#: src/gui_frontend/app_view.rs:82 msgid "Stats:" msgstr "" -#: src/gui_frontend/app_view.rs:98 +#: src/gui_frontend/app_view.rs:96 msgid "Last playtime:" msgstr "" -#: src/gui_frontend/app_view.rs:112 +#: src/gui_frontend/app_view.rs:110 msgid "Type:" msgstr "" -#: src/gui_frontend/app_view.rs:126 +#: src/gui_frontend/app_view.rs:124 msgid "Developer:" msgstr "" -#: src/gui_frontend/app_view.rs:143 +#: src/gui_frontend/app_view.rs:141 msgid "Metacritic:" msgstr "" -#: src/gui_frontend/app_view.rs:157 +#: src/gui_frontend/app_view.rs:155 msgid "Failed to load app." msgstr "" -#: src/gui_frontend/app_view.rs:163 +#: src/gui_frontend/app_view.rs:161 msgid "No entries found." msgstr "" -#: src/gui_frontend/app_view.rs:180 +#: src/gui_frontend/app_view.rs:178 msgid "Achievements" msgstr "" -#: src/gui_frontend/app_view.rs:183 +#: src/gui_frontend/app_view.rs:181 msgid "Stats" msgstr "" -#: src/gui_frontend/stat_view.rs:102 +#: src/gui_frontend/stat_view.rs:152 msgid "Increment only" msgstr "" -#: src/gui_frontend/stat_view.rs:107 +#: src/gui_frontend/stat_view.rs:157 msgid "This statistic is protected." msgstr "" @@ -286,38 +326,6 @@ msgstr "" msgid "Stop and go back" msgstr "" -#: src/gui_frontend/achievement_view.rs:78 -msgid "All" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:82 -msgid "Locked" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:87 -msgid "Unlocked" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:163 -msgid "Steam default" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:166 -msgid "Alphabetically" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:170 -msgid "Global percentage" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:174 -msgid "Unlock date" -msgstr "" - -#: src/gui_frontend/achievement_view.rs:185 -msgid "Order achievements" -msgstr "" - #: src/gui_frontend/achievement_manual_view/mod.rs:57 msgid "No achievements staged" msgstr "" @@ -453,33 +461,33 @@ msgstr "" msgid "Clear selected user" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:444 +#: src/gui_frontend/app_list_view/mod.rs:442 msgid "SamRewritten could not connect to Steam. Is it running?" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:449 +#: src/gui_frontend/app_list_view/mod.rs:447 msgid "Try again" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:466 -#: src/gui_frontend/app_list_view/mod.rs:1778 +#: src/gui_frontend/app_list_view/mod.rs:464 +#: src/gui_frontend/app_list_view/mod.rs:1769 msgid "Name or AppId (Ctrl+K)" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:482 +#: src/gui_frontend/app_list_view/mod.rs:499 msgid "Show or hide the sidebar" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1773 +#: src/gui_frontend/app_list_view/mod.rs:1764 msgid "Achievement or stat..." msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1892 +#: src/gui_frontend/app_list_view/mod.rs:1883 #: src/gui_frontend/app_list_view/settings_bindings.rs:328 msgid "Could not change the in-game setting" msgstr "" -#: src/gui_frontend/app_list_view/mod.rs:1894 +#: src/gui_frontend/app_list_view/mod.rs:1885 #: src/gui_frontend/app_list_view/settings_bindings.rs:329 msgid "The change could not be applied. Restart SamRewritten and try again." msgstr "" diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index f4f1337..2d35efb 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -17,14 +17,12 @@ use crate::gui_frontend::MainApplication; use crate::gui_frontend::achievement_automatic_view::create_achievements_automatic_view; use crate::gui_frontend::achievement_manual_view::create_achievements_manual_view; use crate::gui_frontend::gobjects::achievement::GAchievementObject; -use crate::gui_frontend::i18n::tr; -use gtk::gio::ListStore; +use gtk::gio::{ListStore, SimpleAction}; use gtk::glib; use gtk::prelude::*; use gtk::{ - Align, Box, CheckButton, CustomFilter, CustomSorter, FilterChange, FilterListModel, Label, - MenuButton, NoSelection, Orientation, Popover, SortListModel, SorterChange, Stack, - StackTransitionType, StringFilter, StringFilterMatchMode, ToggleButton, + CustomFilter, CustomSorter, FilterChange, FilterListModel, Label, NoSelection, SortListModel, + SorterChange, Stack, StackTransitionType, StringFilter, StringFilterMatchMode, }; use std::cell::Cell; use std::cmp::Ordering; @@ -37,14 +35,7 @@ pub fn create_achievements_view( app_unlocked_achievements_count: Rc>, application: &MainApplication, app_achievement_count_value: &Label, -) -> ( - Stack, - ListStore, - StringFilter, - Arc, - Box, - MenuButton, -) { +) -> (Stack, ListStore, StringFilter, Arc) { let app_achievements_model = ListStore::new::(); let app_timed_achievements_model = ListStore::new::(); @@ -73,48 +64,6 @@ pub fn create_achievements_view( .model(&app_achievement_filter_model) .filter(&achievement_status_filter) .build(); - let all_toggle = ToggleButton::builder() - .icon_name("object-select-symbolic") - .tooltip_text(tr("All").as_str()) - .build(); - let locked_toggle = ToggleButton::builder() - .icon_name("changes-prevent-symbolic") - .tooltip_text(tr("Locked").as_str()) - .group(&all_toggle) - .build(); - let unlocked_toggle = ToggleButton::builder() - .icon_name("changes-allow-symbolic") - .tooltip_text(tr("Unlocked").as_str()) - .group(&all_toggle) - .build(); - all_toggle.set_active(true); - let status_filter_box = Box::builder() - .orientation(Orientation::Horizontal) - .css_classes(["linked"]) - .valign(Align::Center) - .visible(false) - .build(); - status_filter_box.append(&all_toggle); - status_filter_box.append(&locked_toggle); - status_filter_box.append(&unlocked_toggle); - for (button, state) in [ - (&all_toggle, None), - (&locked_toggle, Some(false)), - (&unlocked_toggle, Some(true)), - ] { - button.connect_toggled(glib::clone!( - #[strong] - achievement_status, - #[weak] - achievement_status_filter, - move |button| { - if button.is_active() { - achievement_status.set(state); - achievement_status_filter.changed(FilterChange::Different); - } - } - )); - } let app_achievement_timed_filter_model = FilterListModel::builder() .model(&app_timed_achievements_model) .filter(&app_achievement_string_filter) @@ -152,62 +101,58 @@ pub fn create_achievements_view( .sorter(&achievement_sorter) .build(); - let order_popover_box = Box::builder() - .orientation(Orientation::Vertical) - .margin_start(8) - .margin_end(8) - .margin_top(8) - .margin_bottom(8) - .build(); - let steam_order = CheckButton::builder() - .label(tr("Steam default").as_str()) - .build(); - let alphabetical_order = CheckButton::builder() - .label(tr("Alphabetically").as_str()) - .group(&steam_order) - .build(); - let percentage_order = CheckButton::builder() - .label(tr("Global percentage").as_str()) - .group(&steam_order) - .build(); - let unlock_date_order = CheckButton::builder() - .label(tr("Unlock date").as_str()) - .group(&steam_order) - .build(); - percentage_order.set_active(true); - order_popover_box.append(&steam_order); - order_popover_box.append(&alphabetical_order); - order_popover_box.append(&percentage_order); - order_popover_box.append(&unlock_date_order); - let order_popover = Popover::builder().child(&order_popover_box).build(); - let order_button = MenuButton::builder() - .icon_name("view-sort-descending-symbolic") - .tooltip_text(tr("Order achievements").as_str()) - .popover(&order_popover) - .visible(false) - .build(); - for (button, order) in [ - (&steam_order, 0), - (&alphabetical_order, 1), - (&percentage_order, 2), - (&unlock_date_order, 3), - ] { - button.connect_toggled(glib::clone!( - #[strong] - achievement_order, - #[weak] - achievement_sorter, - #[weak] - order_popover, - move |button| { - if button.is_active() { - achievement_order.set(order); - achievement_sorter.changed(SorterChange::Different); - order_popover.popdown(); - } - } - )); - } + let order_action = SimpleAction::new_stateful( + "achievement-order", + Some(&String::static_variant_type()), + &"global-percentage".to_variant(), + ); + order_action.connect_activate(glib::clone!( + #[strong] + achievement_order, + #[weak] + achievement_sorter, + move |action, target| { + let Some(value) = target.and_then(|target| target.str()) else { + return; + }; + let order = match value { + "steam-default" => 0, + "alphabetical" => 1, + "unlock-date" => 3, + _ => 2, + }; + action.set_state(&value.to_variant()); + achievement_order.set(order); + achievement_sorter.changed(SorterChange::Different); + } + )); + application.add_action(&order_action); + + let state_action = SimpleAction::new_stateful( + "achievement-state", + Some(&String::static_variant_type()), + &"all".to_variant(), + ); + state_action.connect_activate(glib::clone!( + #[strong] + achievement_status, + #[weak] + achievement_status_filter, + move |action, target| { + let Some(value) = target.and_then(|target| target.str()) else { + return; + }; + let state = match value { + "locked" => Some(false), + "unlocked" => Some(true), + _ => None, + }; + action.set_state(&value.to_variant()); + achievement_status.set(state); + achievement_status_filter.changed(FilterChange::Different); + } + )); + application.add_action(&state_action); let app_achievement_selection_model = NoSelection::new(Option::::None); app_achievement_selection_model.set_model(Some(&app_achievement_sort_model)); @@ -239,7 +184,5 @@ pub fn create_achievements_view( app_achievements_model, app_achievement_string_filter, cancel_timed_unlock, - status_filter_box, - order_button, ) } diff --git a/src/gui_frontend/app_list_view/mod.rs b/src/gui_frontend/app_list_view/mod.rs index 696b053..d03a0ae 100644 --- a/src/gui_frontend/app_list_view/mod.rs +++ b/src/gui_frontend/app_list_view/mod.rs @@ -43,8 +43,8 @@ use crate::gui_frontend::request::{ AppProgress, GetCollections, GetRunningApps, LaunchApp, Request, SetStealthMode, StopApp, }; use crate::gui_frontend::ui_components::{ - create_context_menu_button, set_context_popover_to_app_list_context, - set_context_popover_to_profile_context, + create_context_menu_button, set_app_details_view_options, + set_context_popover_to_app_list_context, set_context_popover_to_profile_context, }; use crate::gui_frontend::widgets::steam_app_card::{CARD_HEIGHT, CARD_MIN_WIDTH, SteamAppCard}; use crate::utils::action_journal; @@ -393,8 +393,8 @@ pub fn create_main_ui( app_stack, app_shimmer_image, app_label, - _app_achievements_button, - _app_stats_button, + app_achievements_button, + app_stats_button, app_achievement_count_value, app_stats_count_value, app_type_value, @@ -410,8 +410,6 @@ pub fn create_main_ui( app_pane, cancel_timed_unlock, app_achievements_stack, - achievement_status_filter_box, - achievement_order_button, ) = create_app_view( app_id.clone(), app_unlocked_achievements_count.clone(), @@ -477,6 +475,25 @@ pub fn create_main_ui( context_menu_button_loading_progress_label, context_menu_button_info_label, ) = create_context_menu_button(); + app_achievements_button.connect_clicked(clone!( + #[weak] + menu_model, + move |_| set_app_details_view_options(&menu_model, true) + )); + app_stats_button.connect_clicked(clone!( + #[weak] + menu_model, + move |_| set_app_details_view_options(&menu_model, false) + )); + app_stack.connect_visible_child_name_notify(clone!( + #[weak] + menu_model, + move |stack| match stack.visible_child_name().as_deref() { + Some("achievements") => set_app_details_view_options(&menu_model, true), + Some("stats") => set_app_details_view_options(&menu_model, false), + _ => {} + } + )); let sidebar_button = ToggleButton::builder() .icon_name("sidebar-show-symbolic") .tooltip_text(tr("Show or hide the sidebar").as_str()) @@ -484,10 +501,8 @@ pub fn create_main_ui( header_bar.pack_start(&back_button); header_bar.pack_start(&sidebar_button); header_bar.pack_start(&search_entry); - header_bar.pack_start(&achievement_order_button); header_bar.pack_end(&context_menu_button); header_bar.pack_end(&context_menu_button_loading); - header_bar.pack_end(&achievement_status_filter_box); let list_scrolled_window = ScrolledWindow::builder() .hscrollbar_policy(PolicyType::Never) @@ -1712,22 +1727,6 @@ pub fn create_main_ui( &app_stack, ); - app_stack.connect_visible_child_name_notify(clone!( - #[weak] - list_stack, - #[weak] - achievement_status_filter_box, - #[weak] - achievement_order_button, - move |stack| { - let show_achievement_controls = list_stack.visible_child_name().as_deref() - == Some("app") - && stack.visible_child_name().as_deref() == Some("achievements"); - achievement_status_filter_box.set_visible(show_achievement_controls); - achievement_order_button.set_visible(show_achievement_controls); - } - )); - list_stack.connect_visible_child_notify(clone!( #[weak] back_button, @@ -1741,10 +1740,6 @@ pub fn create_main_ui( search_entry, #[weak] action_refresh_app_list, - #[weak] - achievement_status_filter_box, - #[weak] - achievement_order_button, #[strong] prefetched_progress, #[strong] @@ -1753,10 +1748,6 @@ pub fn create_main_ui( let page = stack.visible_child_name(); let page = page.as_deref(); let on_own_page = page == Some("app") || page == Some("profile"); - let show_achievement_controls = page == Some("app") - && app_stack.visible_child_name().as_deref() == Some("achievements"); - achievement_status_filter_box.set_visible(show_achievement_controls); - achievement_order_button.set_visible(show_achievement_controls); sidebar_button.set_visible(!on_own_page); sidebar_button.set_sensitive(page == Some("list")); search_entry.set_sensitive(page != Some("profile")); diff --git a/src/gui_frontend/app_view.rs b/src/gui_frontend/app_view.rs index ef4657f..160338b 100644 --- a/src/gui_frontend/app_view.rs +++ b/src/gui_frontend/app_view.rs @@ -24,8 +24,8 @@ use gtk::glib::clone; use gtk::pango::{EllipsizeMode, WrapMode}; use gtk::prelude::*; use gtk::{ - Align, Box, Label, MenuButton, Orientation, Separator, Spinner, Stack, StackTransitionType, - StringFilter, ToggleButton, + Align, Box, Label, Orientation, Separator, Spinner, Stack, StackTransitionType, StringFilter, + ToggleButton, }; use std::cell::Cell; use std::rc::Rc; @@ -57,8 +57,6 @@ pub fn create_app_view( Box, Arc, Stack, - Box, - MenuButton, ) { let app_spinner = Spinner::builder().spinning(true).margin_end(5).build(); let app_spinner_label = Label::builder().label(tr("Loading...").as_str()).build(); @@ -228,8 +226,6 @@ pub fn create_app_view( app_achievements_model, app_achievement_string_filter, cancel_timed_unlock, - achievement_status_filter_box, - achievement_order_button, ) = create_achievements_view( app_id.clone(), app_unlocked_achievements_count, @@ -237,7 +233,8 @@ pub fn create_app_view( &app_achievement_count_value, ); - let (app_stat_scrolled_window, app_stat_model, app_stat_string_filter) = create_stats_view(); + let (app_stat_scrolled_window, app_stat_model, app_stat_string_filter) = + create_stats_view(application); let app_stack = Stack::builder() .transition_type(StackTransitionType::SlideLeftRight) @@ -341,7 +338,5 @@ pub fn create_app_view( app_pane, cancel_timed_unlock, app_achievements_stack, - achievement_status_filter_box, - achievement_order_button, ) } diff --git a/src/gui_frontend/stat_view.rs b/src/gui_frontend/stat_view.rs index 98f3cc6..0e6fe59 100644 --- a/src/gui_frontend/stat_view.rs +++ b/src/gui_frontend/stat_view.rs @@ -15,27 +15,33 @@ use super::gobjects::stat::GStatObject; use super::request::{Request, SetFloatStat, SetIntStat}; +use crate::gui_frontend::MainApplication; use crate::gui_frontend::i18n::tr; use crate::utils::action_journal::{Batch, Change, Op}; -use gtk::gio::{ListStore, spawn_blocking}; +use gtk::gio::prelude::ActionMapExt; +use gtk::gio::{ListStore, SimpleAction, spawn_blocking}; use gtk::glib::SignalHandlerId; use gtk::glib::object::Cast; +use gtk::glib::prelude::{StaticVariantType, ToVariant}; use gtk::glib::translate::FromGlib; use gtk::pango::EllipsizeMode; use gtk::prelude::{ - BoxExt, GObjectPropertyExpressionExt, ListItemExt, ObjectExt, ToValue, WidgetExt, + BoxExt, GObjectPropertyExpressionExt, ListItemExt, ObjectExt, SorterExt, ToValue, WidgetExt, }; use gtk::{ - Adjustment, Align, Box, ClosureExpression, FilterListModel, Frame, Label, ListItem, ListView, - NoSelection, Orientation, ScrolledWindow, SignalListItemFactory, SpinButton, StringFilter, - StringFilterMatchMode, Widget, glib, + Adjustment, Align, Box, ClosureExpression, CustomSorter, FilterListModel, Frame, Label, + ListItem, ListView, NoSelection, Orientation, ScrolledWindow, SignalListItemFactory, + SortListModel, SorterChange, SpinButton, StringFilter, StringFilterMatchMode, Widget, glib, }; +use std::cell::Cell; use std::cell::RefCell; +use std::cmp::Ordering; use std::ffi::c_ulong; +use std::rc::Rc; use std::sync::mpsc::channel; use std::time::Duration; -pub fn create_stats_view() -> (Frame, ListStore, StringFilter) { +pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, StringFilter) { let stats_list_factory = SignalListItemFactory::new(); let app_stats_model = ListStore::new::(); @@ -48,8 +54,52 @@ pub fn create_stats_view() -> (Frame, ListStore, StringFilter) { .model(&app_stats_model) .filter(&app_stats_string_filter) .build(); + + // 0 keeps Steam's schema order; 1 sorts by the displayed, localized name. + let stat_order = Rc::new(Cell::new(0u8)); + let stat_sorter = CustomSorter::new({ + let stat_order = Rc::clone(&stat_order); + move |obj1, obj2| { + if stat_order.get() == 0 { + return Ordering::Equal.into(); + } + let stat1 = obj1.downcast_ref::().unwrap(); + let stat2 = obj2.downcast_ref::().unwrap(); + stat1 + .display_name() + .to_lowercase() + .cmp(&stat2.display_name().to_lowercase()) + .into() + } + }); + let app_stats_sort_model = SortListModel::builder() + .model(&app_stats_filter_model) + .sorter(&stat_sorter) + .build(); + + let order_action = SimpleAction::new_stateful( + "stat-order", + Some(&String::static_variant_type()), + &"steam-default".to_variant(), + ); + order_action.connect_activate(glib::clone!( + #[strong] + stat_order, + #[weak] + stat_sorter, + move |action, target| { + let Some(value) = target.and_then(|target| target.str()) else { + return; + }; + action.set_state(&value.to_variant()); + stat_order.set(u8::from(value == "alphabetical")); + stat_sorter.changed(SorterChange::Different); + } + )); + application.add_action(&order_action); + let app_stats_selection_model = NoSelection::new(Option::::None); - app_stats_selection_model.set_model(Some(&app_stats_filter_model)); + app_stats_selection_model.set_model(Some(&app_stats_sort_model)); let app_stats_list_view = ListView::builder() .orientation(Orientation::Vertical) diff --git a/src/gui_frontend/ui_components.rs b/src/gui_frontend/ui_components.rs index 55cc8aa..62399f6 100644 --- a/src/gui_frontend/ui_components.rs +++ b/src/gui_frontend/ui_components.rs @@ -16,6 +16,7 @@ use crate::gui_frontend::MainApplication; use crate::gui_frontend::application_actions::set_app_action_enabled; use crate::gui_frontend::i18n::{tr, tr_noop}; +use gtk::gio::prelude::MenuModelExt; use gtk::prelude::{BoxExt, SettingsExt, ToVariant}; use gtk::{Label, License, MenuButton, Popover, PopoverMenu, PositionType, Spinner}; @@ -414,9 +415,57 @@ pub fn set_context_popover_to_app_details_context( ACHIEVEMENT_LANGUAGES_FROM_FETCH.with(|f| f.set(false)); fill_achievement_language_menu(&[]); + set_app_details_view_options(menu_model, true); + + set_app_action_enabled(application, "refresh_app_list", false); +} + +/// Replace the tab-specific portion of the app-details menu. +pub fn set_app_details_view_options(menu_model: >k::gio::Menu, achievements: bool) { + while menu_model.n_items() > 3 { + menu_model.remove(3); + } + + if !achievements { + let order_menu = gtk::gio::Menu::new(); + for (label, value) in [ + (tr("Steam default"), "steam-default"), + (tr("Alphabetically"), "alphabetical"), + ] { + let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some("app.stat-order")); + item.set_action_and_target_value(Some("app.stat-order"), Some(&value.to_variant())); + order_menu.append_item(&item); + } + menu_model.append_submenu(Some(tr("Stat order").as_str()), &order_menu); + return; + } + ACHIEVEMENT_LANGUAGE_MENU.with(|menu| { menu_model.append_submenu(Some(tr("Achievement language").as_str()), menu); }); - set_app_action_enabled(application, "refresh_app_list", false); + let order_menu = gtk::gio::Menu::new(); + for (label, value) in [ + (tr("Steam default"), "steam-default"), + (tr("Alphabetically"), "alphabetical"), + (tr("Global percentage"), "global-percentage"), + (tr("Unlock date"), "unlock-date"), + ] { + let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some("app.achievement-order")); + item.set_action_and_target_value(Some("app.achievement-order"), Some(&value.to_variant())); + order_menu.append_item(&item); + } + menu_model.append_submenu(Some(tr("Achievement order").as_str()), &order_menu); + + let state_menu = gtk::gio::Menu::new(); + for (label, value) in [ + (tr("All"), "all"), + (tr("Locked"), "locked"), + (tr("Unlocked"), "unlocked"), + ] { + let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some("app.achievement-state")); + item.set_action_and_target_value(Some("app.achievement-state"), Some(&value.to_variant())); + state_menu.append_item(&item); + } + menu_model.append_submenu(Some(tr("Achievement state").as_str()), &state_menu); } From f0a7c94d1ab683395931f6c0d530482526e6d7cb Mon Sep 17 00:00:00 2001 From: antoniohauren Date: Thu, 3 Sep 2026 15:16:10 -0300 Subject: [PATCH 4/7] Fix achievement filter and ordering updates --- .../achievement_manual_view/copy_mode.rs | 11 ++- .../achievement_manual_view/mod.rs | 16 +++- .../achievement_manual_view/row_factory.rs | 26 ++++--- src/gui_frontend/achievement_view.rs | 58 ++++++++++---- src/gui_frontend/stat_view.rs | 37 ++++++++- src/gui_frontend/ui_components.rs | 77 +++++++++++-------- src/gui_frontend/unlock_scheduler.rs | 29 +++++++ 7 files changed, 189 insertions(+), 65 deletions(-) diff --git a/src/gui_frontend/achievement_manual_view/copy_mode.rs b/src/gui_frontend/achievement_manual_view/copy_mode.rs index efd2ff6..e20df9a 100644 --- a/src/gui_frontend/achievement_manual_view/copy_mode.rs +++ b/src/gui_frontend/achievement_manual_view/copy_mode.rs @@ -23,7 +23,9 @@ use crate::gui_frontend::gobjects::mode_state::{GUnlockModeState, MODE_COPY_TIMI use crate::gui_frontend::i18n::tr; use crate::gui_frontend::request::{GetFriendUnlockTimes, GetFriends, GetUserAvatar, Request}; use crate::gui_frontend::unlock_queue::UnlockQueue; -use crate::gui_frontend::unlock_scheduler::{compute_copy_timing_ms, run_timed_unlock}; +use crate::gui_frontend::unlock_scheduler::{ + AchievementModelUpdates, compute_copy_timing_ms, run_timed_unlock, +}; use crate::gui_frontend::widgets::shimmer_image::ShimmerImage; use crate::utils::action_journal::Op; use crate::utils::format::format_seconds_to_hh_mm_ss; @@ -72,6 +74,7 @@ pub(super) fn install_copy_mode( raw_model: &ListStore, timed_raw_model: &ListStore, cancelled_task: &Arc, + model_updates: &AchievementModelUpdates, achievement_views_stack: &Stack, application: &MainApplication, ) { @@ -82,6 +85,7 @@ pub(super) fn install_copy_mode( let raw_model = raw_model.clone(); let timed_raw_model = timed_raw_model.clone(); let cancelled_task = cancelled_task.clone(); + let model_updates = model_updates.clone(); let achievement_views_stack = achievement_views_stack.clone(); let application = application.clone(); @@ -389,6 +393,8 @@ pub(super) fn install_copy_mode( #[strong] cancelled_task, #[strong] + model_updates, + #[strong] timed_raw_model, #[weak(rename_to = raw_model)] raw_model, @@ -425,6 +431,8 @@ pub(super) fn install_copy_mode( timed_raw_model, #[strong] cancelled_task, + #[strong] + model_updates, async move { run_timed_unlock( app_id_val, @@ -433,6 +441,7 @@ pub(super) fn install_copy_mode( times_ms, timed_raw_model, cancelled_task, + model_updates, ) .await; } diff --git a/src/gui_frontend/achievement_manual_view/mod.rs b/src/gui_frontend/achievement_manual_view/mod.rs index 28fd65f..af4d57f 100644 --- a/src/gui_frontend/achievement_manual_view/mod.rs +++ b/src/gui_frontend/achievement_manual_view/mod.rs @@ -30,7 +30,8 @@ use crate::gui_frontend::gsettings::get_settings; use crate::gui_frontend::i18n::tr; use crate::gui_frontend::unlock_queue::{UnlockQueue, resolve_target_count}; use crate::gui_frontend::unlock_scheduler::{ - SPACING_EVEN, SPACING_RANDOM, compute_unlock_times_ms, run_timed_unlock, unlock_all_immediately, + AchievementModelUpdates, SPACING_EVEN, SPACING_RANDOM, compute_unlock_times_ms, + run_timed_unlock, unlock_all_immediately, }; use crate::utils::action_journal::Op; use crate::utils::format::format_achievement_progress; @@ -41,7 +42,7 @@ use gtk::gio::{ListStore, SimpleAction}; use gtk::glib::{MainContext, clone}; use gtk::prelude::*; use gtk::{ - Box, Button, CustomFilter, Frame, Label, ListView, NoSelection, Orientation, ScrolledWindow, + Box, Button, Frame, Label, ListView, NoSelection, Orientation, ScrolledWindow, SignalListItemFactory, SpinButton, Stack, ToggleButton, glib, }; use header::create_header; @@ -111,7 +112,7 @@ pub fn create_achievements_manual_view( app_id: &Rc>>, app_unlocked_achievements_count: &Rc>, filtered_model: &NoSelection, - status_filter: &CustomFilter, + model_updates: &AchievementModelUpdates, raw_model: &ListStore, timed_raw_model: &ListStore, achievement_views_stack: &Stack, @@ -119,6 +120,7 @@ pub fn create_achievements_manual_view( application: &MainApplication, ) -> (Frame, Arc) { let settings = get_settings(); + let model_updates = model_updates.clone(); let mode_state = Rc::new(GUnlockModeState::default()); let initial_mode = settings.string("unlock-mode").to_string(); @@ -338,6 +340,8 @@ pub fn create_achievements_manual_view( cancelled_task, #[strong] timed_raw_model, + #[strong] + model_updates, #[weak] application, #[weak(rename_to = raw_model)] @@ -387,6 +391,8 @@ pub fn create_achievements_manual_view( timed_raw_model, #[strong] cancelled_task, + #[strong] + model_updates, async move { run_timed_unlock( app_id_val, @@ -395,6 +401,7 @@ pub fn create_achievements_manual_view( times_ms, timed_raw_model, cancelled_task, + model_updates, ) .await; } @@ -414,6 +421,7 @@ pub fn create_achievements_manual_view( raw_model, timed_raw_model, &cancelled_task, + &model_updates, achievement_views_stack, application, ); @@ -431,7 +439,7 @@ pub fn create_achievements_manual_view( &header.queue_label, &cancelled_task, &update_autofill, - status_filter, + &model_updates, ); let app_achievements_list_view = ListView::builder() diff --git a/src/gui_frontend/achievement_manual_view/row_factory.rs b/src/gui_frontend/achievement_manual_view/row_factory.rs index 936243a..7441f7b 100644 --- a/src/gui_frontend/achievement_manual_view/row_factory.rs +++ b/src/gui_frontend/achievement_manual_view/row_factory.rs @@ -19,13 +19,14 @@ use crate::gui_frontend::gobjects::achievement::GAchievementObject; use crate::gui_frontend::gobjects::mode_state::GUnlockModeState; use crate::gui_frontend::request::{Request, SetAchievement}; use crate::gui_frontend::unlock_queue::UnlockQueue; +use crate::gui_frontend::unlock_scheduler::AchievementModelUpdates; use crate::gui_frontend::widgets::achievement_row::AchievementRow; use crate::utils::action_journal::{Batch, Change, Op}; use crate::utils::format::format_achievement_progress; use gtk::gio::{ListStore, spawn_blocking}; use gtk::glib::{self, MainContext, clone}; use gtk::prelude::*; -use gtk::{Button, CustomFilter, FilterChange, Label, ListItem, SignalListItemFactory}; +use gtk::{Button, Label, ListItem, SignalListItemFactory}; use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; @@ -44,7 +45,7 @@ pub(super) fn install_row_factory( queue_label: &Label, cancelled_task: &Arc, update_autofill: &Rc, - status_filter: &CustomFilter, + model_updates: &AchievementModelUpdates, ) { factory.connect_setup(clone!( #[strong] @@ -59,8 +60,8 @@ pub(super) fn install_row_factory( cancelled_task, #[strong] update_autofill, - #[weak] - status_filter, + #[strong] + model_updates, #[weak] raw_model, #[weak] @@ -96,8 +97,8 @@ pub(super) fn install_row_factory( raw_model, #[weak] start_button, - #[weak] - status_filter, + #[strong] + model_updates, move |switch| { let Some(achievement_object) = list_item.item().and_downcast::() @@ -148,14 +149,21 @@ pub(super) fn install_row_factory( achievement_object, #[weak] start_button, - #[weak] - status_filter, + #[strong] + model_updates, async move { let result = handle.await.expect("spawn_blocking task panicked"); // Steam accepting the call and then failing to // store it, which is a failure like any other. match result { Ok(true) => { + achievement_object.set_unlock_time_seconds(if unlocked { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.as_secs()) + } else { + 0 + }); Batch::new(Op::ManualToggle, app_id_val, "").record(vec![ Change::Achievement { id: achievement_object.id(), @@ -176,7 +184,7 @@ pub(super) fn install_row_factory( start_button .set_sensitive(new_unlocked != raw_model_len as usize); update_autofill(); - status_filter.changed(FilterChange::Different); + model_updates.changed(); } Ok(false) => { eprintln!("[CLIENT] Steam did not store the achievement"); diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index 2d35efb..27f48c4 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -17,6 +17,7 @@ use crate::gui_frontend::MainApplication; use crate::gui_frontend::achievement_automatic_view::create_achievements_automatic_view; use crate::gui_frontend::achievement_manual_view::create_achievements_manual_view; use crate::gui_frontend::gobjects::achievement::GAchievementObject; +use crate::gui_frontend::unlock_scheduler::AchievementModelUpdates; use gtk::gio::{ListStore, SimpleAction}; use gtk::glib; use gtk::prelude::*; @@ -30,6 +31,27 @@ use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::AtomicBool; +#[derive(Clone, Copy, Default)] +enum AchievementOrder { + SteamDefault, + Alphabetical, + #[default] + GlobalPercentage, + UnlockDate, +} + +impl AchievementOrder { + fn from_action_target(value: &str) -> Option { + match value { + "steam-default" => Some(Self::SteamDefault), + "alphabetical" => Some(Self::Alphabetical), + "global-percentage" => Some(Self::GlobalPercentage), + "unlock-date" => Some(Self::UnlockDate), + _ => None, + } + } +} + pub fn create_achievements_view( app_id: Rc>>, app_unlocked_achievements_count: Rc>, @@ -69,26 +91,24 @@ pub fn create_achievements_view( .filter(&app_achievement_string_filter) .build(); - // 0 = Steam's source order, 1 = name, 2 = global percentage, - // 3 = most recently unlocked. - let achievement_order = Rc::new(Cell::new(2u8)); + let achievement_order = Rc::new(Cell::new(AchievementOrder::default())); let achievement_sorter = CustomSorter::new({ let achievement_order = Rc::clone(&achievement_order); move |obj1, obj2| { let achievement1 = obj1.downcast_ref::().unwrap(); let achievement2 = obj2.downcast_ref::().unwrap(); match achievement_order.get() { - 0 => Ordering::Equal.into(), - 1 => achievement1 + AchievementOrder::SteamDefault => Ordering::Equal.into(), + AchievementOrder::Alphabetical => achievement1 .name() .to_lowercase() .cmp(&achievement2.name().to_lowercase()) .into(), - 3 => achievement2 + AchievementOrder::UnlockDate => achievement2 .unlock_time_seconds() .cmp(&achievement1.unlock_time_seconds()) .into(), - _ => achievement2 + AchievementOrder::GlobalPercentage => achievement2 .global_achieved_percent() .partial_cmp(&achievement1.global_achieved_percent()) .unwrap_or(Ordering::Equal) @@ -100,6 +120,8 @@ pub fn create_achievements_view( .model(&app_achievement_status_filter_model) .sorter(&achievement_sorter) .build(); + let achievement_model_updates = + AchievementModelUpdates::new(&achievement_status_filter, &achievement_sorter); let order_action = SimpleAction::new_stateful( "achievement-order", @@ -115,11 +137,8 @@ pub fn create_achievements_view( let Some(value) = target.and_then(|target| target.str()) else { return; }; - let order = match value { - "steam-default" => 0, - "alphabetical" => 1, - "unlock-date" => 3, - _ => 2, + let Some(order) = AchievementOrder::from_action_target(value) else { + return; }; action.set_state(&value.to_variant()); achievement_order.set(order); @@ -143,9 +162,10 @@ pub fn create_achievements_view( return; }; let state = match value { + "all" => None, "locked" => Some(false), "unlocked" => Some(true), - _ => None, + _ => return, }; action.set_state(&value.to_variant()); achievement_status.set(state); @@ -166,7 +186,7 @@ pub fn create_achievements_view( &app_id, &app_unlocked_achievements_count, &app_achievement_selection_model, - &achievement_status_filter, + &achievement_model_updates, &app_achievements_model, &app_timed_achievements_model, &achievement_views_stack, @@ -186,3 +206,13 @@ pub fn create_achievements_view( cancel_timed_unlock, ) } + +#[cfg(test)] +mod tests { + use super::AchievementOrder; + + #[test] + fn achievement_order_rejects_unknown_action_targets() { + assert!(AchievementOrder::from_action_target("unknown").is_none()); + } +} diff --git a/src/gui_frontend/stat_view.rs b/src/gui_frontend/stat_view.rs index 0e6fe59..956f0a1 100644 --- a/src/gui_frontend/stat_view.rs +++ b/src/gui_frontend/stat_view.rs @@ -41,6 +41,23 @@ use std::rc::Rc; use std::sync::mpsc::channel; use std::time::Duration; +#[derive(Clone, Copy, Default)] +enum StatOrder { + #[default] + SteamDefault, + Alphabetical, +} + +impl StatOrder { + fn from_action_target(value: &str) -> Option { + match value { + "steam-default" => Some(Self::SteamDefault), + "alphabetical" => Some(Self::Alphabetical), + _ => None, + } + } +} + pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, StringFilter) { let stats_list_factory = SignalListItemFactory::new(); let app_stats_model = ListStore::new::(); @@ -55,12 +72,11 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St .filter(&app_stats_string_filter) .build(); - // 0 keeps Steam's schema order; 1 sorts by the displayed, localized name. - let stat_order = Rc::new(Cell::new(0u8)); + let stat_order = Rc::new(Cell::new(StatOrder::default())); let stat_sorter = CustomSorter::new({ let stat_order = Rc::clone(&stat_order); move |obj1, obj2| { - if stat_order.get() == 0 { + if matches!(stat_order.get(), StatOrder::SteamDefault) { return Ordering::Equal.into(); } let stat1 = obj1.downcast_ref::().unwrap(); @@ -91,8 +107,11 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St let Some(value) = target.and_then(|target| target.str()) else { return; }; + let Some(order) = StatOrder::from_action_target(value) else { + return; + }; action.set_state(&value.to_variant()); - stat_order.set(u8::from(value == "alphabetical")); + stat_order.set(order); stat_sorter.changed(SorterChange::Different); } )); @@ -416,3 +435,13 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St (app_stats_frame, app_stats_model, app_stats_string_filter) } + +#[cfg(test)] +mod tests { + use super::StatOrder; + + #[test] + fn stat_order_rejects_unknown_action_targets() { + assert!(StatOrder::from_action_target("unknown").is_none()); + } +} diff --git a/src/gui_frontend/ui_components.rs b/src/gui_frontend/ui_components.rs index 62399f6..258f49c 100644 --- a/src/gui_frontend/ui_components.rs +++ b/src/gui_frontend/ui_components.rs @@ -427,16 +427,15 @@ pub fn set_app_details_view_options(menu_model: >k::gio::Menu, achievements: b } if !achievements { - let order_menu = gtk::gio::Menu::new(); - for (label, value) in [ - (tr("Steam default"), "steam-default"), - (tr("Alphabetically"), "alphabetical"), - ] { - let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some("app.stat-order")); - item.set_action_and_target_value(Some("app.stat-order"), Some(&value.to_variant())); - order_menu.append_item(&item); - } - menu_model.append_submenu(Some(tr("Stat order").as_str()), &order_menu); + append_target_submenu( + menu_model, + tr("Stat order"), + "app.stat-order", + [ + (tr("Steam default"), "steam-default"), + (tr("Alphabetically"), "alphabetical"), + ], + ); return; } @@ -444,28 +443,40 @@ pub fn set_app_details_view_options(menu_model: >k::gio::Menu, achievements: b menu_model.append_submenu(Some(tr("Achievement language").as_str()), menu); }); - let order_menu = gtk::gio::Menu::new(); - for (label, value) in [ - (tr("Steam default"), "steam-default"), - (tr("Alphabetically"), "alphabetical"), - (tr("Global percentage"), "global-percentage"), - (tr("Unlock date"), "unlock-date"), - ] { - let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some("app.achievement-order")); - item.set_action_and_target_value(Some("app.achievement-order"), Some(&value.to_variant())); - order_menu.append_item(&item); - } - menu_model.append_submenu(Some(tr("Achievement order").as_str()), &order_menu); - - let state_menu = gtk::gio::Menu::new(); - for (label, value) in [ - (tr("All"), "all"), - (tr("Locked"), "locked"), - (tr("Unlocked"), "unlocked"), - ] { - let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some("app.achievement-state")); - item.set_action_and_target_value(Some("app.achievement-state"), Some(&value.to_variant())); - state_menu.append_item(&item); + append_target_submenu( + menu_model, + tr("Achievement order"), + "app.achievement-order", + [ + (tr("Steam default"), "steam-default"), + (tr("Alphabetically"), "alphabetical"), + (tr("Global percentage"), "global-percentage"), + (tr("Unlock date"), "unlock-date"), + ], + ); + append_target_submenu( + menu_model, + tr("Achievement state"), + "app.achievement-state", + [ + (tr("All"), "all"), + (tr("Locked"), "locked"), + (tr("Unlocked"), "unlocked"), + ], + ); +} + +fn append_target_submenu( + menu_model: >k::gio::Menu, + label: gtk::glib::GString, + action: &str, + entries: [(gtk::glib::GString, &'static str); N], +) { + let submenu = gtk::gio::Menu::new(); + for (label, value) in entries { + let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some(action)); + item.set_action_and_target_value(Some(action), Some(&value.to_variant())); + submenu.append_item(&item); } - menu_model.append_submenu(Some(tr("Achievement state").as_str()), &state_menu); + menu_model.append_submenu(Some(label.as_str()), &submenu); } diff --git a/src/gui_frontend/unlock_scheduler.rs b/src/gui_frontend/unlock_scheduler.rs index c77e7e3..1f3e781 100644 --- a/src/gui_frontend/unlock_scheduler.rs +++ b/src/gui_frontend/unlock_scheduler.rs @@ -21,6 +21,8 @@ use crate::utils::action_journal::{Batch, Change, Op}; use crate::utils::format::format_seconds_to_hh_mm_ss; use gtk::gio::{ListStore, spawn_blocking}; use gtk::glib; +use gtk::prelude::{FilterExt, SorterExt}; +use gtk::{CustomFilter, CustomSorter, FilterChange, SorterChange}; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -29,6 +31,26 @@ pub const SPACING_RANDOM: &str = "random"; const MIN_GAP_MS: u64 = 2_000; +#[derive(Clone)] +pub struct AchievementModelUpdates { + status_filter: CustomFilter, + sorter: CustomSorter, +} + +impl AchievementModelUpdates { + pub fn new(status_filter: &CustomFilter, sorter: &CustomSorter) -> Self { + Self { + status_filter: status_filter.clone(), + sorter: sorter.clone(), + } + } + + pub fn changed(&self) { + self.status_filter.changed(FilterChange::Different); + self.sorter.changed(SorterChange::Different); + } +} + pub fn compute_unlock_times_ms(count: usize, total_ms: u64, spacing: &str) -> Vec { if count == 0 { return Vec::new(); @@ -97,6 +119,7 @@ pub async fn run_timed_unlock( times_ms: Vec, timed_raw_model: ListStore, cancelled: Arc, + model_updates: AchievementModelUpdates, ) { debug_assert_eq!(achievements.len(), times_ms.len()); if achievements.is_empty() { @@ -152,6 +175,11 @@ pub async fn run_timed_unlock( // Only `Ok(Ok(true))` is Steam saying it stored the thing. match result { Ok(Ok(true)) => { + achievement.set_unlock_time_seconds( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.as_secs()), + ); batch.record(vec![Change::Achievement { id: achievement.id(), name: achievement.name(), @@ -174,6 +202,7 @@ pub async fn run_timed_unlock( achievement.set_is_achieved(false); } } + model_updates.changed(); next_index += 1; } From 96bd422458f029f6cd4b534455872ec9d4b3afe6 Mon Sep 17 00:00:00 2001 From: PaulCombal Date: Thu, 3 Sep 2026 21:59:36 +0200 Subject: [PATCH 5/7] code review --- .github/workflows/check.yml | 4 +- po/POTFILES.in | 1 - po/fr.po | 185 +++++++++++------- po/samrewritten.pot | 68 +++---- .../achievement_manual_view/row_factory.rs | 2 +- src/gui_frontend/achievement_view.rs | 11 +- src/gui_frontend/stat_view.rs | 44 ++--- src/gui_frontend/ui_components.rs | 79 ++++---- 8 files changed, 216 insertions(+), 178 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7ce64e4..1a6623f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,10 +17,10 @@ jobs: - name: Cache cargo registry and target uses: Swatinem/rust-cache@v2 - - name: Install GTK dependencies + - name: Install GTK and gettext dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-4-dev libadwaita-1-dev + sudo apt-get install -y libgtk-4-dev libadwaita-1-dev gettext - name: Check formatting run: cargo fmt --check diff --git a/po/POTFILES.in b/po/POTFILES.in index bfc3f1e..3161975 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,7 +5,6 @@ src/gui_frontend/dialogs.rs src/gui_frontend/app_view.rs src/gui_frontend/stat_view.rs src/gui_frontend/achievement_automatic_view.rs -src/gui_frontend/achievement_view.rs src/gui_frontend/achievement_manual_view/mod.rs src/gui_frontend/achievement_manual_view/header.rs src/gui_frontend/achievement_manual_view/config_popover.rs diff --git a/po/fr.po b/po/fr.po index cfbb9a2..f24a569 100644 --- a/po/fr.po +++ b/po/fr.po @@ -2,114 +2,154 @@ msgid "" msgstr "" "Project-Id-Version: SamRewritten\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 21:40+0200\n" +"POT-Creation-Date: 2026-09-03 21:30+0200\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: src/gui_frontend/ui_components.rs:159 src/gui_frontend/app_view.rs:62 +#: src/gui_frontend/ui_components.rs:160 src/gui_frontend/app_view.rs:62 #: src/gui_frontend/app_list_view/mod.rs:421 msgid "Loading..." msgstr "Chargement..." -#: src/gui_frontend/ui_components.rs:190 +#: src/gui_frontend/ui_components.rs:191 msgid "Select all visible apps" msgstr "Sélectionner les applications visibles" -#: src/gui_frontend/ui_components.rs:194 +#: src/gui_frontend/ui_components.rs:195 msgid "Deselect all apps" msgstr "Tout désélectionner" -#: src/gui_frontend/ui_components.rs:198 +#: src/gui_frontend/ui_components.rs:199 msgid "Unlock all in selection" msgstr "Tout déverrouiller dans la sélection" -#: src/gui_frontend/ui_components.rs:202 +#: src/gui_frontend/ui_components.rs:203 msgid "Reset all in selection" msgstr "Tout réinitialiser dans la sélection" -#: src/gui_frontend/ui_components.rs:206 +#: src/gui_frontend/ui_components.rs:207 #: src/gui_frontend/app_list_view/progress_actions.rs:116 msgid "Export selected apps progress" msgstr "Exporter la progression sélection" -#: src/gui_frontend/ui_components.rs:210 +#: src/gui_frontend/ui_components.rs:211 msgid "Import progress..." msgstr "Importer progression..." -#: src/gui_frontend/ui_components.rs:216 +#: src/gui_frontend/ui_components.rs:217 msgid "Appear in-game while managing" msgstr "Apparaître in-game pendant l'édition" -#: src/gui_frontend/ui_components.rs:222 +#: src/gui_frontend/ui_components.rs:223 msgid "Refresh app list" msgstr "Actualiser la liste des apps" -#: src/gui_frontend/ui_components.rs:226 +#: src/gui_frontend/ui_components.rs:227 msgid "Refresh achievement counts" msgstr "Actualiser le décompte de succès" -#: src/gui_frontend/ui_components.rs:229 src/gui_frontend/ui_components.rs:413 +#: src/gui_frontend/ui_components.rs:230 src/gui_frontend/ui_components.rs:471 msgid "About" msgstr "À propos" -#: src/gui_frontend/ui_components.rs:233 +#: src/gui_frontend/ui_components.rs:234 msgid "Change Steam folder…" msgstr "Changer le dossier Steam…" -#: src/gui_frontend/ui_components.rs:240 +#: src/gui_frontend/ui_components.rs:241 msgid "Bulk process" msgstr "Traitement par lot" -#: src/gui_frontend/ui_components.rs:246 +#: src/gui_frontend/ui_components.rs:247 msgid "System" msgstr "Système" -#: src/gui_frontend/ui_components.rs:247 +#: src/gui_frontend/ui_components.rs:248 msgid "Light" msgstr "Clair" -#: src/gui_frontend/ui_components.rs:248 +#: src/gui_frontend/ui_components.rs:249 msgid "Dark" msgstr "Sombre" -#: src/gui_frontend/ui_components.rs:257 +#: src/gui_frontend/ui_components.rs:258 msgid "Disable animations" msgstr "Désactiver les animations" -#: src/gui_frontend/ui_components.rs:261 +#: src/gui_frontend/ui_components.rs:262 msgid "Appearance" msgstr "Apparence" -#: src/gui_frontend/ui_components.rs:267 +#: src/gui_frontend/ui_components.rs:268 msgid "System default" msgstr "Système par défaut" -#: src/gui_frontend/ui_components.rs:270 +#: src/gui_frontend/ui_components.rs:271 msgid "Language" msgstr "Langue" -#: src/gui_frontend/ui_components.rs:387 +#: src/gui_frontend/ui_components.rs:388 msgid "Game default" msgstr "Langue du jeu" -#: src/gui_frontend/ui_components.rs:406 +#: src/gui_frontend/ui_components.rs:411 msgid "Refresh achievements & stats" msgstr "Actualiser les succès et statistiques" -#: src/gui_frontend/ui_components.rs:410 +#: src/gui_frontend/ui_components.rs:415 msgid "Reset everything" msgstr "Tout réinitialiser" -#: src/gui_frontend/ui_components.rs:418 +#: src/gui_frontend/ui_components.rs:435 msgid "Achievement language" msgstr "Langue des succès" +#: src/gui_frontend/ui_components.rs:440 +msgid "Achievement order" +msgstr "Tri des succès" + +#: src/gui_frontend/ui_components.rs:443 src/gui_frontend/ui_components.rs:465 +msgid "Steam default" +msgstr "Par défaut Steam" + +#: src/gui_frontend/ui_components.rs:444 src/gui_frontend/ui_components.rs:466 +msgid "Alphabetically" +msgstr "Alphabétique" + +#: src/gui_frontend/ui_components.rs:445 +msgid "Global percentage" +msgstr "Pourcentage global" + +#: src/gui_frontend/ui_components.rs:446 +msgid "Unlock date" +msgstr "Date de déblocage" + +#: src/gui_frontend/ui_components.rs:451 +msgid "Achievement state" +msgstr "Filtre des succès" + +#: src/gui_frontend/ui_components.rs:454 +msgid "All" +msgstr "Tous" + +#: src/gui_frontend/ui_components.rs:455 +msgid "Locked" +msgstr "Verrouillés" + +#: src/gui_frontend/ui_components.rs:456 +msgid "Unlocked" +msgstr "Déverrouillés" + +#: src/gui_frontend/ui_components.rs:462 +msgid "Stat order" +msgstr "Tri des stats" + #: src/gui_frontend/dialogs.rs:89 src/gui_frontend/dialogs.rs:162 #: src/gui_frontend/dialogs.rs:185 src/gui_frontend/dialogs.rs:233 -#: src/gui_frontend/unlock_scheduler.rs:185 +#: src/gui_frontend/unlock_scheduler.rs:217 #: src/gui_frontend/app_list_view/progress_actions.rs:43 msgid "OK" msgstr "OK" @@ -293,11 +333,11 @@ msgstr "Succès" msgid "Stats" msgstr "Statistiques" -#: src/gui_frontend/stat_view.rs:102 +#: src/gui_frontend/stat_view.rs:166 msgid "Increment only" msgstr "Incrément uniquement" -#: src/gui_frontend/stat_view.rs:107 +#: src/gui_frontend/stat_view.rs:171 msgid "This statistic is protected." msgstr "Cette statistique est protégée." @@ -305,19 +345,19 @@ msgstr "Cette statistique est protégée." msgid "Stop and go back" msgstr "Arrêter et revenir" -#: src/gui_frontend/achievement_manual_view/mod.rs:57 +#: src/gui_frontend/achievement_manual_view/mod.rs:58 msgid "No achievements staged" msgstr "Aucun succès préparé" -#: src/gui_frontend/achievement_manual_view/mod.rs:58 +#: src/gui_frontend/achievement_manual_view/mod.rs:59 msgid "{count} staged" msgstr "{count} préparé(s)" -#: src/gui_frontend/achievement_manual_view/mod.rs:96 +#: src/gui_frontend/achievement_manual_view/mod.rs:97 msgid "Already at or above target ({progress})" msgstr "Déjà à la cible ou au-dessus ({progress})" -#: src/gui_frontend/achievement_manual_view/mod.rs:102 +#: src/gui_frontend/achievement_manual_view/mod.rs:103 msgid "Auto-fill {count} achievement(s) ({progress})" msgstr "Remplir automatiquement {count} succès ({progress})" @@ -383,7 +423,7 @@ msgid "Spacing" msgstr "Espacement" #: src/gui_frontend/achievement_manual_view/copy_controls.rs:38 -#: src/gui_frontend/achievement_manual_view/copy_mode.rs:234 +#: src/gui_frontend/achievement_manual_view/copy_mode.rs:238 msgid "Choose a friend to copy timing from" msgstr "Choisir un ami pour lequel copier le minutage" @@ -404,11 +444,11 @@ msgstr "Écart max" msgid "First delay" msgstr "Délai initial" -#: src/gui_frontend/achievement_manual_view/copy_mode.rs:101 +#: src/gui_frontend/achievement_manual_view/copy_mode.rs:105 msgid "No friend loaded" msgstr "Aucun ami chargé" -#: src/gui_frontend/achievement_manual_view/copy_mode.rs:111 +#: src/gui_frontend/achievement_manual_view/copy_mode.rs:115 msgid "{count} achievements over {time}" msgstr "{count} succès sur {time}" @@ -449,148 +489,148 @@ msgid "Try again" msgstr "Réessayer" #: src/gui_frontend/app_list_view/mod.rs:464 -#: src/gui_frontend/app_list_view/mod.rs:1752 +#: src/gui_frontend/app_list_view/mod.rs:1769 msgid "Name or AppId (Ctrl+K)" msgstr "Nom ou AppId (Ctrl+K)" -#: src/gui_frontend/app_list_view/mod.rs:480 +#: src/gui_frontend/app_list_view/mod.rs:499 msgid "Show or hide the sidebar" msgstr "Afficher ou masquer la barre latérale" -#: src/gui_frontend/app_list_view/mod.rs:1747 +#: src/gui_frontend/app_list_view/mod.rs:1764 msgid "Achievement or stat..." msgstr "Succès ou statistique..." -#: src/gui_frontend/app_list_view/mod.rs:1866 -#: src/gui_frontend/app_list_view/settings_bindings.rs:333 +#: src/gui_frontend/app_list_view/mod.rs:1883 +#: src/gui_frontend/app_list_view/settings_bindings.rs:328 msgid "Could not change the in-game setting" msgstr "Impossible de changer l'option in-game" -#: src/gui_frontend/app_list_view/mod.rs:1868 -#: src/gui_frontend/app_list_view/settings_bindings.rs:334 +#: src/gui_frontend/app_list_view/mod.rs:1885 +#: src/gui_frontend/app_list_view/settings_bindings.rs:329 msgid "The change could not be applied. Restart SamRewritten and try again." msgstr "" "Le changement n'a pas pu être appliqué. Redémarrez SamRewritten et réessayez." -#: src/gui_frontend/app_list_view/sidebar.rs:58 +#: src/gui_frontend/app_list_view/sidebar.rs:57 msgid "Hide with no achievements" msgstr "Masquer sans succès" -#: src/gui_frontend/app_list_view/sidebar.rs:64 +#: src/gui_frontend/app_list_view/sidebar.rs:63 msgid "Hide at 100%" msgstr "Masquer à 100 %" -#: src/gui_frontend/app_list_view/sidebar.rs:70 +#: src/gui_frontend/app_list_view/sidebar.rs:69 msgid "Hide at 0%" msgstr "Masquer à 0 %" -#: src/gui_frontend/app_list_view/sidebar.rs:76 +#: src/gui_frontend/app_list_view/sidebar.rs:75 msgid "Hide never launched" msgstr "Masquer jamais lancés" -#: src/gui_frontend/app_list_view/sidebar.rs:82 +#: src/gui_frontend/app_list_view/sidebar.rs:81 msgid "Only currently idling" msgstr "Uniquement in-game" -#: src/gui_frontend/app_list_view/sidebar.rs:88 +#: src/gui_frontend/app_list_view/sidebar.rs:87 msgid "Hide hidden in Steam" msgstr "Masquer les masqués Steam" -#: src/gui_frontend/app_list_view/sidebar.rs:94 +#: src/gui_frontend/app_list_view/sidebar.rs:93 msgid "Show junk" msgstr "Afficher superflus" -#: src/gui_frontend/app_list_view/sidebar.rs:108 +#: src/gui_frontend/app_list_view/sidebar.rs:107 msgid "App ID" msgstr "App ID" -#: src/gui_frontend/app_list_view/sidebar.rs:113 +#: src/gui_frontend/app_list_view/sidebar.rs:112 msgid "Name" msgstr "Nom" -#: src/gui_frontend/app_list_view/sidebar.rs:118 +#: src/gui_frontend/app_list_view/sidebar.rs:117 msgid "Last played" msgstr "Lancé récemment" -#: src/gui_frontend/app_list_view/sidebar.rs:123 +#: src/gui_frontend/app_list_view/sidebar.rs:122 msgid "Playtime" msgstr "Temps de jeu" -#: src/gui_frontend/app_list_view/sidebar.rs:128 +#: src/gui_frontend/app_list_view/sidebar.rs:127 msgid "Completion" msgstr "Complétion" -#: src/gui_frontend/app_list_view/sidebar.rs:133 +#: src/gui_frontend/app_list_view/sidebar.rs:132 msgid "Achievements left" msgstr "Succès restants" -#: src/gui_frontend/app_list_view/sidebar.rs:183 +#: src/gui_frontend/app_list_view/sidebar.rs:180 msgid "Filters on a search term, which SamRewritten cannot reproduce exactly." msgstr "" "Filtre selon un terme de recherche, ce que SamRewritten ne peut pas " "reproduire exactement." -#: src/gui_frontend/app_list_view/sidebar.rs:186 +#: src/gui_frontend/app_list_view/sidebar.rs:183 msgid "Uses a Steam filter SamRewritten cannot reproduce exactly." msgstr "" "Utilise un filtre Steam que SamRewritten ne peut pas reproduire exactement." -#: src/gui_frontend/app_list_view/sidebar.rs:189 +#: src/gui_frontend/app_list_view/sidebar.rs:186 msgid "" "Needs information from Steam that could not be read. Refresh to try again." msgstr "" "Nécessite des informations de Steam qui n'ont pas pu être lues. Actualisez " "pour réessayer." -#: src/gui_frontend/app_list_view/sidebar.rs:192 +#: src/gui_frontend/app_list_view/sidebar.rs:189 msgid "" "Needs your friends' games, which Steam can only tell us when it is online." msgstr "" "Nécessite les jeux de vos amis, que Steam ne peut indiquer qu'en ligne." -#: src/gui_frontend/app_list_view/sidebar.rs:201 +#: src/gui_frontend/app_list_view/sidebar.rs:198 msgid "Favorites" msgstr "Favoris" -#: src/gui_frontend/app_list_view/sidebar.rs:228 +#: src/gui_frontend/app_list_view/sidebar.rs:225 msgid "View profile" msgstr "Voir le profil" -#: src/gui_frontend/app_list_view/sidebar.rs:283 +#: src/gui_frontend/app_list_view/sidebar.rs:280 #: src/gui_frontend/profile_view/mod.rs:484 msgid "Steam user" msgstr "Utilisateur Steam" -#: src/gui_frontend/app_list_view/sidebar.rs:305 +#: src/gui_frontend/app_list_view/sidebar.rs:300 msgid "Steam is offline. What needs its servers is turned off." msgstr "Steam est hors ligne. Ce qui dépend de ses serveurs est désactivé." -#: src/gui_frontend/app_list_view/sidebar.rs:316 +#: src/gui_frontend/app_list_view/sidebar.rs:311 msgid "Fetching completion…" msgstr "Chargement de la complétion…" -#: src/gui_frontend/app_list_view/sidebar.rs:322 +#: src/gui_frontend/app_list_view/sidebar.rs:317 msgid "Click to cancel" msgstr "Cliquez pour annuler" -#: src/gui_frontend/app_list_view/sidebar.rs:353 +#: src/gui_frontend/app_list_view/sidebar.rs:348 msgid "Filters" msgstr "Filtres" -#: src/gui_frontend/app_list_view/sidebar.rs:364 +#: src/gui_frontend/app_list_view/sidebar.rs:359 msgid "Steam collection" msgstr "Collection Steam" -#: src/gui_frontend/app_list_view/sidebar.rs:365 -#: src/gui_frontend/app_list_view/sidebar.rs:588 +#: src/gui_frontend/app_list_view/sidebar.rs:360 +#: src/gui_frontend/app_list_view/sidebar.rs:580 msgid "All games" msgstr "Tous les jeux" -#: src/gui_frontend/app_list_view/sidebar.rs:448 +#: src/gui_frontend/app_list_view/sidebar.rs:442 msgid "Sort by" msgstr "Trier par" -#: src/gui_frontend/app_list_view/sidebar.rs:513 +#: src/gui_frontend/app_list_view/sidebar.rs:505 msgid "Reset filters" msgstr "Réinitialiser les filtres" @@ -1360,9 +1400,6 @@ msgstr "Déjà déverrouillé." #~ msgid "Hide with no unlocks" #~ msgstr "Masquer sans succès obtenu" -#~ msgid "Alphabetical" -#~ msgstr "Alphabétique" - #~ msgid "Recently played" #~ msgstr "Lancé récemment" diff --git a/po/samrewritten.pot b/po/samrewritten.pot index 867588a..c2914d9 100644 --- a/po/samrewritten.pot +++ b/po/samrewritten.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: SamRewritten\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 14:52-0300\n" +"POT-Creation-Date: 2026-09-03 21:30+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -59,7 +59,7 @@ msgstr "" msgid "Refresh achievement counts" msgstr "" -#: src/gui_frontend/ui_components.rs:230 src/gui_frontend/ui_components.rs:414 +#: src/gui_frontend/ui_components.rs:230 src/gui_frontend/ui_components.rs:471 msgid "About" msgstr "" @@ -103,61 +103,61 @@ msgstr "" msgid "Game default" msgstr "" -#: src/gui_frontend/ui_components.rs:407 +#: src/gui_frontend/ui_components.rs:411 msgid "Refresh achievements & stats" msgstr "" -#: src/gui_frontend/ui_components.rs:411 +#: src/gui_frontend/ui_components.rs:415 msgid "Reset everything" msgstr "" -#: src/gui_frontend/ui_components.rs:432 src/gui_frontend/ui_components.rs:449 -msgid "Steam default" +#: src/gui_frontend/ui_components.rs:435 +msgid "Achievement language" msgstr "" -#: src/gui_frontend/ui_components.rs:433 src/gui_frontend/ui_components.rs:450 -msgid "Alphabetically" +#: src/gui_frontend/ui_components.rs:440 +msgid "Achievement order" msgstr "" -#: src/gui_frontend/ui_components.rs:439 -msgid "Stat order" +#: src/gui_frontend/ui_components.rs:443 src/gui_frontend/ui_components.rs:465 +msgid "Steam default" msgstr "" -#: src/gui_frontend/ui_components.rs:444 -msgid "Achievement language" +#: src/gui_frontend/ui_components.rs:444 src/gui_frontend/ui_components.rs:466 +msgid "Alphabetically" msgstr "" -#: src/gui_frontend/ui_components.rs:451 +#: src/gui_frontend/ui_components.rs:445 msgid "Global percentage" msgstr "" -#: src/gui_frontend/ui_components.rs:452 +#: src/gui_frontend/ui_components.rs:446 msgid "Unlock date" msgstr "" -#: src/gui_frontend/ui_components.rs:458 -msgid "Achievement order" +#: src/gui_frontend/ui_components.rs:451 +msgid "Achievement state" msgstr "" -#: src/gui_frontend/ui_components.rs:462 +#: src/gui_frontend/ui_components.rs:454 msgid "All" msgstr "" -#: src/gui_frontend/ui_components.rs:463 +#: src/gui_frontend/ui_components.rs:455 msgid "Locked" msgstr "" -#: src/gui_frontend/ui_components.rs:464 +#: src/gui_frontend/ui_components.rs:456 msgid "Unlocked" msgstr "" -#: src/gui_frontend/ui_components.rs:470 -msgid "Achievement state" +#: src/gui_frontend/ui_components.rs:462 +msgid "Stat order" msgstr "" #: src/gui_frontend/dialogs.rs:89 src/gui_frontend/dialogs.rs:162 #: src/gui_frontend/dialogs.rs:185 src/gui_frontend/dialogs.rs:233 -#: src/gui_frontend/unlock_scheduler.rs:185 +#: src/gui_frontend/unlock_scheduler.rs:217 #: src/gui_frontend/app_list_view/progress_actions.rs:43 msgid "OK" msgstr "" @@ -187,9 +187,9 @@ msgstr "" #: src/gui_frontend/dialogs.rs:290 msgid "" "If you've installed Steam in a custom location, you can point SamRewritten " -"to it using environment variables. Please check the GitHub page for instructions, or to " -"report your issue." +"to it using environment variables. Please check the GitHub page for instructions, or to report " +"your issue." msgstr "" #: src/gui_frontend/dialogs.rs:295 @@ -314,11 +314,11 @@ msgstr "" msgid "Stats" msgstr "" -#: src/gui_frontend/stat_view.rs:152 +#: src/gui_frontend/stat_view.rs:166 msgid "Increment only" msgstr "" -#: src/gui_frontend/stat_view.rs:157 +#: src/gui_frontend/stat_view.rs:171 msgid "This statistic is protected." msgstr "" @@ -326,19 +326,19 @@ msgstr "" msgid "Stop and go back" msgstr "" -#: src/gui_frontend/achievement_manual_view/mod.rs:57 +#: src/gui_frontend/achievement_manual_view/mod.rs:58 msgid "No achievements staged" msgstr "" -#: src/gui_frontend/achievement_manual_view/mod.rs:58 +#: src/gui_frontend/achievement_manual_view/mod.rs:59 msgid "{count} staged" msgstr "" -#: src/gui_frontend/achievement_manual_view/mod.rs:96 +#: src/gui_frontend/achievement_manual_view/mod.rs:97 msgid "Already at or above target ({progress})" msgstr "" -#: src/gui_frontend/achievement_manual_view/mod.rs:102 +#: src/gui_frontend/achievement_manual_view/mod.rs:103 msgid "Auto-fill {count} achievement(s) ({progress})" msgstr "" @@ -404,7 +404,7 @@ msgid "Spacing" msgstr "" #: src/gui_frontend/achievement_manual_view/copy_controls.rs:38 -#: src/gui_frontend/achievement_manual_view/copy_mode.rs:234 +#: src/gui_frontend/achievement_manual_view/copy_mode.rs:238 msgid "Choose a friend to copy timing from" msgstr "" @@ -425,11 +425,11 @@ msgstr "" msgid "First delay" msgstr "" -#: src/gui_frontend/achievement_manual_view/copy_mode.rs:101 +#: src/gui_frontend/achievement_manual_view/copy_mode.rs:105 msgid "No friend loaded" msgstr "" -#: src/gui_frontend/achievement_manual_view/copy_mode.rs:111 +#: src/gui_frontend/achievement_manual_view/copy_mode.rs:115 msgid "{count} achievements over {time}" msgstr "" diff --git a/src/gui_frontend/achievement_manual_view/row_factory.rs b/src/gui_frontend/achievement_manual_view/row_factory.rs index 7441f7b..30b32b2 100644 --- a/src/gui_frontend/achievement_manual_view/row_factory.rs +++ b/src/gui_frontend/achievement_manual_view/row_factory.rs @@ -184,7 +184,6 @@ pub(super) fn install_row_factory( start_button .set_sensitive(new_unlocked != raw_model_len as usize); update_autofill(); - model_updates.changed(); } Ok(false) => { eprintln!("[CLIENT] Steam did not store the achievement"); @@ -196,6 +195,7 @@ pub(super) fn install_row_factory( } } switch.set_sensitive(true); + model_updates.changed(); } )); } diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index 27f48c4..14c9334 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -131,8 +131,10 @@ pub fn create_achievements_view( order_action.connect_activate(glib::clone!( #[strong] achievement_order, - #[weak] + #[strong] achievement_sorter, + #[weak(rename_to = sort_model)] + app_achievement_sort_model, move |action, target| { let Some(value) = target.and_then(|target| target.str()) else { return; @@ -142,7 +144,12 @@ pub fn create_achievements_view( }; action.set_state(&value.to_variant()); achievement_order.set(order); - achievement_sorter.changed(SorterChange::Different); + if matches!(order, AchievementOrder::SteamDefault) { + sort_model.set_sorter(None::<&CustomSorter>); + } else { + sort_model.set_sorter(Some(&achievement_sorter)); + achievement_sorter.changed(SorterChange::Different); + } } )); application.add_action(&order_action); diff --git a/src/gui_frontend/stat_view.rs b/src/gui_frontend/stat_view.rs index 956f0a1..b9067da 100644 --- a/src/gui_frontend/stat_view.rs +++ b/src/gui_frontend/stat_view.rs @@ -26,24 +26,20 @@ use gtk::glib::prelude::{StaticVariantType, ToVariant}; use gtk::glib::translate::FromGlib; use gtk::pango::EllipsizeMode; use gtk::prelude::{ - BoxExt, GObjectPropertyExpressionExt, ListItemExt, ObjectExt, SorterExt, ToValue, WidgetExt, + BoxExt, GObjectPropertyExpressionExt, ListItemExt, ObjectExt, ToValue, WidgetExt, }; use gtk::{ Adjustment, Align, Box, ClosureExpression, CustomSorter, FilterListModel, Frame, Label, ListItem, ListView, NoSelection, Orientation, ScrolledWindow, SignalListItemFactory, - SortListModel, SorterChange, SpinButton, StringFilter, StringFilterMatchMode, Widget, glib, + SortListModel, SpinButton, StringFilter, StringFilterMatchMode, Widget, glib, }; -use std::cell::Cell; use std::cell::RefCell; -use std::cmp::Ordering; use std::ffi::c_ulong; -use std::rc::Rc; use std::sync::mpsc::channel; use std::time::Duration; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy)] enum StatOrder { - #[default] SteamDefault, Alphabetical, } @@ -72,25 +68,17 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St .filter(&app_stats_string_filter) .build(); - let stat_order = Rc::new(Cell::new(StatOrder::default())); - let stat_sorter = CustomSorter::new({ - let stat_order = Rc::clone(&stat_order); - move |obj1, obj2| { - if matches!(stat_order.get(), StatOrder::SteamDefault) { - return Ordering::Equal.into(); - } - let stat1 = obj1.downcast_ref::().unwrap(); - let stat2 = obj2.downcast_ref::().unwrap(); - stat1 - .display_name() - .to_lowercase() - .cmp(&stat2.display_name().to_lowercase()) - .into() - } + let stat_sorter = CustomSorter::new(|obj1, obj2| { + let stat1 = obj1.downcast_ref::().unwrap(); + let stat2 = obj2.downcast_ref::().unwrap(); + stat1 + .display_name() + .to_lowercase() + .cmp(&stat2.display_name().to_lowercase()) + .into() }); let app_stats_sort_model = SortListModel::builder() .model(&app_stats_filter_model) - .sorter(&stat_sorter) .build(); let order_action = SimpleAction::new_stateful( @@ -100,9 +88,9 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St ); order_action.connect_activate(glib::clone!( #[strong] - stat_order, - #[weak] stat_sorter, + #[weak(rename_to = sort_model)] + app_stats_sort_model, move |action, target| { let Some(value) = target.and_then(|target| target.str()) else { return; @@ -111,8 +99,10 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St return; }; action.set_state(&value.to_variant()); - stat_order.set(order); - stat_sorter.changed(SorterChange::Different); + match order { + StatOrder::SteamDefault => sort_model.set_sorter(None::<&CustomSorter>), + StatOrder::Alphabetical => sort_model.set_sorter(Some(&stat_sorter)), + } } )); application.add_action(&order_action); diff --git a/src/gui_frontend/ui_components.rs b/src/gui_frontend/ui_components.rs index 258f49c..d526d36 100644 --- a/src/gui_frontend/ui_components.rs +++ b/src/gui_frontend/ui_components.rs @@ -398,6 +398,10 @@ fn fill_achievement_language_menu(languages: &[String]) { }); } +/// Entries `set_context_popover_to_app_details_context` puts above the +/// tab-specific portion, and therefore the first index the latter may clear. +const APP_DETAILS_FIXED_ITEMS: i32 = 2; + pub fn set_context_popover_to_app_details_context( menu_model: >k::gio::Menu, application: &MainApplication, @@ -411,7 +415,7 @@ pub fn set_context_popover_to_app_details_context( Some(tr("Reset everything").as_str()), Some("app.clear_all_stats_and_achievements"), ); - menu_model.append(Some(tr("About").as_str()), Some("app.about")); + debug_assert_eq!(menu_model.n_items(), APP_DETAILS_FIXED_ITEMS); ACHIEVEMENT_LANGUAGES_FROM_FETCH.with(|f| f.set(false)); fill_achievement_language_menu(&[]); @@ -422,61 +426,62 @@ pub fn set_context_popover_to_app_details_context( /// Replace the tab-specific portion of the app-details menu. pub fn set_app_details_view_options(menu_model: >k::gio::Menu, achievements: bool) { - while menu_model.n_items() > 3 { - menu_model.remove(3); + while menu_model.n_items() > APP_DETAILS_FIXED_ITEMS { + menu_model.remove(APP_DETAILS_FIXED_ITEMS); } - if !achievements { + if achievements { + ACHIEVEMENT_LANGUAGE_MENU.with(|menu| { + menu_model.append_submenu(Some(tr("Achievement language").as_str()), menu); + }); + + append_target_submenu( + menu_model, + tr("Achievement order").as_str(), + "app.achievement-order", + &[ + (tr("Steam default"), "steam-default"), + (tr("Alphabetically"), "alphabetical"), + (tr("Global percentage"), "global-percentage"), + (tr("Unlock date"), "unlock-date"), + ], + ); + append_target_submenu( + menu_model, + tr("Achievement state").as_str(), + "app.achievement-state", + &[ + (tr("All"), "all"), + (tr("Locked"), "locked"), + (tr("Unlocked"), "unlocked"), + ], + ); + } else { append_target_submenu( menu_model, - tr("Stat order"), + tr("Stat order").as_str(), "app.stat-order", - [ + &[ (tr("Steam default"), "steam-default"), (tr("Alphabetically"), "alphabetical"), ], ); - return; } - ACHIEVEMENT_LANGUAGE_MENU.with(|menu| { - menu_model.append_submenu(Some(tr("Achievement language").as_str()), menu); - }); - - append_target_submenu( - menu_model, - tr("Achievement order"), - "app.achievement-order", - [ - (tr("Steam default"), "steam-default"), - (tr("Alphabetically"), "alphabetical"), - (tr("Global percentage"), "global-percentage"), - (tr("Unlock date"), "unlock-date"), - ], - ); - append_target_submenu( - menu_model, - tr("Achievement state"), - "app.achievement-state", - [ - (tr("All"), "all"), - (tr("Locked"), "locked"), - (tr("Unlocked"), "unlocked"), - ], - ); + menu_model.append(Some(tr("About").as_str()), Some("app.about")); } -fn append_target_submenu( +fn append_target_submenu( menu_model: >k::gio::Menu, - label: gtk::glib::GString, + label: &str, action: &str, - entries: [(gtk::glib::GString, &'static str); N], + entries: &[(gtk::glib::GString, &str)], ) { let submenu = gtk::gio::Menu::new(); for (label, value) in entries { - let item = gtk::gio::MenuItem::new(Some(label.as_str()), Some(action)); + let item = gtk::gio::MenuItem::new(Some(label.as_str()), None); item.set_action_and_target_value(Some(action), Some(&value.to_variant())); submenu.append_item(&item); } - menu_model.append_submenu(Some(label.as_str()), &submenu); + menu_model.append_submenu(Some(label), &submenu); } From 9ee8431ef28843b5c7906292339fea6ab8b68c1a Mon Sep 17 00:00:00 2001 From: antoniohauren Date: Thu, 3 Sep 2026 18:55:26 -0300 Subject: [PATCH 6/7] fix(gui): persist detail view ordering --- .../org.samrewritten.SamRewritten.gschema.xml | 14 +++++++++++++- src/gui_frontend/achievement_view.rs | 16 ++++++++++++++-- src/gui_frontend/stat_view.rs | 17 +++++++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/assets/org.samrewritten.SamRewritten.gschema.xml b/assets/org.samrewritten.SamRewritten.gschema.xml index 0213147..4e0c3be 100644 --- a/assets/org.samrewritten.SamRewritten.gschema.xml +++ b/assets/org.samrewritten.SamRewritten.gschema.xml @@ -85,6 +85,18 @@ How the achievement view commits toggles. Valid values are 'autocommit' (each switch commits immediately), 'deferred' (toggles build an ordered queue that the user fires later), and 'copytiming' (replay a friend's unlock cadence). + + 'global-percentage' + Achievement Order + How achievements are sorted. Valid values are 'steam-default', 'alphabetical', 'global-percentage', and 'unlock-date'. + + + + 'steam-default' + Stat Order + How stats are sorted. Valid values are 'steam-default' and 'alphabetical'. + + 5 Copy Timing Maximum Gap (Minutes) @@ -157,4 +169,4 @@ Whether games you placed in Steam's own Hidden collection are left out of the games list. - \ No newline at end of file + diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index 14c9334..cdf2602 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -17,6 +17,7 @@ use crate::gui_frontend::MainApplication; use crate::gui_frontend::achievement_automatic_view::create_achievements_automatic_view; use crate::gui_frontend::achievement_manual_view::create_achievements_manual_view; use crate::gui_frontend::gobjects::achievement::GAchievementObject; +use crate::gui_frontend::gsettings::get_settings; use crate::gui_frontend::unlock_scheduler::AchievementModelUpdates; use gtk::gio::{ListStore, SimpleAction}; use gtk::glib; @@ -58,6 +59,7 @@ pub fn create_achievements_view( application: &MainApplication, app_achievement_count_value: &Label, ) -> (Stack, ListStore, StringFilter, Arc) { + let settings = get_settings(); let app_achievements_model = ListStore::new::(); let app_timed_achievements_model = ListStore::new::(); @@ -91,7 +93,9 @@ pub fn create_achievements_view( .filter(&app_achievement_string_filter) .build(); - let achievement_order = Rc::new(Cell::new(AchievementOrder::default())); + let order_value = settings.string("achievement-order").to_string(); + let initial_order = AchievementOrder::from_action_target(&order_value).unwrap_or_default(); + let achievement_order = Rc::new(Cell::new(initial_order)); let achievement_sorter = CustomSorter::new({ let achievement_order = Rc::clone(&achievement_order); move |obj1, obj2| { @@ -120,19 +124,24 @@ pub fn create_achievements_view( .model(&app_achievement_status_filter_model) .sorter(&achievement_sorter) .build(); + if matches!(initial_order, AchievementOrder::SteamDefault) { + app_achievement_sort_model.set_sorter(None::<&CustomSorter>); + } let achievement_model_updates = AchievementModelUpdates::new(&achievement_status_filter, &achievement_sorter); let order_action = SimpleAction::new_stateful( "achievement-order", Some(&String::static_variant_type()), - &"global-percentage".to_variant(), + &order_value.to_variant(), ); order_action.connect_activate(glib::clone!( #[strong] achievement_order, #[strong] achievement_sorter, + #[strong] + settings, #[weak(rename_to = sort_model)] app_achievement_sort_model, move |action, target| { @@ -143,6 +152,9 @@ pub fn create_achievements_view( return; }; action.set_state(&value.to_variant()); + if let Err(e) = settings.set_string("achievement-order", value) { + eprintln!("[CLIENT] Error saving achievement order: {e:?}"); + } achievement_order.set(order); if matches!(order, AchievementOrder::SteamDefault) { sort_model.set_sorter(None::<&CustomSorter>); diff --git a/src/gui_frontend/stat_view.rs b/src/gui_frontend/stat_view.rs index b9067da..af5f9cd 100644 --- a/src/gui_frontend/stat_view.rs +++ b/src/gui_frontend/stat_view.rs @@ -16,9 +16,10 @@ use super::gobjects::stat::GStatObject; use super::request::{Request, SetFloatStat, SetIntStat}; use crate::gui_frontend::MainApplication; +use crate::gui_frontend::gsettings::get_settings; use crate::gui_frontend::i18n::tr; use crate::utils::action_journal::{Batch, Change, Op}; -use gtk::gio::prelude::ActionMapExt; +use gtk::gio::prelude::{ActionMapExt, SettingsExt}; use gtk::gio::{ListStore, SimpleAction, spawn_blocking}; use gtk::glib::SignalHandlerId; use gtk::glib::object::Cast; @@ -55,6 +56,7 @@ impl StatOrder { } pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, StringFilter) { + let settings = get_settings(); let stats_list_factory = SignalListItemFactory::new(); let app_stats_model = ListStore::new::(); @@ -80,15 +82,23 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St let app_stats_sort_model = SortListModel::builder() .model(&app_stats_filter_model) .build(); + let order_value = settings.string("stat-order").to_string(); + let initial_order = + StatOrder::from_action_target(&order_value).unwrap_or(StatOrder::SteamDefault); + if matches!(initial_order, StatOrder::Alphabetical) { + app_stats_sort_model.set_sorter(Some(&stat_sorter)); + } let order_action = SimpleAction::new_stateful( "stat-order", Some(&String::static_variant_type()), - &"steam-default".to_variant(), + &order_value.to_variant(), ); order_action.connect_activate(glib::clone!( #[strong] stat_sorter, + #[strong] + settings, #[weak(rename_to = sort_model)] app_stats_sort_model, move |action, target| { @@ -99,6 +109,9 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St return; }; action.set_state(&value.to_variant()); + if let Err(e) = settings.set_string("stat-order", value) { + eprintln!("[CLIENT] Error saving stat order: {e:?}"); + } match order { StatOrder::SteamDefault => sort_model.set_sorter(None::<&CustomSorter>), StatOrder::Alphabetical => sort_model.set_sorter(Some(&stat_sorter)), From cbbe49a22fa31e39d79d3b4cea524f8dbca9ee3a Mon Sep 17 00:00:00 2001 From: PaulCombal Date: Fri, 4 Sep 2026 18:44:30 +0200 Subject: [PATCH 7/7] code review --- src/gui_frontend/achievement_view.rs | 15 ++++++++++++--- src/gui_frontend/stat_view.rs | 14 ++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/gui_frontend/achievement_view.rs b/src/gui_frontend/achievement_view.rs index cdf2602..fbd38b6 100644 --- a/src/gui_frontend/achievement_view.rs +++ b/src/gui_frontend/achievement_view.rs @@ -51,6 +51,15 @@ impl AchievementOrder { _ => None, } } + + fn as_action_target(self) -> &'static str { + match self { + Self::SteamDefault => "steam-default", + Self::Alphabetical => "alphabetical", + Self::GlobalPercentage => "global-percentage", + Self::UnlockDate => "unlock-date", + } + } } pub fn create_achievements_view( @@ -93,8 +102,8 @@ pub fn create_achievements_view( .filter(&app_achievement_string_filter) .build(); - let order_value = settings.string("achievement-order").to_string(); - let initial_order = AchievementOrder::from_action_target(&order_value).unwrap_or_default(); + let initial_order = AchievementOrder::from_action_target(&settings.string("achievement-order")) + .unwrap_or_default(); let achievement_order = Rc::new(Cell::new(initial_order)); let achievement_sorter = CustomSorter::new({ let achievement_order = Rc::clone(&achievement_order); @@ -133,7 +142,7 @@ pub fn create_achievements_view( let order_action = SimpleAction::new_stateful( "achievement-order", Some(&String::static_variant_type()), - &order_value.to_variant(), + &initial_order.as_action_target().to_variant(), ); order_action.connect_activate(glib::clone!( #[strong] diff --git a/src/gui_frontend/stat_view.rs b/src/gui_frontend/stat_view.rs index af5f9cd..9bd5993 100644 --- a/src/gui_frontend/stat_view.rs +++ b/src/gui_frontend/stat_view.rs @@ -53,6 +53,13 @@ impl StatOrder { _ => None, } } + + fn as_action_target(self) -> &'static str { + match self { + Self::SteamDefault => "steam-default", + Self::Alphabetical => "alphabetical", + } + } } pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, StringFilter) { @@ -82,9 +89,8 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St let app_stats_sort_model = SortListModel::builder() .model(&app_stats_filter_model) .build(); - let order_value = settings.string("stat-order").to_string(); - let initial_order = - StatOrder::from_action_target(&order_value).unwrap_or(StatOrder::SteamDefault); + let initial_order = StatOrder::from_action_target(&settings.string("stat-order")) + .unwrap_or(StatOrder::SteamDefault); if matches!(initial_order, StatOrder::Alphabetical) { app_stats_sort_model.set_sorter(Some(&stat_sorter)); } @@ -92,7 +98,7 @@ pub fn create_stats_view(application: &MainApplication) -> (Frame, ListStore, St let order_action = SimpleAction::new_stateful( "stat-order", Some(&String::static_variant_type()), - &order_value.to_variant(), + &initial_order.as_action_target().to_variant(), ); order_action.connect_activate(glib::clone!( #[strong]