diff --git a/src/user_data_page/data_subpage.blp b/src/user_data_page/data_subpage.blp index 152ab2f..adc8edc 100644 --- a/src/user_data_page/data_subpage.blp +++ b/src/user_data_page/data_subpage.blp @@ -1,8 +1,17 @@ using Gtk 4.0; using Adw 1; -template $DataSubpage : Box { - Box outer_box { +template $DataSubpage : Stack { + Adw.StatusPage loading_data { + title: _("Loading Data"); + description: _("This should only take a moment"); + child: + Spinner { + spinning: true; + } + ; + } + Box content_box { orientation: vertical; Box label_box { margin-start: 24; @@ -46,14 +55,13 @@ template $DataSubpage : Box { margin-bottom: 9; } ScrolledWindow scrolled_window { + vexpand: true; Box { orientation: vertical; - vexpand: true; Separator { margin-start: 12; margin-end: 12; - // margin-top: 9; - margin-bottom: 6; + margin-bottom: 9; } FlowBox flow_box { styles ["boxed-list"] @@ -68,4 +76,13 @@ template $DataSubpage : Box { } } } -} \ No newline at end of file + Adw.StatusPage no_data { + // Contents will be set from the subpage object + } + Adw.StatusPage no_results { + title: _("No Results Found"); + description: _("Try a different search"); + icon-name: "system-search-symbolic"; + valign: center; + } +} diff --git a/src/user_data_page/data_subpage.py b/src/user_data_page/data_subpage.py index 60b06dc..05d069c 100644 --- a/src/user_data_page/data_subpage.py +++ b/src/user_data_page/data_subpage.py @@ -6,13 +6,12 @@ from .host_info import HostInfo import subprocess @Gtk.Template(resource_path="/io/github/flattool/Warehouse/user_data_page/data_subpage.ui") -class DataSubpage(Gtk.Box): +class DataSubpage(Gtk.Stack): __gtype_name__ = 'DataSubpage' gtc = Gtk.Template.Child scrolled_window = gtc() - outer_box = gtc() label_box = gtc() subtitle_size_box = gtc() title = gtc() @@ -21,6 +20,12 @@ class DataSubpage(Gtk.Box): size_label = gtc() flow_box = gtc() + # Statuses + loading_data = gtc() + content_box = gtc() + no_data = gtc() + no_results = gtc() + def human_readable_size(self): working_size = self.total_size units = ['KB', 'MB', 'GB', 'TB'] @@ -60,27 +65,74 @@ class DataSubpage(Gtk.Box): self.size_label.set_label(self.human_readable_size()) self.spinner.set_visible(False) self.ready_to_sort_size = True - self.flow_box.invalidate_sort() + if self.sort_mode == "size": + self.flow_box.invalidate_sort() + self.set_visible_child(self.content_box) + + def trash_handler(self, trashed_box): + self.flow_box.remove(trashed_box) + if not self.flow_box.get_child_at_index(0): + self.set_visible_child(self.no_data) + + def set_selection_mode(self, is_enabled): + self.selected_boxes.clear() + idx = 0 + while box := self.flow_box.get_child_at_index(idx): + idx += 1 + box = box.get_child() + if not is_enabled: + GLib.idle_add(lambda *_, box=box: box.check_button.set_active(False)) + GLib.idle_add(lambda *_, box=box: box.check_button.set_visible(is_enabled)) + + def box_select_handler(self, _, box): + box = box.get_child() + if not box.check_button.get_visible(): + return + cb = box.check_button + if cb.get_active(): + cb.set_active(False) + self.selected_boxes.remove(box) + else: + cb.set_active(True) + self.selected_boxes.append(box) + + total = len(self.selected_boxes) + self.parent_page.copy_button.set_sensitive(total) + self.parent_page.trash_button.set_sensitive(total) + + def select_all_handler(self, *args): + idx = 0 + while box := self.flow_box.get_child_at_index(idx): + idx += 1 + self.box_select_handler(None, box) def generate_list(self, flatpaks, data): self.boxes.clear() + self.selected_boxes.clear() self.ready_to_sort_size = False self.finished_boxes = 0 self.total_size = 0 self.total_items = len(data) - self.subtitle.set_label(_("{} Items").format(self.total_items)) + + if self.total_items == 1: + self.subtitle.set_label(_("1 Item")) + else: + self.subtitle.set_label(_("{} Items").format(self.total_items)) + self.min_horizontal_label_width = self.label_box.get_preferred_size()[1].width if flatpaks: for i, pak in enumerate(flatpaks): - box = DataBox(self.parent_page.toast_overlay, pak.info["name"], pak.info["id"], pak.data_path, pak.icon_path, self.box_size_callback, lambda trashed_box: self.flow_box.remove(trashed_box)) + box = DataBox(self.parent_page.toast_overlay, pak.info["name"], pak.info["id"], pak.data_path, pak.icon_path, self.box_size_callback, self.trash_handler) self.boxes.append(box) self.flow_box.append(box) else: for i, folder in enumerate(data): - box = DataBox(self.parent_page.toast_overlay, folder.split('.')[-1], folder, f"{HostInfo.home}/.var/app/{folder}", None, self.box_size_callback, lambda trashed_box: self.flow_box.remove(trashed_box)) + box = DataBox(self.parent_page.toast_overlay, folder.split('.')[-1], folder, f"{HostInfo.home}/.var/app/{folder}", None, self.box_size_callback, self.trash_handler) self.flow_box.append(box) child = self.flow_box.get_child_at_index(i) child.set_focusable(False) + + self.flow_box.connect("child-activated", self.box_select_handler) idx = 0 while box := self.flow_box.get_child_at_index(idx): @@ -90,13 +142,34 @@ class DataSubpage(Gtk.Box): child.set_focusable(False) child.row.set_focusable(child.check_button.get_visible()) + if idx == 0: + self.set_visible_child(self.no_data) + elif self.sort_mode != "size": + self.set_visible_child(self.content_box) + def filter_func(self, box): search_text = self.parent_page.search_entry.get_text().lower() box = box.get_child() - return search_text in box.title.lower() or search_text in box.subtitle.lower() + if search_text in box.title.lower() or search_text in box.subtitle.lower(): + self.is_result = True + return True def on_invalidate(self, box): + current_status = self.get_visible_child() + if not current_status is self.no_results: + self.prev_status = self.get_visible_child() + + self.is_result = False self.flow_box.invalidate_filter() + if self.is_result: + self.set_visible_child(self.prev_status) + else: + self.set_visible_child(self.no_results) + + if self.parent_page.search_entry.get_text().lower() != "" and self.total_items == 0: + self.set_visible_child(self.no_results) + elif self.total_items == 0: + self.set_visible_child(self.no_data) def label_orientation_handler(self, adj): current_page_width = adj.get_upper() - 24 @@ -106,7 +179,7 @@ class DataSubpage(Gtk.Box): else: GLib.idle_add(lambda *_: self.label_box.set_orientation(Gtk.Orientation.HORIZONTAL)) - def __init__(self, title, parent_page, main_window, **kwargs): + def __init__(self, title, parent_page, is_active, main_window, **kwargs): super().__init__(**kwargs) GLib.idle_add(lambda *_: self.title.set_label(title)) @@ -117,19 +190,32 @@ class DataSubpage(Gtk.Box): # Extra Object Creation self.main_window = main_window self.parent_page = parent_page + # self.is_active = is_active self.sort_mode = "" self.sort_ascend = False self.total_size = 0 self.total_items = 0 self.boxes = [] + self.selected_boxes = [] self.ready_to_sort_size = False self.finished_boxes = 0 self.min_horizontal_label_width = self.label_box.get_preferred_size()[1].width + self.is_result = False + self.prev_status = None # Apply self.flow_box.set_sort_func(self.sort_func) self.flow_box.set_filter_func(self.filter_func) + if is_active: + self.no_data.set_icon_name("error-symbolic") + self.no_data.set_title(_("No Active Data")) + self.no_data.set_description(_("Warehouse cannot see any active user data or your system has no active user data present")) + else: + self.no_data.set_icon_name("check-plain-symbolic") + self.no_data.set_title(_("No Leftover Data")) + self.no_data.set_description(_("There is no leftover user data")) + # Connections parent_page.search_entry.connect("search-changed", self.on_invalidate) diff --git a/src/user_data_page/user_data_page.blp b/src/user_data_page/user_data_page.blp index 6d934d4..1f36697 100644 --- a/src/user_data_page/user_data_page.blp +++ b/src/user_data_page/user_data_page.blp @@ -38,26 +38,13 @@ template $UserDataPage : Adw.BreakpointBin { tooltip-text: _("Search User Data"); } [end] - MenuButton active_sort_button { - popover: active_sort_pop; + MenuButton sort_button { + popover: sort_pop; icon-name: "vertical-arrows-long-symbolic"; tooltip-text: _("Sort User Data"); } [end] - ToggleButton active_select_button { - icon-name: "selection-mode-symbolic"; - tooltip-text: _("Select User Data"); - } - [end] - MenuButton leftover_sort_button { - visible: false; - popover: leftover_sort_pop; - icon-name: "vertical-arrows-long-symbolic"; - tooltip-text: _("Sort User Data"); - } - [end] - ToggleButton leftover_select_button { - visible: false; + ToggleButton select_button { icon-name: "selection-mode-symbolic"; tooltip-text: _("Select User Data"); } @@ -76,6 +63,7 @@ template $UserDataPage : Adw.BreakpointBin { } [bottom] Revealer revealer { + reveal-child: bind select_button.active; transition-type: slide_up; [center] Box bottom_bar { @@ -91,6 +79,7 @@ template $UserDataPage : Adw.BreakpointBin { } } Button copy_button { + sensitive: false; styles ["raised"] Adw.ButtonContent { icon-name: "edit-copy-symbolic"; @@ -98,11 +87,12 @@ template $UserDataPage : Adw.BreakpointBin { can-shrink: true; } } - Button uninstall_button { + Button trash_button { + sensitive: false; styles ["raised"] Adw.ButtonContent { icon-name: "user-trash-symbolic"; - label: _("Uninstall"); + label: _("Move to Trash"); can-shrink: true; } } @@ -121,7 +111,7 @@ template $UserDataPage : Adw.BreakpointBin { } } -Popover active_sort_pop { +Popover sort_pop { styles ["menu"] Box { orientation: vertical; @@ -132,7 +122,7 @@ Popover active_sort_pop { Box { homogeneous: true; spacing: 3; - ToggleButton active_asc { + ToggleButton sort_ascend { active: true; styles ["flat"] Adw.ButtonContent { @@ -140,8 +130,8 @@ Popover active_sort_pop { label: _("Ascending"); } } - ToggleButton active_dsc { - group: active_asc; + ToggleButton sort_descend { + group: sort_ascend; styles ["flat"] Adw.ButtonContent { icon-name: "view-sort-descending-symbolic"; @@ -154,7 +144,7 @@ Popover active_sort_pop { Box { homogeneous: true; spacing: 3; - ToggleButton active_sort_name { + ToggleButton sort_name { active: true; styles ["flat"] Adw.ButtonContent { @@ -162,77 +152,16 @@ Popover active_sort_pop { label: _("Name"); } } - ToggleButton active_sort_id { - group: active_sort_name; + ToggleButton sort_id { + group: sort_name; styles ["flat"] Adw.ButtonContent { icon-name: "tag-outline-symbolic"; label: _("ID"); } } - ToggleButton active_sort_size { - group: active_sort_name; - styles ["flat"] - Adw.ButtonContent { - icon-name: "harddisk-symbolic"; - label: _("Size"); - } - } - } - } -} - -Popover leftover_sort_pop { - styles ["menu"] - Box { - orientation: vertical; - margin-start: 6; - margin-end: 6; - margin-top: 6; - margin-bottom: 6; - Box { - homogeneous: true; - spacing: 3; - ToggleButton leftover_asc { - active: true; - styles ["flat"] - Adw.ButtonContent { - icon-name: "view-sort-ascending-symbolic"; - label: _("Ascending"); - } - } - ToggleButton leftover_dsc { - group: leftover_asc; - styles ["flat"] - Adw.ButtonContent { - icon-name: "view-sort-descending-symbolic"; - label: _("Descending"); - } - } - } - Separator { - } - Box { - homogeneous: true; - spacing: 3; - ToggleButton leftover_sort_name { - active: true; - styles ["flat"] - Adw.ButtonContent { - icon-name: "font-x-generic-symbolic"; - label: _("Name"); - } - } - ToggleButton leftover_sort_id { - group: leftover_sort_name; - styles ["flat"] - Adw.ButtonContent { - icon-name: "tag-outline-symbolic"; - label: _("ID"); - } - } - ToggleButton leftover_sort_size { - group: leftover_sort_name; + ToggleButton sort_size { + group: sort_name; styles ["flat"] Adw.ButtonContent { icon-name: "harddisk-symbolic"; diff --git a/src/user_data_page/user_data_page.py b/src/user_data_page/user_data_page.py index 9d4767f..885d6d5 100644 --- a/src/user_data_page/user_data_page.py +++ b/src/user_data_page/user_data_page.py @@ -14,27 +14,22 @@ class UserDataPage(Adw.BreakpointBin): switcher_bar = gtc() sidebar_button = gtc() search_button = gtc() - active_select_button = gtc() - active_sort_button = gtc() - leftover_select_button = gtc() - leftover_sort_button = gtc() + select_button = gtc() + sort_button = gtc() search_entry = gtc() toast_overlay = gtc() stack = gtc() revealer = gtc() - active_asc = gtc() - active_dsc = gtc() - active_sort_name = gtc() - active_sort_id = gtc() - active_sort_size = gtc() - - leftover_asc = gtc() - leftover_dsc = gtc() - leftover_sort_name = gtc() - leftover_sort_id = gtc() - leftover_sort_size = gtc() + sort_ascend = gtc() + sort_descend = gtc() + sort_name = gtc() + sort_id = gtc() + sort_size = gtc() + select_all_button = gtc() + copy_button = gtc() + trash_button = gtc() # Referred to in the main window # It is used to determine if a new page should be made or not @@ -55,10 +50,12 @@ class UserDataPage(Adw.BreakpointBin): self.leftover_data.append(folder) def start_loading(self, *args): - self.adp.size_label.set_label("Loading Size…") + self.adp.set_visible_child(self.adp.loading_data) + self.adp.size_label.set_label("Loading Size") self.adp.spinner.set_visible(True) self.adp.flow_box.remove_all() - self.ldp.size_label.set_label("Loading Size…") + self.ldp.set_visible_child(self.ldp.loading_data) + self.ldp.size_label.set_label("Loading Size") self.ldp.spinner.set_visible(True) self.ldp.flow_box.remove_all() @@ -70,17 +67,68 @@ class UserDataPage(Adw.BreakpointBin): Gio.Task.new(None, None, callback).run_in_thread(self.sort_data) - def switch_view_handler(self, page): - self.active_select_button.set_visible(page is self.adp) - self.active_sort_button.set_visible(page is self.adp) - self.leftover_select_button.set_visible(page is self.ldp) - self.leftover_sort_button.set_visible(page is self.ldp) - self.active_select_button.set_active(False) - self.leftover_select_button.set_active(False) - self.revealer_handler() + def sorter(self, button=None): + if button and not button.get_active(): + return - def revealer_handler(self, *args): - self.revealer.set_reveal_child(self.active_select_button.get_active() or self.leftover_select_button.get_active()) + if self.sort_name.get_active(): + self.adp.sort_mode = "name" + self.ldp.sort_mode = "name" + elif self.sort_id.get_active(): + self.adp.sort_mode = "id" + self.ldp.sort_mode = "id" + elif self.sort_size.get_active(): + self.adp.sort_mode = "size" + self.ldp.sort_mode = "size" + + + self.adp.sort_ascend = self.sort_ascend.get_active() + self.ldp.sort_ascend = self.sort_ascend.get_active() + + self.adp.flow_box.invalidate_sort() + self.ldp.flow_box.invalidate_sort() + + def view_change_handler(self, *args): + child = self.stack.get_visible_child() + if child.total_size == 0: + self.search_button.set_active(False) + self.search_button.set_sensitive(False) + self.select_button.set_active(False) + self.select_button.set_sensitive(False) + self.sort_button.set_active(False) + self.sort_button.set_sensitive(False) + else: + self.search_button.set_sensitive(True) + self.select_button.set_sensitive(True) + self.sort_button.set_sensitive(True) + + has_selected = len(child.selected_boxes) > 0 + self.copy_button.set_sensitive(has_selected) + self.trash_button.set_sensitive(has_selected) + + def select_toggle_handler(self, *args): + active = self.select_button.get_active() + self.adp.set_selection_mode(active) + self.ldp.set_selection_mode(active) + if not active: + self.copy_button.set_sensitive(False) + self.trash_button.set_sensitive(False) + + def select_all_handler(self, *args): + child = self.stack.get_visible_child() + child.select_all_handler() + + def copy_handler(self, *args): + child = self.stack.get_visible_child() + to_copy = "" + for box in child.selected_boxes: + to_copy += "\n" + box.data_path + + if len(to_copy) > 0: + HostInfo.clipboard.set(to_copy.replace("\n", "", 1)) + self.toast_overlay.add_toast(Adw.Toast(title=_("Copied paths"))) + else: + self.toast_overlay.add_toast(ErrorToast(_("Could not copy paths"), _("No boxes were selected")).toast) def __init__(self, main_window, **kwargs): super().__init__(**kwargs) @@ -88,8 +136,8 @@ class UserDataPage(Adw.BreakpointBin): # Extra Object Creation self.__class__.instance = self # self.adj = self.scrolled_window.get_vadjustment() - self.adp = DataSubpage(_("Active Data"), self, main_window) - self.ldp = DataSubpage(_("Leftover Data"), self, main_window) + self.adp = DataSubpage(_("Active Data"), self, True, main_window) + self.ldp = DataSubpage(_("Leftover Data"), self, False, main_window) self.data_flatpaks = [] self.active_data = [] self.leftover_data = [] @@ -115,44 +163,18 @@ class UserDataPage(Adw.BreakpointBin): # self.sidebar_button.connect("clicked", lambda *_, ms=main_window.main_split: ms.set_show_sidebar(not ms.get_show_sidebar() if not ms.get_collapsed() else True)) main_window.main_split.connect("notify::show-sidebar", lambda *_: self.sidebar_button.set_active(ms.get_show_sidebar())) self.sidebar_button.connect("toggled", lambda *_: ms.set_show_sidebar(self.sidebar_button.get_active())) - self.stack.connect("notify::visible-child", lambda *_: self.switch_view_handler(self.stack.get_visible_child())) - self.active_select_button.connect("toggled", self.revealer_handler) - self.leftover_select_button.connect("toggled", self.revealer_handler) + self.stack.connect("notify::visible-child", self.view_change_handler) + + self.select_button.connect("toggled", self.select_toggle_handler) - def sorter(button=None): - if button and not button.get_active(): - return + self.select_all_button.connect("clicked", self.select_all_handler) + self.copy_button.connect("clicked", self.copy_handler) - if self.active_sort_name.get_active(): - self.adp.sort_mode = "name" - elif self.active_sort_id.get_active(): - self.adp.sort_mode = "id" - elif self.active_sort_size.get_active(): - self.adp.sort_mode = "size" - - if self.leftover_sort_name.get_active(): - self.ldp.sort_mode = "name" - elif self.leftover_sort_id.get_active(): - self.ldp.sort_mode = "id" - elif self.leftover_sort_size.get_active(): - self.ldp.sort_mode = "size" + self.sort_ascend.connect("clicked", self.sorter) + self.sort_descend.connect("clicked", self.sorter) + self.sort_name.connect("clicked", self.sorter) + self.sort_id.connect("clicked", self.sorter) + self.sort_size.connect("clicked", self.sorter) - self.adp.sort_ascend = self.active_asc.get_active() - self.ldp.sort_ascend = self.leftover_asc.get_active() - - self.adp.flow_box.invalidate_sort() - self.ldp.flow_box.invalidate_sort() - - self.active_asc.connect("clicked", sorter) - self.active_dsc.connect("clicked", sorter) - self.active_sort_name.connect("clicked", sorter) - self.active_sort_id.connect("clicked", sorter) - self.active_sort_size.connect("clicked", sorter) - - self.leftover_asc.connect("clicked", sorter) - self.leftover_dsc.connect("clicked", sorter) - self.leftover_sort_name.connect("clicked", sorter) - self.leftover_sort_id.connect("clicked", sorter) - self.leftover_sort_size.connect("clicked", sorter) - - sorter() \ No newline at end of file + # Apply again + self.sorter() \ No newline at end of file