add batch copying

This commit is contained in:
heliguy
2024-07-20 23:13:45 -04:00
parent 4bb6c54dec
commit d2ab5f223e
4 changed files with 223 additions and 169 deletions

View File

@@ -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 {
}
}
}
}
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;
}
}

View File

@@ -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)

View File

@@ -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";

View File

@@ -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()
# Apply again
self.sorter()