From 687edd639b9be14948e409f4cdd7db4ea4179b72 Mon Sep 17 00:00:00 2001 From: robojerk Date: Mon, 9 Jun 2025 20:07:58 -0700 Subject: [PATCH] Initial commit: I had claude do some AI slop to add qt qui --- requirements.txt | 9 + src/flatpost.py | 4583 +------------------------------------------ src/gui/__init__.py | 4 + src/gui/base.py | 101 + src/gui/factory.py | 49 + src/gui/gtk_gui.py | 135 ++ src/gui/qt_gui.py | 127 ++ 7 files changed, 495 insertions(+), 4513 deletions(-) create mode 100644 requirements.txt create mode 100644 src/gui/__init__.py create mode 100644 src/gui/base.py create mode 100644 src/gui/factory.py create mode 100644 src/gui/gtk_gui.py create mode 100644 src/gui/qt_gui.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0b34cf5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# Common dependencies +requests>=2.28.0 + +# Platform-specific dependencies +# Windows +PySide6>=6.5.0; platform_system=="Windows" + +# Linux/Unix +PyGObject>=3.42.0; platform_system!="Windows" \ No newline at end of file diff --git a/src/flatpost.py b/src/flatpost.py index 1247d27..b07c4e0 100755 --- a/src/flatpost.py +++ b/src/flatpost.py @@ -1,89 +1,44 @@ #!/usr/bin/python3 -import gi import sys -gi.require_version("Gtk", "3.0") -gi.require_version("GLib", "2.0") -gi.require_version("Flatpak", "1.0") -gi.require_version('GdkPixbuf', '2.0') -from gi.repository import Gtk, Gio, Gdk, GLib, GdkPixbuf -import flatpost.fp_turbo as fp_turbo -from flatpost.fp_turbo import AppStreamComponentKind as AppKind +import os import json import threading import subprocess from pathlib import Path from html.parser import HTMLParser import requests -import os import pwd from datetime import datetime -class MainWindow(Gtk.Window): - def __init__(self, system_mode=False, system_only_mode=False): - app_title = "Flatpost (user mode)" - if system_only_mode: - app_title = "Flatpost (system-only mode)" - elif system_mode: - app_title = "Flatpost (system mode)" - super().__init__(title=app_title) +import flatpost.fp_turbo as fp_turbo +from flatpost.fp_turbo import AppStreamComponentKind as AppKind +from flatpost.gui.factory import GUIFactory + +class FlatpostApp: + def __init__(self, system_mode=False, system_only_mode=False, gui_backend="gtk"): self.system_mode = system_mode self.system_only_mode = system_only_mode - self.system_switch = Gtk.Switch() - # Create system mode label - self.system_label = Gtk.Label(label="System Mode") - if self.system_mode: - self.system_switch.set_active(True) - if self.system_only_mode: - self.system_switch.set_active(True) - self.system_switch.set_sensitive(False) - # Step 1: Verify file exists and is accessible - icon_path = "/usr/share/icons/hicolor/1024x1024/apps/com.flatpost.flatpostapp.png" - if not os.path.exists(icon_path): - print("ERROR: Icon file not found!") - return - - # Step 2: Test loading individual pixbufs - try: - # Try loading smallest size first - self.pixbuf16 = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 16, 16, True) - - # Now load full set of sizes - self.pixbuf24 = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 24, 24, True) - self.pixbuf32 = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 32, 32, True) - self.pixbuf48 = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 48, 48, True) - self.pixbuf64 = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 64, 64, True) - self.pixbuf128 = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 128, 128, True) - - Gtk.Window.set_default_icon(self.pixbuf48) - - # Set the icon list - self.set_icon_list([self.pixbuf16, self.pixbuf24, self.pixbuf32, self.pixbuf48, self.pixbuf64, self.pixbuf128]) - - except Exception as e: - print(f"ERROR loading icon: {str(e)}") - - - # Store search results as an instance variable + + # Create GUI instance + self.gui = GUIFactory.create_gui( + backend=gui_backend, + system_mode=system_mode, + system_only_mode=system_only_mode + ) + + # Initialize data self.all_apps = [] self.current_component_type = None - self.category_results = [] # Initialize empty list + self.category_results = [] self.subcategory_buttons = {} - self.collection_results = [] # Initialize empty list - self.installed_results = [] # Initialize empty list - self.updates_results = [] # Initialize empty list - self.current_page = None # Track current page - self.current_group = None # Track current group (system/collections/categories) - - # Set window size - self.set_default_size(1280, 720) - - # Enable drag and drop - self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.drag_dest_add_uri_targets() - self.connect("drag-data-received", self.on_drag_data_received) - - # Define category groups and their titles + self.collection_results = [] + self.installed_results = [] + self.updates_results = [] + self.current_page = None + self.current_group = None + + # Define category groups self.category_groups = { 'system': { 'installed': 'Installed', @@ -109,4453 +64,55 @@ class MainWindow(Gtk.Window): 'utility': 'Utilities' } } - - # Define subcategories - self.subcategory_groups = { - 'audiovideo': { - 'audiovideoediting': 'Audio & Video Editing', - 'discburning': 'Disc Burning', - 'midi': 'Midi', - 'mixer': 'Mixer', - 'player': 'Player', - 'recorder': 'Recorder', - 'sequencer': 'Sequencer', - 'tuner': 'Tuner', - 'tv': 'TV' - }, - 'development': { - 'building': 'Building', - 'database': 'Database', - 'debugger': 'Debugger', - 'guidesigner': 'GUI Designer', - 'ide': 'IDE', - 'profiling': 'Profiling', - 'revisioncontrol': 'Revision Control', - 'translation': 'Translation', - 'webdevelopment': 'Web Development' - }, - 'game': { - 'actiongame': 'Action Games', - 'adventuregame': 'Adventure Games', - 'arcadegame': 'Arcade Games', - 'blocksgame': 'Blocks Games', - 'boardgame': 'Board Games', - 'cardgame': 'Card Games', - 'emulator': 'Emulators', - 'kidsgame': 'Kids\' Games', - 'logicgame': 'Logic Games', - 'roleplaying': 'Role Playing', - 'shooter': 'Shooter', - 'simulation': 'Simulation', - 'sportsgame': 'Sports Games', - 'strategygame': 'Strategy Games' - }, - 'graphics': { - '2dgraphics': '2D Graphics', - '3dgraphics': '3D Graphics', - 'ocr': 'OCR', - 'photography': 'Photography', - 'publishing': 'Publishing', - 'rastergraphics': 'Raster Graphics', - 'scanning': 'Scanning', - 'vectorgraphics': 'Vector Graphics', - 'viewer': 'Viewer' - }, - 'network': { - 'chat': 'Chat', - 'email': 'Email', - 'feed': 'Feed', - 'filetransfer': 'File Transfer', - 'hamradio': 'Ham Radio', - 'instantmessaging': 'Instant Messaging', - 'ircclient': 'IRC Client', - 'monitor': 'Monitor', - 'news': 'News', - 'p2p': 'P2P', - 'remoteaccess': 'Remote Access', - 'telephony': 'Telephony', - 'videoconference': 'Video Conference', - 'webbrowser': 'Web Browser', - 'webdevelopment': 'Web Development' - }, - 'office': { - 'calendar': 'Calendar', - 'chart': 'Chart', - 'contactmanagement': 'Contact Management', - 'database': 'Database', - 'dictionary': 'Dictionary', - 'email': 'Email', - 'finance': 'Finance', - 'presentation': 'Presentation', - 'projectmanagement': 'Project Management', - 'publishing': 'Publishing', - 'spreadsheet': 'Spreadsheet', - 'viewer': 'Viewer', - 'wordprocessor': 'Word Processor' - }, - 'system': { - 'emulator': 'Emulators', - 'filemanager': 'File Manager', - 'filesystem': 'Filesystem', - 'filetools': 'File Tools', - 'monitor': 'Monitor', - 'security': 'Security', - 'terminalemulator': 'Terminal Emulator' - }, - 'utility': { - 'accessibility': 'Accessibility', - 'archiving': 'Archiving', - 'calculator': 'Calculator', - 'clock': 'Clock', - 'compression': 'Compression', - 'filetools': 'File Tools', - 'telephonytools': 'Telephony Tools', - 'texteditor': 'Text Editor', - 'texttools': 'Text Tools' - } - } - - # Add CSS provider for custom styling - css_provider = Gtk.CssProvider() - css_provider.load_from_data(""" - .panel-header { - font-size: 24px; - font-weight: bold; - padding: 12px; - } - .top-bar { - margin: 0px; - padding: 0px; - border: 0px; - } - - # revealer and tool_box are hidden components inside GtkSearchBar - # This gets rid of the stupid grey line the tool_box causes. - #search_hidden_revealer, - #search_hidden_tool_box { - background: transparent; - border: none; - box-shadow: none; - background-image: none; - border-image: none; - padding: 0px; - margin: 0px; - } - - .category-group-header { - padding: 6px; - margin: 0; - font-weight: bold; - } - .category-button { - border: 0px; - padding: 6px; - margin: 0; - background: none; - } - - .pan-button { - border: 0px; - padding: 6px; - margin: 0; - background: none; - box-shadow: none; - } - - .no-scroll-bars scrollbar { - min-width: 0px; - opacity: 0; - margin-top: -20px; - } - - .subcategory-group-header { - padding: 6px; - margin: 0; - } - .subcategory-group-header active { - padding: 6px; - margin: 0; - font-weight: bold; - } - .subcategory-button { - border: 0px; - padding: 6px; - margin: 0; - background: none; - } - .subcategory-button.active { - font-weight: bold; - } - - .subcategories-scroll { - border: none; - background-color: transparent; - min-height: 40px; - } - - .repo-item { - padding: 6px; - margin: 2px; - border-bottom: 1px solid #eee; - } - .repo-delete-button { - border: none; - padding: 6px; - margin-left: 6px; - } - .repo-list-header { - font-size: 18px; - padding: 5px;; - } - - .app-window { - border: 0px; - margin: 0px; - padding-right: 20px; - background: none; - } - - .app-list-header { - font-size: 18px; - padding-top: 4px; - padding-bottom: 4px; - } - .app-list-summary { - padding-top: 2px; - padding-bottom: 2px; - } - .app-page-header { - font-size: 24px; - font-weight: bold; - padding: 12px; - } - - .app-repo-label { - font-size: 0.8em; - } - - .app-type-label { - font-size: 0.8em; - } - .updates_available_bar { - background-color: #18A3FF; - padding: 4px; - } - .screenshot-bullet { - color: #18A3FF; - font-size: 30px; - padding: 4px; - border-radius: 50%; - transition: all 0.2s ease; - } - .screenshot-bullet:hover { - background-color: rgba(24, 163, 255, 0.2); - } - .details-window { - border: 0px; - margin: 0px; - padding: 20px; - background: none; - } - .details-textview { - background-color: transparent; - border-width: 0; - border-radius: 0; - } - .permissions-window { - border: 0px; - margin: 0px; - padding: 20px; - background: none; - } - .permissions-header-label { - font-weight: bold; - font-size: 24px; - } - .permissions-row { - padding: 4px; - background: none; - } - .permissions-item-label { - font-weight: bold; - font-size: 14px; - } - .permissions-item-summary { - font-size: 12px; - } - .permissions-global-indicator { - background: none; - } - .permissions-spacing-box { - background: none; - padding: 5px; - } - .permissions-path-vbox { - padding: 6px; - } - .permissions-path { - padding: 6px; - } - .permissions-path-text text { - color: @search_fg_color; - } - - .permissions-path-text textview { - border-radius: 4px; - padding: 8px; - background-color: @search_bg_color; - border: 1px solid @search_border_color; - margin: 8px; - } - - .permissions-path-text border { - background-color: @search_border_color; - border-radius: 4px; - } - - .permissions-path-scroll { - padding: 6px; - } - .permissions-bus-box { - padding-left: 8px; - background: none; - } - combobox, - combobox box, - combobox button { - font-size: 12px; - padding-top: 0px; - padding-bottom: 0px; - margin: 0px; - min-height: 0px; - } - button { - padding-top: 0px; - padding-bottom: 0px; - margin: 0px; - min-height: 0px; - } - .app-action-button { - border-radius: 4px; - padding: 8px; - transition: all 0.2s ease; - } - .app-action-button:hover { - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - } - .app-url-label { - color: #18A3FF; - text-decoration: underline; - } - - .app-url-label:hover { - text-decoration: none; - } - """) - - # Add CSS provider to the default screen - Gtk.StyleContext.add_provider_for_screen( - Gdk.Screen.get_default(), - css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 600 - ) - - self.refresh_data() - - # Create main layout - self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - - self.add(self.main_box) - - # Create_header_bar - self.create_header_bar() - - # Create panels - self.create_panels() - - # Select Trending by default - self.select_default_category() - - def on_drag_data_received(self, widget, context, x, y, data, info, time): - """Handle drag and drop events""" - # Check if data is a URI list - if isinstance(data, int): - return - uri = data.get_uris()[0] - file_path = Gio.File.new_for_uri(uri).get_path() - if file_path and file_path.endswith('.flatpakref'): - self.handle_flatpakref_file(file_path) - if file_path and file_path.endswith('.flatpakrepo'): - self.handle_flatpakrepo_file(file_path) - context.finish(True, False, time) - - def handle_flatpakref_file(self, file_path): - """Handle .flatpakref file installation""" - self.on_install_clicked(None, file_path) - - def handle_flatpakrepo_file(self, file_path): - """Handle .flatpakrepo file installation""" - self.on_add_repo_button_clicked(None, file_path) - - def create_header_bar(self): - # Create horizontal bar - self.top_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.top_bar.get_style_context().add_class("top-bar") - self.top_bar.set_hexpand(True) - self.top_bar.set_vexpand(False) - self.top_bar.set_spacing(6) - self.top_bar.set_margin_top(0) - self.top_bar.set_margin_bottom(0) - self.top_bar.set_margin_start(0) - self.top_bar.set_margin_end(0) - + + # Setup GUI + self.setup_gui() + + def setup_gui(self): + """Setup the GUI components.""" + # Create header bar + header = self.gui.create_header_bar() + + # Create main panels + main_box = self.gui.create_panels() + + # Create left panel (categories) + left_panel = self.gui.create_applications_panel("Categories") + main_box.addWidget(left_panel) + + # Create right panel (applications) + right_panel = self.gui.create_applications_panel("Applications") + main_box.addWidget(right_panel) + # Add search bar - self.searchbar = Gtk.SearchBar() - self.searchbar.set_show_close_button(False) - self.searchbar.set_hexpand(False) - self.searchbar.set_vexpand(False) - self.searchbar.set_margin_top(0) - self.searchbar.set_margin_bottom(0) - self.searchbar.set_margin_start(0) - self.searchbar.set_margin_end(0) - revealer = self.searchbar.get_children()[0] - revealer.set_name("search_hidden_revealer") - revealer.set_margin_top(0) - revealer.set_margin_bottom(0) - revealer.set_margin_start(0) - revealer.set_margin_end(0) - tool_box = revealer.get_children()[0] - tool_box.set_name("search_hidden_tool_box") - tool_box.set_margin_top(0) - tool_box.set_margin_bottom(0) - tool_box.set_margin_start(0) - tool_box.set_margin_end(0) - - # Create search entry with icon - searchentry = Gtk.SearchEntry() - searchentry.set_placeholder_text("Search applications...") - searchentry.set_icon_from_gicon(Gtk.EntryIconPosition.PRIMARY, - Gio.Icon.new_for_string('system-search-symbolic')) - - # Connect search entry signals - searchentry.connect("search-changed", self.on_search_changed) - searchentry.connect("activate", self.on_search_activate) - - # Connect search entry to search bar - self.searchbar.connect_entry(searchentry) - self.searchbar.add(searchentry) - self.searchbar.set_search_mode(True) - - self.top_bar.pack_start(self.searchbar, False, False, 0) - - self.component_type_combo_label = Gtk.Label(label="Search Type:") - # Create component type dropdown - self.component_type_combo = Gtk.ComboBoxText() - self.component_type_combo.props.valign = Gtk.Align.CENTER - self.component_type_combo.set_hexpand(False) - self.component_type_combo.set_vexpand(False) - self.component_type_combo.set_wrap_width(1) - self.component_type_combo.set_size_request(150, 32) # Set width in pixels - self.component_type_combo.connect("changed", self.on_component_type_changed) - - # Add "ALL" option first - self.component_type_combo.append_text("ALL") - - # Add all component types - for kind in AppKind: - if kind != AppKind.UNKNOWN: - self.component_type_combo.append_text(kind.name) - - # Select "ALL" by default - self.component_type_combo.set_active(0) - - # Add dropdown to header bar - self.top_bar.pack_start(self.component_type_combo_label, False, False, 0) - self.top_bar.pack_start(self.component_type_combo, False, False, 0) - - # Add about button - about_button = Gtk.Button() - about_button.set_size_request(26, 26) # 40x40 pixels - about_button.get_style_context().add_class("app-action-button") - about_button.set_tooltip_text("About") - about_button_icon = Gio.Icon.new_for_string('help-about-symbolic') - about_button.set_image(Gtk.Image.new_from_gicon(about_button_icon, Gtk.IconSize.BUTTON)) - about_button.connect("clicked", self.on_about_clicked) - - - # Add global overrides button - global_overrides_button = Gtk.Button() - global_overrides_button.set_size_request(26, 26) # 40x40 pixels - global_overrides_button.get_style_context().add_class("app-action-button") - global_overrides_button.set_tooltip_text("Global Setting Overrides") - global_overrides_button_icon = Gio.Icon.new_for_string('applications-system-symbolic') - global_overrides_button.set_image(Gtk.Image.new_from_gicon(global_overrides_button_icon, Gtk.IconSize.BUTTON)) - global_overrides_button.connect("clicked", self.global_on_options_clicked) - - # Add refresh metadata button - refresh_metadata_button = Gtk.Button() - refresh_metadata_button.set_size_request(26, 26) # 40x40 pixels - refresh_metadata_button.get_style_context().add_class("app-action-button") - refresh_metadata_button.set_tooltip_text("Refresh metadata") - refresh_metadata_button_icon = Gio.Icon.new_for_string('view-refresh-symbolic') - refresh_metadata_button.set_image(Gtk.Image.new_from_gicon(refresh_metadata_button_icon, Gtk.IconSize.BUTTON)) - refresh_metadata_button.connect("clicked", self.on_refresh_metadata_button_clicked) - - parent_system_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - parent_system_box.set_vexpand(True) - # Create system mode switch box - system_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - system_box.set_hexpand(False) - system_box.set_vexpand(False) - system_box.set_margin_top(0) - system_box.set_margin_bottom(0) - system_box.set_margin_start(0) - system_box.set_margin_end(6) - system_box.set_halign(Gtk.Align.CENTER) - - self.system_switch.props.valign = Gtk.Align.CENTER - self.system_switch.connect("notify::active", self.on_system_mode_toggled) - self.system_switch.set_hexpand(False) - self.system_switch.set_vexpand(False) - - # Pack switch and label - if not self.system_only_mode: - system_box.pack_end(self.system_switch, False, False, 0) - system_box.pack_end(self.system_label, False, False, 0) - system_box.pack_end(about_button, False, False, 0) - system_box.pack_end(global_overrides_button, False, False, 0) - system_box.pack_end(refresh_metadata_button, False, False, 0) - parent_system_box.pack_end(system_box, False, False, 0) - # Add system controls to header - self.top_bar.pack_end(parent_system_box, False, False, 0) - - # Add the top bar to the main box - self.main_box.pack_start(self.top_bar, False, True, 0) - - def on_about_clicked(self, button): - """Show the about dialog with version and license information.""" - # Create the dialog - about_dialog = Gtk.Dialog( - title="About Flatpost", - parent=self, - modal=True, - destroy_with_parent=True - ) - - # Set size - about_dialog.set_default_size(400, 200) - - # Create content area - content_area = about_dialog.get_content_area() - - # Create main box for content - main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - main_box.set_border_width(12) - - # Icon - image = Gtk.Image.new_from_pixbuf(self.pixbuf64) - - # Version label - name_label = Gtk.Label(label="Flatpost") - name_label.get_style_context().add_class("permissions-header-label") - version_label = Gtk.Label(label="Version 1.0.5") - copyright_label = Gtk.Label(label=f"Copyright © 2025-{datetime.now().year} Thomas Crider") - program_label = Gtk.Label(label="This program comes with absolutely no warranty.") - - license_label = Gtk.Label(label="License:") - license_url = Gtk.Label(label="BSD 2-Clause License") - license_url.set_use_underline(True) - license_url.set_use_markup(True) - license_url.set_markup('BSD 2-Clause License') - license_event_box = Gtk.EventBox() - license_event_box.add(license_url) - license_event_box.connect("button-release-event", - lambda w, e: Gio.AppInfo.launch_default_for_uri("https://github.com/GloriousEggroll/flatpost/blob/main/LICENSE")) - - issue_label = Gtk.Label(label="Report an Issue:") - issue_url = Gtk.Label(label="https://github.com/GloriousEggroll/flatpost/issue") - issue_url.set_use_underline(True) - issue_url.set_use_markup(True) - issue_url.set_markup('https://github.com/GloriousEggroll/flatpost/issue') - issue_event_box = Gtk.EventBox() - issue_event_box.add(issue_url) - issue_event_box.connect("button-release-event", - lambda w, e: Gio.AppInfo.launch_default_for_uri("https://github.com/GloriousEggroll/flatpost/issues")) - - - - # Add all widgets - content_area.add(main_box) - main_box.pack_start(name_label, False, False, 0) - main_box.pack_start(image, False, False, 0) - main_box.pack_start(version_label, False, False, 0) - main_box.pack_start(copyright_label, False, False, 0) - main_box.pack_start(program_label, False, False, 0) - main_box.pack_start(license_label, False, False, 0) - main_box.pack_start(license_event_box, False, False, 0) - main_box.pack_start(issue_label, False, False, 0) - main_box.pack_start(issue_event_box, False, False, 0) - - # Show the dialog - about_dialog.show_all() - about_dialog.run() - about_dialog.destroy() - - def on_refresh_metadata_button_clicked(self, button): - self.refresh_data() - self.refresh_current_page() - - def on_component_type_changed(self, combo): - """Handle component type filter changes""" - selected_type = combo.get_active_text() - if selected_type: - if selected_type == "ALL": - self.current_component_type = None - else: - self.current_component_type = selected_type - else: - self.current_component_type = None - self.refresh_current_page() - - def relaunch_as_user(self): - uid = int(os.environ.get('ORIG_USER', '')) - try: - pw_record = pwd.getpwuid(uid) - username = pw_record.pw_name - user_home = pw_record.pw_dir - gid = pw_record.pw_gid - - # Drop privileges before exec - os.setgid(gid) - os.setuid(uid) - - # Update environment - os.environ["HOME"] = user_home - os.environ["LOGNAME"] = username - os.environ["USER"] = username - os.environ["XDG_RUNTIME_DIR"] = f"/run/user/{uid}" - - # Re-exec the script - script_path = Path(__file__).resolve() - os.execvp( - sys.executable, - [sys.executable, str(script_path)] - ) - - except Exception as e: - print(f"Failed to drop privileges and exec: {e}") - sys.exit(1) - - def on_system_mode_toggled(self, switch, gparam): - """Handle system mode toggle switch state changes""" - desired_state = switch.get_active() - - if desired_state: - # Get current script path - current_script = sys.argv[0] - - # Re-execute as root with system mode enabled - try: - # Construct command to re-execute with system mode enabled - script_path = Path(__file__).resolve() - os.execvp( - "pkexec", - [ - "pkexec", - "--disable-internal-agent", - "env", - f"DISPLAY={os.environ['DISPLAY']}", - f"XAUTHORITY={os.environ.get('XAUTHORITY', '')}", - f"XDG_CURRENT_DESKTOP={os.environ.get('XDG_CURRENT_DESKTOP', '').lower()}", - f"ORIG_USER={os.getuid()!s}", - f"PKEXEC_UID={os.getuid()!s}", - "G_MESSAGES_DEBUG=none", - sys.executable, - str(script_path), - '--system-mode', - ] - ) - - except subprocess.CalledProcessError: - # Authentication failed, reset switch and show error - switch.set_active(False) - dialog = Gtk.MessageDialog( - transient_for=self, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text="Authentication failed", - secondary_text="Could not enable system mode" - ) - dialog.connect("response", lambda d, r: d.destroy()) - dialog.show() - else: - try: - # Construct command to re-execute with system mode enabled - self.relaunch_as_user() - sys.exit(0) - - except subprocess.CalledProcessError: - # Authentication failed, reset switch and show error - switch.set_active(True) - dialog = Gtk.MessageDialog( - transient_for=self, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text="Authentication failed", - secondary_text="Could not enable user mode" - ) - dialog.connect("response", lambda d, r: d.destroy()) - dialog.show() - - - def populate_repo_dropdown(self): - # Get list of repositories - repos = fp_turbo.repolist(self.system_mode) - - # Clear existing items - self.repo_dropdown.remove_all() - - # Add repository names - for repo in repos: - self.repo_dropdown.append_text(repo.get_name()) - - # Connect selection changed signal - self.repo_dropdown.connect("changed", self.on_repo_selected) - - def on_repo_selected(self, dropdown): - active_index = dropdown.get_active() - if active_index != -1: - self.selected_repo = dropdown.get_model()[active_index][0] - print(f"Selected repository: {self.selected_repo}") - - def refresh_data(self): - # Create dialog and progress bar - dialog = Gtk.Dialog( - title="Fetching metadata, please wait...", - parent=self, - modal=True, - destroy_with_parent=True - ) - dialog.set_size_request(400, 100) - - progress_bar = Gtk.ProgressBar() - progress_bar.set_text("Initializing...") - progress_bar.set_show_text(True) - dialog.vbox.pack_start(progress_bar, True, True, 0) - dialog.vbox.set_spacing(12) - - # Show the dialog - dialog.show_all() - - searcher = fp_turbo.get_reposearcher(self.system_mode) - - # Define thread target function - def retrieve_metadata(): - try: - category_results, collection_results, installed_results, updates_results, all_apps = searcher.retrieve_metadata(self.system_mode) - self.category_results = category_results - self.collection_results = collection_results - self.installed_results = installed_results - self.updates_results = updates_results - self.all_apps = all_apps - except Exception as e: - dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=f"Error retrieving metadata: {str(e)}" - ) - dialog.run() - dialog.destroy() - # Start the refresh thread - refresh_thread = threading.Thread(target=retrieve_metadata) - refresh_thread.start() - def update_progress(): - while refresh_thread.is_alive(): - progress_bar.set_text("Fetching...") - progress = searcher.refresh_progress - progress_bar.set_fraction(progress / 100) - return True - else: - progress_bar.set_fraction(100 / 100) - dialog.destroy() - - # Start the progress update timer - GLib.timeout_add_seconds(0.5, update_progress) - dialog.run() - if not refresh_thread.is_alive() and dialog.is_active(): - dialog.destroy() - - def refresh_local(self): - try: - searcher = fp_turbo.get_reposearcher(self.system_mode) - installed_results, updates_results = searcher.refresh_local(self.system_mode) - self.installed_results = installed_results - self.updates_results = updates_results - except Exception as e: - message_type = Gtk.MessageType.ERROR - dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text=f"Error refreshing local data: {str(e)}" - ) - dialog.run() - dialog.destroy() - - - def create_panels(self): - # Check if panels already exist - if hasattr(self, 'left_panel') and self.left_panel.get_parent(): - self.main_box.remove(self.left_panel) - - if hasattr(self, 'right_panel') and self.right_panel.get_parent(): - self.main_box.remove(self.right_panel) - - # Create left panel with grouped categories - self.left_panel = self.create_grouped_category_panel("Categories", self.category_groups) - - # Create right panel - self.right_panel = self.create_applications_panel("Applications") - - # Create panels container - self.panels_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.panels_box.set_hexpand(True) - - # Pack the panels with proper expansion - self.panels_box.pack_start(self.left_panel, False, False, 0) # Left panel doesn't expand - self.panels_box.pack_end(self.right_panel, True, True, 0) # Right panel expands both ways - - # Add panels container to main box - self.main_box.pack_start(self.panels_box, True, True, 0) - - def create_grouped_category_panel(self, title, groups): - - # Create container for categories - panel_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - panel_container.set_spacing(6) - panel_container.set_border_width(6) - panel_container.set_size_request(300, -1) # Set fixed width - panel_container.set_hexpand(False) - panel_container.set_vexpand(True) - panel_container.set_halign(Gtk.Align.FILL) # Fill horizontally - panel_container.set_valign(Gtk.Align.FILL) # Align to top - - # Create scrollable area - scrolled_window = Gtk.ScrolledWindow() - scrolled_window.set_hexpand(True) - scrolled_window.set_vexpand(True) # Expand vertically - scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - - # Create container for categories - container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - container.set_spacing(6) - container.set_border_width(6) - container.set_halign(Gtk.Align.FILL) # Fill horizontally - container.set_valign(Gtk.Align.START) # Align to top - container.set_hexpand(True) - container.set_vexpand(False) # Expand vertically - - # Dictionary to store category widgets - self.category_widgets = {} - - # Add group headers and categories - for group_name, categories in groups.items(): - # Create a box for the header - header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - header_box.get_style_context().add_class("category-group-header") - header_box.set_hexpand(True) # Make the box expand horizontally - - # Create the label - group_header = Gtk.Label(label=group_name.upper()) - group_header.get_style_context().add_class("title-2") - group_header.set_halign(Gtk.Align.START) - - # Add the label to the box - header_box.pack_start(group_header, False, False, 0) - - # Add the box to the container - container.pack_start(header_box, False, False, 0) - container.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) - - # Store widgets for this group - self.category_widgets[group_name] = [] - - # Add categories in the group - for category, display_title in categories.items(): - # Create a clickable box for each category - category_box = Gtk.EventBox() - category_box.set_hexpand(True) - category_box.set_halign(Gtk.Align.FILL) - category_box.set_margin_top(2) - category_box.set_margin_bottom(2) - - # Create label for the category - category_label = Gtk.Label(label=display_title) - category_label.set_halign(Gtk.Align.START) - category_label.set_hexpand(True) - category_label.get_style_context().add_class("category-button") - - # Add label to the box - category_box.add(category_label) - - # Connect click event - category_box.connect("button-release-event", - lambda widget, event, cat=category, grp=group_name: - self.on_category_clicked(cat, grp)) - - # Store widget in group - self.category_widgets[group_name].append(category_box) - container.pack_start(category_box, False, False, 0) - - # Add container to scrolled window - scrolled_window.add(container) - - # Pack the scrolled window directly into main box - panel_container.pack_start(scrolled_window, True, True, 0) - - return panel_container - - def on_search_changed(self, searchentry): - """Handle search text changes""" - pass # Don't perform search on every keystroke - - - def on_search_activate(self, searchentry): - """Handle Enter key press in search""" - self.update_category_header("Search Results") - search_term = searchentry.get_text().lower() - if not search_term: - # Reset to showing all categories when search is empty - self.show_category_apps(self.current_category) - return - - # Combine all searchable fields - searchable_items = [] - for app in self.all_apps: - details = app.get_details() - searchable_items.append({ - 'text': f"{details['name']} {details['description']} {details['categories']}".lower(), - 'app': app, - 'id': details['id'].lower(), - 'name': details['name'].lower() - }) - - # Filter and rank results - filtered_apps = self.rank_search_results(search_term, searchable_items) - - # Show search results - self.show_search_results(filtered_apps) - - def rank_search_results(self, search_term, searchable_items): - """Rank search results based on match type and component type filter""" - exact_id_matches = [] - exact_name_matches = [] - partial_matches = [] - other_matches = [] - - # Get current component type filter - component_type_filter = self.current_component_type - if component_type_filter is None: - component_type_filter = None # Allow all types - - # Process each item - for item in searchable_items: - # Check if component type matches filter - if component_type_filter and item['app'].get_details()['kind'] != component_type_filter: - continue - - # Check exact ID match - if item['id'] == search_term: - exact_id_matches.append(item['app']) - continue - - # Check exact name match - if item['name'] == search_term: - exact_name_matches.append(item['app']) - continue - - # Check for partial matches longer than 5 characters - if len(search_term) > 5: - if search_term in item['id'] or search_term in item['name']: - partial_matches.append(item['app']) - continue - - # Check for other matches - if search_term in item['text']: - other_matches.append(item['app']) - - # Combine results in order of priority - return exact_id_matches + exact_name_matches + partial_matches + other_matches - - def show_search_results(self, apps): - """Display search results in the right panel""" - self.display_apps(apps) - - def on_category_clicked(self, category, group, *args): - # Remove active state and reset labels for all widgets - for group_name in self.category_widgets: - for widget in self.category_widgets[group_name]: - label = widget.get_children()[0] - label.set_use_markup(False) - - # Loop through known original titles to find a match - for grp in self.category_groups: - for key, val in self.category_groups[grp].items(): - # Escape val for comparison with possible markup in label - safe_val = GLib.markup_escape_text(val) - if safe_val in label.get_text() or val in label.get_text(): - label.set_label(val) - break - - # Add active state and markup icon - display_title = self.category_groups[group][category] - for widget in self.category_widgets[group]: - label = widget.get_children()[0] - if label.get_text() == display_title: - safe_title = GLib.markup_escape_text(display_title) - markup = f"{safe_title} " - label.set_markup(markup) - break - - if self.updates_results == []: - self.updates_available_bar.set_visible(False) - - self.current_page = category - self.current_group = group - self.update_category_header(category) - self.update_subcategories_bar(category) - self.update_updates_available_bar(category) - self.show_category_apps(category) - - - - - def refresh_current_page(self): - """Refresh the currently displayed page""" - if self.current_page and self.current_group: - self.on_category_clicked(self.current_page, self.current_group) - - def update_category_header(self, category): - """Update the category header text based on the selected category.""" - display_title = "" - if category in self.category_groups['system']: - display_title = self.category_groups['system'][category] - if category in self.category_groups['collections']: - display_title = self.category_groups['collections'][category] - elif category in self.category_groups['categories']: - display_title = self.category_groups['categories'][category] - else: # Find the parent category and get the title - for parent_category, subcategories in self.subcategory_groups.items(): - if category in subcategories: - parent_title = self.category_groups['categories'].get(parent_category, parent_category) - subcat_title = subcategories[category] - display_title = f"{parent_title} » {subcat_title}" - break - if display_title == "": - # Fallback if category isn't found - display_title = category - self.category_header.set_label(display_title) - - def create_applications_panel(self, title): - # Create right panel - self.right_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.right_panel.set_hexpand(True) # Add this line - self.right_panel.set_vexpand(True) # Add this line - - # Add category header - self.category_header = Gtk.Label(label="") - self.category_header.get_style_context().add_class("panel-header") - self.category_header.set_hexpand(True) - self.category_header.set_halign(Gtk.Align.START) - self.right_panel.pack_start(self.category_header, False, False, 0) - - # Create subcategories bar (initially hidden) - self.subcategories_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.subcategories_bar.set_hexpand(True) - self.subcategories_bar.set_spacing(6) - self.subcategories_bar.set_border_width(6) - self.subcategories_bar.set_visible(False) - self.subcategories_bar.set_halign(Gtk.Align.FILL) # Ensure full width - self.right_panel.pack_start(self.subcategories_bar, False, False, 0) - - # Create subcategories bar (initially hidden) - self.updates_available_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.updates_available_bar.set_hexpand(True) - self.updates_available_bar.set_spacing(6) - self.updates_available_bar.set_border_width(6) - self.updates_available_bar.set_visible(False) - self.updates_available_bar.set_halign(Gtk.Align.FILL) # Ensure full width - self.right_panel.pack_start(self.updates_available_bar, False, False, 0) - self.right_panel.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) - - # Create scrollable area - self.category_scrolled_window = Gtk.ScrolledWindow() - self.category_scrolled_window.set_hexpand(True) - self.category_scrolled_window.set_vexpand(True) - - # Create container for applications - self.right_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.right_container.set_spacing(6) - self.right_container.set_border_width(6) - self.right_container.set_hexpand(True) # Add this line - self.right_container.set_vexpand(True) # Add this line - self.right_container.get_style_context().add_class("app-window") - self.category_scrolled_window.add(self.right_container) - self.right_panel.pack_start(self.category_scrolled_window, True, True, 0) - return self.right_panel - - def create_subcategory_container(self): - container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - container.set_spacing(6) - container.set_border_width(6) - container.set_hexpand(True) - container.set_halign(Gtk.Align.CENTER) - container.set_homogeneous(False) - return container - - def create_scroll_buttons(self): - pan_start = Gtk.Button() - pan_start_icon = Gio.Icon.new_for_string('pan-start-symbolic') - pan_start.set_image(Gtk.Image.new_from_gicon(pan_start_icon, Gtk.IconSize.BUTTON)) - pan_start.get_style_context().add_class("pan-button") - pan_start.connect("clicked", self.on_pan_start) - - pan_end = Gtk.Button() - pan_end_icon = Gio.Icon.new_for_string('pan-end-symbolic') - pan_end.set_image(Gtk.Image.new_from_gicon(pan_end_icon, Gtk.IconSize.BUTTON)) - pan_end.get_style_context().add_class("pan-button") - pan_end.connect("clicked", self.on_pan_end) - - return pan_start, pan_end - - def build_subcategory_bar(self, category): - container = self.create_subcategory_container() - - for subcategory, title in self.subcategory_groups[category].items(): - subcategory_box = Gtk.EventBox() - subcategory_box.set_hexpand(False) - subcategory_box.set_halign(Gtk.Align.START) - subcategory_box.set_margin_top(2) - subcategory_box.set_margin_bottom(2) - - label = Gtk.Label(label=title) - label.set_halign(Gtk.Align.START) - label.set_hexpand(False) - label.get_style_context().add_class("subcategory-button") - - if subcategory == category: - label.get_style_context().add_class("selected") - - subcategory_box.add(label) - subcategory_box.connect( - "button-release-event", - lambda widget, event, subcat=subcategory: self.on_subcategory_clicked(subcat) - ) - - self.subcategory_buttons[subcategory] = label - container.pack_start(subcategory_box, False, False, 0) - - return container - - def build_subcategory_context_view(self, category, parent_category): - container = self.create_subcategory_container() - - parent_box = Gtk.EventBox() - parent_box.set_hexpand(False) - parent_box.set_halign(Gtk.Align.START) - parent_box.set_margin_top(2) - parent_box.set_margin_bottom(2) - - parent_label = Gtk.Label(label=self.category_groups['categories'][parent_category]) - parent_label.set_halign(Gtk.Align.START) - parent_label.set_hexpand(False) - parent_label.get_style_context().add_class("subcategory-button") - - parent_box.add(parent_label) - parent_box.connect( - "button-release-event", - lambda widget, event, cat=parent_category, grp='categories': - self.on_category_clicked(cat, grp) - ) - container.pack_start(parent_box, False, False, 0) - - subcategory_box = Gtk.EventBox() - subcategory_box.set_hexpand(False) - subcategory_box.set_halign(Gtk.Align.START) - subcategory_box.set_margin_top(2) - subcategory_box.set_margin_bottom(2) - - subcategory_label = Gtk.Label(label=self.subcategory_groups[parent_category][category]) - subcategory_label.set_halign(Gtk.Align.START) - subcategory_label.set_hexpand(False) - subcategory_label.get_style_context().add_class("subcategory-button") - subcategory_box.add(subcategory_label) - subcategory_box.connect( - "button-release-event", - lambda widget, event, subcat=category: - self.on_subcategory_clicked(subcat) - ) - container.pack_start(subcategory_box, False, False, 0) - - return container - - def scroll_to_widget(self, widget): - """Scrolls the scrolled window to ensure the widget is fully visible.""" - adjustment = self.scrolled_window.get_hadjustment() - - # Container is the Gtk.Box inside the scrolled window - container = self.scrolled_window.get_child() - if not container: - return False - - # Translate widget's position relative to the container - widget_coords = widget.translate_coordinates(container, 0, 0) - if not widget_coords: - return False - - widget_x, _ = widget_coords - widget_width = widget.get_allocated_width() - view_start = adjustment.get_value() - view_end = view_start + adjustment.get_page_size() - - # Scroll only if the widget is outside the visible area - if widget_x < view_start: - adjustment.set_value(widget_x) - elif (widget_x + widget_width) > view_end: - adjustment.set_value(widget_x + widget_width - adjustment.get_page_size()) - - return False - - def update_subcategories_bar(self, category): - for child in self.subcategories_bar.get_children(): - child.destroy() - self.subcategory_buttons.clear() - - if not hasattr(self, 'scrolled_window'): - self.scrolled_window = Gtk.ScrolledWindow() - - for child in self.scrolled_window.get_children(): - child.destroy() - - self.scrolled_window.set_hexpand(True) - self.scrolled_window.set_vexpand(False) - self.scrolled_window.set_size_request(-1, 40) - self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER) - self.scrolled_window.set_min_content_width(0) - self.scrolled_window.set_max_content_width(-1) - self.scrolled_window.set_overlay_scrolling(False) - self.scrolled_window.get_style_context().add_class("no-scroll-bars") - - pan_start, pan_end = self.create_scroll_buttons() - self.subcategories_bar.get_style_context().add_class("subcategory-group-header") - self.subcategories_bar.set_visible(True) - - if category in self.subcategory_groups: - container = self.build_subcategory_bar(category) - else: - parent_category = self.get_parent_category(category) - if parent_category: - container = self.build_subcategory_context_view(category, parent_category) - else: - self.subcategories_bar.set_visible(False) - return - - self.scrolled_window.add(container) - self.subcategories_bar.pack_start(pan_start, False, False, 0) - self.subcategories_bar.pack_start(self.scrolled_window, True, True, 0) - self.subcategories_bar.pack_start(pan_end, False, False, 0) - self.subcategories_bar.queue_resize() - self.subcategories_bar.show_all() - - def update_updates_available_bar(self, category): - for child in self.updates_available_bar.get_children(): - child.destroy() - - if category == "updates": - if self.updates_results != [] : - self.updates_available_bar.get_style_context().add_class("updates_available_bar") - self.updates_available_bar.set_visible(True) - - buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - buttons_box.set_spacing(6) - buttons_box.set_margin_top(4) - buttons_box.set_halign(Gtk.Align.END) - - update_all_button = Gtk.Button() - update_all_button.set_size_request(26, 26) # 40x40 pixels - update_all_button.get_style_context().add_class("app-action-button") - update_all_icon = Gio.Icon.new_for_string('system-software-update-symbolic') - update_all_button.set_image(Gtk.Image.new_from_gicon(update_all_icon, Gtk.IconSize.BUTTON)) - update_all_button.connect("clicked", self.on_update_all_button_clicked) - buttons_box.pack_end(update_all_button, False, False, 0) - - # Create left label - left_label = Gtk.Label(label="Update All: ") - left_label.set_halign(Gtk.Align.END) # Align left - self.updates_available_bar.pack_end(buttons_box, False, False, 0) - self.updates_available_bar.pack_end(left_label, False, False, 0) - - self.updates_available_bar.show_all() - else: - self.updates_available_bar.set_visible(False) - - def on_update_all_button_clicked(self, button=None): - # Create a message dialog - dialog = Gtk.MessageDialog( - transient_for=self, # Parent window - modal=True, # Make it modal - message_type=Gtk.MessageType.QUESTION, - buttons=Gtk.ButtonsType.OK_CANCEL, - text="Download and install all available Flatpak updates?", - title="Confirm" - ) - - # Show the dialog and get the response - response = dialog.run() - - # Handle the response - if response == Gtk.ResponseType.OK: - # Perform Removal - def perform_update(): - # Show waiting dialog - GLib.idle_add(self.show_waiting_dialog, "Updating packages...") - - success, message = fp_turbo.update_all_flatpaks(self.updates_results, self.system_mode) - - # Update UI on main thread - GLib.idle_add(lambda: self.on_task_complete(dialog, success, message)) - - # Start spinner and begin installation - thread = threading.Thread(target=perform_update) - thread.daemon = True # Allow program to exit even if thread is still running - thread.start() - dialog.destroy() - - def get_parent_category(self, subcategory): - for parent, subcats in self.subcategory_groups.items(): - if subcategory in subcats: - return parent - return None - - def on_pan_start(self, button): - # Get the scrolled window's adjustment - adjustment = self.scrolled_window.get_hadjustment() - # Scroll to the left by a page - adjustment.set_value(adjustment.get_value() - adjustment.get_page_size()) - - def on_pan_end(self, button): - # Get the scrolled window's adjustment - adjustment = self.scrolled_window.get_hadjustment() - # Scroll to the right by a page - adjustment.set_value(adjustment.get_value() + adjustment.get_page_size()) - - def highlight_selected_subcategory(self, selected_subcat): - for subcat, widget in self.subcategory_buttons.items(): - if subcat == selected_subcat: - widget.get_style_context().add_class("active") - else: - widget.get_style_context().remove_class("active") - - # Scroll to make sure the selected subcategory is visible - selected_widget = self.subcategory_buttons.get(selected_subcat) - if selected_widget: - adj = self.scrolled_window.get_hadjustment() - alloc = selected_widget.get_allocation() - new_value = alloc.x + alloc.width / 2 - adj.get_page_size() / 2 - adj.set_value(max(0, new_value)) - - def on_subcategory_clicked(self, subcategory): - """Handle subcategory button clicks.""" - # Remove 'selected' from all subcategory buttons - for label in self.subcategory_buttons.values(): - label.get_style_context().remove_class("selected") - - # Add 'selected' to the clicked one - if subcategory in self.subcategory_buttons: - self.subcategory_buttons[subcategory].get_style_context().add_class("selected") - - # Update current state - self.current_page = subcategory - self.current_group = 'subcategories' - self.update_category_header(subcategory) - self.highlight_selected_subcategory(subcategory) - self.show_category_apps(subcategory) - if subcategory in self.subcategory_buttons: - selected_widget = self.subcategory_buttons[subcategory] - GLib.idle_add(self.scroll_to_widget, selected_widget) - - # Create and connect buttons - def create_button(self, callback, app, label=None, condition=None): - """Create a button with optional visibility condition""" - button = Gtk.Button() - if label: - button = Gtk.Button(label=label) - button.get_style_context().add_class("app-button") - if condition is not None: - # if not condition(app): - return None - button.connect("clicked", callback, app) - return button - - def clear_container(self, container): - """Clear all widgets from a container""" - for child in container.get_children(): - child.destroy() - - def get_app_priority(self, kind): - """Convert AppKind to numeric priority for sorting""" - priorities = { - "DESKTOP_APP": 0, - "ADDON": 1, - "RUNTIME": 2 - } - return priorities.get(kind, 3) - - def show_category_apps(self, category): - # Initialize apps list - apps = [] - vadjustment = self.category_scrolled_window.get_vadjustment() - vadjustment.set_value(vadjustment.get_lower()) - - # Load system data - if 'installed' in category: - apps.extend([app for app in self.installed_results]) - if 'updates' in category: - apps.extend([app for app in self.updates_results]) - - if ('installed' in category) or ('updates' in category): - # Sort apps by component type priority - if apps: - apps.sort(key=lambda app: self.get_app_priority(app.get_details()['kind'])) - - # Define paths - app_data_dir = Path.home() / ".local" / "share" / "flatpost" - system_data_dir = Path("/usr/share/flatpost") - - # Ensure local directory exists - app_data_dir.mkdir(parents=True, exist_ok=True) - - # Define file paths - json_path = app_data_dir / "collections_data.json" - - # Load collections data - try: - with open(json_path, 'r', encoding='utf-8') as f: - collections_data = json.load(f) - - # Find the specific category in collections data - category_entry = next(( - entry for entry in collections_data - if entry['category'] == category - ), None) - - if category_entry: - # Get all app IDs in this category - app_ids_in_category = [ - hit['app_id'] for hit in category_entry['data']['hits'] - ] - - # Filter apps based on presence in category - apps.extend([ - app for app in self.collection_results - if app.get_details()['id'] in app_ids_in_category - ]) - else: - # Fallback to previous behavior if category isn't in collections - apps.extend([ - app for app in self.collection_results - if category in app.get_details()['categories'] - ]) - - except (IOError, json.JSONDecodeError) as e: - apps.extend([ - app for app in self.collection_results - if category in app.get_details()['categories'] - ]) - - - if 'repositories' in category: - # Clear existing content - for child in self.right_container.get_children(): - child.destroy() - - # Create header bar - header_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - header_bar.set_hexpand(True) - header_bar.set_spacing(6) - header_bar.set_border_width(6) - - # Create left label - left_label = Gtk.Label(label="On/Off") - left_label.get_style_context().add_class("repo-list-header") - left_label.set_halign(Gtk.Align.START) # Align left - header_bar.pack_start(left_label, True, True, 0) - - # Center container to fix "URL" label alignment - center_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - center_container.set_halign(Gtk.Align.START) # Align left - - # Create center label - center_label = Gtk.Label(label="URL") - center_label.get_style_context().add_class("repo-list-header") - center_label.set_halign(Gtk.Align.START) # Align center - - center_container.pack_start(center_label, True, True, 0) - header_bar.pack_start(center_container, True, True, 0) - - # Create right label - right_label = Gtk.Label(label="+/-") - right_label.get_style_context().add_class("repo-list-header") - right_label.set_halign(Gtk.Align.END) # Align right - header_bar.pack_end(right_label, False, False, 0) - - # Add header bar to container - self.right_container.pack_start(header_bar, False, False, 0) - - # Get list of repositories - repos = fp_turbo.repolist(self.system_mode) - - # Create a scrolled window for repositories - scrolled_window = Gtk.ScrolledWindow() - scrolled_window.set_hexpand(True) - scrolled_window.set_vexpand(True) - - # Create container for repositories - repo_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - repo_container.set_spacing(6) - repo_container.set_border_width(6) - - # Add repository button - add_repo_button = Gtk.Button() - add_repo_button.set_size_request(26, 26) # 40x40 pixels - add_repo_button.get_style_context().add_class("app-action-button") - add_icon = Gio.Icon.new_for_string('list-add-symbolic') - add_repo_button.set_image(Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.BUTTON)) - add_repo_button.connect("clicked", self.on_add_repo_button_clicked) - - add_flathub_repo_button = Gtk.Button(label="Add Flathub Repo") - add_flathub_repo_button.connect("clicked", self.on_add_flathub_repo_button_clicked) - - add_flathub_beta_repo_button = Gtk.Button(label="Add Flathub Beta Repo") - add_flathub_beta_repo_button.connect("clicked", self.on_add_flathub_beta_repo_button_clicked) - - # Check for existing Flathub repositories and disable buttons accordingly - flathub_url = "https://dl.flathub.org/repo/" - flathub_beta_url = "https://dl.flathub.org/beta-repo/" - - existing_urls = [repo.get_url().rstrip('/') for repo in repos] - add_flathub_repo_button.set_sensitive(flathub_url.rstrip('/') not in existing_urls) - add_flathub_beta_repo_button.set_sensitive(flathub_beta_url.rstrip('/') not in existing_urls) - - # Add repositories to container - for repo in repos: - repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - repo_box.set_spacing(6) - repo_box.set_hexpand(True) - - # Create checkbox - if self.system_mode: - checkbox = Gtk.CheckButton(label=f"{repo.get_name()} (System)") - else: - checkbox = Gtk.CheckButton(label=f"{repo.get_name()} (User)") - checkbox.set_active(not repo.get_disabled()) - if not repo.get_disabled(): - checkbox.get_style_context().remove_class("dim-label") - else: - checkbox.get_style_context().add_class("dim-label") - checkbox.connect("toggled", self.on_repo_toggled, repo) - checkbox_url_label = Gtk.Label(label=repo.get_url()) - checkbox_url_label.set_halign(Gtk.Align.START) - checkbox_url_label.set_hexpand(True) - checkbox_url_label.get_style_context().add_class("dim-label") - - # Create delete button - delete_button = Gtk.Button() - delete_button.set_size_request(26, 26) # 40x40 pixels - delete_button.get_style_context().add_class("app-action-button") - delete_icon = Gio.Icon.new_for_string('list-remove-symbolic') - delete_button.set_image(Gtk.Image.new_from_gicon(delete_icon, Gtk.IconSize.BUTTON)) - delete_button.get_style_context().add_class("destructive-action") - delete_button.connect("clicked", self.on_repo_delete, repo) - - # Add widgets to box - repo_box.pack_start(checkbox, False, False, 0) - repo_box.pack_start(checkbox_url_label, False, False, 0) - repo_box.pack_end(delete_button, False, False, 0) - - # Add box to container - repo_container.pack_start(repo_box, False, False, 0) - - repo_container.pack_start(add_repo_button, False, False, 0) - repo_container.pack_start(add_flathub_repo_button, False, False, 0) - repo_container.pack_start(add_flathub_beta_repo_button, False, False, 0) - - # Add container to scrolled window - scrolled_window.add(repo_container) - self.right_container.pack_start(scrolled_window, True, True, 0) - - self.right_container.show_all() - return - - # Apply component type filter if set - component_type_filter = self.current_component_type - if component_type_filter: - apps = [app for app in apps if app.get_details()['kind'] == component_type_filter] - - self.display_apps(apps) - - def create_scaled_icon(self, icon, is_themed=False): - if is_themed: - # For themed icons, create a pixbuf directly using the icon theme - icon_theme = Gtk.IconTheme.get_default() - pb = icon_theme.load_icon(icon.get_names()[0], 64, Gtk.IconLookupFlags.FORCE_SIZE) - else: - # For file-based icons - pb = GdkPixbuf.Pixbuf.new_from_file(icon) - - # Scale to 64x64 using high-quality interpolation - scaled_pb = pb.scale_simple( - 64, 64, # New dimensions - GdkPixbuf.InterpType.BILINEAR # High-quality scaling - ) - - # Create the image widget from the scaled pixbuf - return Gtk.Image.new_from_pixbuf(scaled_pb) - - def display_apps(self, apps): - """Display applications in the right container.""" - self._clear_container() - apps_by_id = self._group_apps_by_id(apps) - for app_id, app_data in apps_by_id.items(): - self._create_and_add_app_row(app_data) - - def _clear_container(self): - """Clear all children from the right container.""" - for child in self.right_container.get_children(): - child.destroy() - - def _group_apps_by_id(self, apps): - """Group applications by their IDs and collect repositories.""" - apps_dict = {} - for app in apps: - details = app.get_details() - app_id = details['id'] - - if app_id not in apps_dict: - apps_dict[app_id] = {'app': app, 'repos': set()} - - apps_dict[app_id]['repos'].add(details.get('repo', 'unknown')) - return apps_dict - - def _create_and_add_app_row(self, app_data): - """Create and add a row for a single application.""" - app = app_data['app'] - details = app.get_details() - - status = self._get_app_status(app) - container = self._create_app_container() - self._setup_icon(container, details) - self._setup_text_layout(container, details, app_data['repos']) - self._setup_buttons(container, status, app) - - self.right_container.pack_start(container, False, False, 0) - self.right_container.pack_start(Gtk.Separator(), False, False, 0) - self.right_container.show_all() - - def _get_app_status(self, app): - """Determine installation and update status of an application.""" - details = app.get_details() - return { - 'is_installed': any(pkg.id == details['id'] for pkg in self.installed_results), - 'is_updatable': any(pkg.id == details['id'] for pkg in self.updates_results), - 'has_donation_url': bool(app.get_details().get('urls', {}).get('donation')) - } - - def _create_app_container(self): - """Create the horizontal container for an application row.""" - container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - container.set_spacing(12) - container.set_margin_top(6) - container.set_margin_bottom(6) - return container - - def _setup_icon(self, container, details): - """Set up the icon box and icon for the application.""" - icon_box = Gtk.Box() - icon_box.set_size_request(88, -1) - - icon_widget = self.create_scaled_icon( - Gio.Icon.new_for_string('package-x-generic-symbolic'), - is_themed=True - ) - - if details['icon_filename']: - icon_path = Path(f"{details['icon_path_128']}/{details['icon_filename']}") - if icon_path.exists(): - icon_widget = self.create_scaled_icon(str(icon_path), is_themed=False) - - icon_widget.set_size_request(64, 64) - icon_box.pack_start(icon_widget, True, True, 0) - container.pack_start(icon_box, False, False, 0) - - def _setup_text_layout(self, container, details, repos): - """Set up the text layout including title, kind, repositories, and description.""" - right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - right_box.set_spacing(4) - right_box.set_hexpand(True) - - # Title - title_label = Gtk.Label(label=details['name']) - title_label.get_style_context().add_class("app-list-header") - title_label.set_halign(Gtk.Align.START) - title_label.set_yalign(0.5) - title_label.set_hexpand(True) - right_box.pack_start(title_label, False, False, 0) - - # Kind label - kind_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - kind_box.set_spacing(4) - kind_box.set_halign(Gtk.Align.START) - kind_box.set_valign(Gtk.Align.START) - - kind_label = Gtk.Label(label=f"Type: {details['kind']}") - kind_box.pack_end(kind_label, False, False, 0) - right_box.pack_start(kind_box, False, False, 0) - - # Repositories - repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - repo_box.set_spacing(4) - repo_box.set_halign(Gtk.Align.START) - repo_box.set_valign(Gtk.Align.START) - - repo_list_label = Gtk.Label(label="Sources: ") - repo_box.pack_start(repo_list_label, False, False, 0) - - for repo in sorted(repos): - repo_label = Gtk.Label(label=f"{repo}") - repo_label.set_halign(Gtk.Align.START) - repo_box.pack_end(repo_label, False, False, 0) - - right_box.pack_start(repo_box, False, False, 0) - - # Description - desc_label = Gtk.Label(label=details['summary']) - desc_label.set_halign(Gtk.Align.START) - desc_label.set_yalign(0.5) - desc_label.set_hexpand(True) - desc_label.set_line_wrap(True) - desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD) - desc_label.get_style_context().add_class("dim-label") - desc_label.get_style_context().add_class("app-list-summary") - right_box.pack_start(desc_label, False, False, 0) - - container.pack_start(right_box, True, True, 0) - - def _setup_buttons(self, container, status, app): - """Set up action buttons for the application.""" - buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - buttons_box.set_spacing(6) - buttons_box.set_margin_top(4) - buttons_box.set_halign(Gtk.Align.END) - buttons_box.set_valign(Gtk.Align.CENTER) # Center vertically - - # Add install/remove buttons separately - if status['is_installed']: - self._add_action_button( - buttons_box, - True, - app, - self.on_remove_clicked, - "list-remove-symbolic", - "remove" - ) - else: - self._add_action_button( - buttons_box, - True, - app, - self.on_install_clicked, - "list-add-symbolic", - "install" - ) - - if status['is_installed']: - self._add_action_button( - buttons_box, - True, - app, - self.on_app_options_clicked, - "applications-system-symbolic", - "options" - ) - - if status['is_updatable']: - self._add_action_button( - buttons_box, - True, - app, - self.on_update_clicked, - 'system-software-update-symbolic', - "update" - ) - - self._add_action_button( - buttons_box, - True, - app, - self.on_details_clicked, - 'help-about-symbolic', - "details" - ) - - if status['has_donation_url']: - self._add_action_button( - buttons_box, - True, - app, - self.on_donate_clicked, - 'donate-symbolic', - "donate" - ) - - container.pack_end(buttons_box, False, False, 0) - - def _add_action_button(self, parent, visible, app, callback, icon_name, tooltip=None): - """Helper method to add a consistent action button.""" - if not visible: - return - - button = self.create_button(callback, app) - if button: - # Set consistent size - button.set_size_request(26, 26) # 40x40 pixels - - # Set consistent style - button.get_style_context().add_class("app-action-button") - - icon = Gio.Icon.new_for_string(icon_name) - button.set_image(Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)) - parent.pack_end(button, False, False, 0) - - def show_waiting_dialog(self, message="Please wait while task is running..."): - """Show a modal dialog with a spinner""" - self.waiting_dialog = Gtk.Dialog( - title="Running Task...", - transient_for=self, - modal=True, - destroy_with_parent=True, - ) - - # Create spinner - self.spinner = Gtk.Spinner() - self.spinner.start() - - # Add content - box = self.waiting_dialog.get_content_area() - box.set_spacing(12) - box.set_border_width(12) - - # Add label and spinner - box.pack_start(Gtk.Label(label=message), False, False, 0) - box.pack_start(self.spinner, False, False, 0) - - # Show dialog - self.waiting_dialog.show_all() - - def on_install_clicked(self, button=None, app=None): - """Handle the Install button click with installation options""" - if not app: - self._show_error("Error: No app specified") - return - - title, label = self._get_dialog_details(app, button) - dialog = self._create_dialog(title) - content_area = self._setup_dialog_content(dialog, label) - - if button and app: - self._handle_repository_selection(content_area, app) - - dialog.show_all() - response = dialog.run() - - if response == Gtk.ResponseType.OK: - self._perform_installation(dialog, app, button) - - dialog.destroy() - - def _get_dialog_details(self, app, button): - """Extract dialog details based on input""" - if button: - details = app.get_details() - return f"Install {details['name']}?", f"Install: {details['id']}" - return f"Install {app}?", f"Install: {app}" - - def _create_dialog(self, title): - """Create and configure the dialog""" - dialog = Gtk.Dialog( - title=title, - transient_for=self, - modal=True, - destroy_with_parent=True, - ) - # Add buttons using the new method - dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) - dialog.add_button("Install", Gtk.ResponseType.OK) - return dialog - - def _setup_dialog_content(self, dialog, label): - """Setup dialog content area""" - content_area = dialog.get_content_area() - content_area.set_spacing(12) - content_area.set_border_width(12) - content_area.pack_start(Gtk.Label(label=label), False, False, 0) - - installation_type = "User" if not self.system_mode else "System" - content_area.pack_start( - Gtk.Label(label=f"Installation Type: {installation_type}"), - False, False, 0 - ) - return content_area - - def _handle_repository_selection(self, content_area, app): - """Handle repository selection logic""" - # Create the combo box - self.repo_combo = Gtk.ComboBoxText() - - # Search for available repositories containing this app - searcher = fp_turbo.get_reposearcher(self.system_mode) - repos = fp_turbo.repolist(self.system_mode) - - # Find repositories that have this specific app - app_id = app.get_details()['id'] - available_repos = { - repo for repo in repos - if not repo.get_disabled() and - searcher.search_flatpak(app_id, repo.get_name()) - } - - if available_repos: - self.repo_combo.remove_all() # Clear any existing items - - # Add all repositories - for repo in available_repos: - self.repo_combo.append_text(repo.get_name()) - - # Only show dropdown if there are multiple repositories - if len(available_repos) >= 2: - # Remove and re-add with dropdown visible - content_area.pack_start(self.repo_combo, False, False, 0) - self.repo_combo.set_button_sensitivity(Gtk.SensitivityType.AUTO) - self.repo_combo.set_active(0) - else: - # Remove and re-add without dropdown - content_area.remove(self.repo_combo) - self.repo_combo.set_active(0) - else: - self.repo_combo.remove_all() # Clear any existing items - self.repo_combo.append_text("No repositories available") - content_area.remove(self.repo_combo) - - def _perform_installation(self, dialog, app, button): - """Handle the installation process""" - selected_repo = None - if button: - selected_repo = self.repo_combo.get_active_text() - - def installation_thread(): - GLib.idle_add(self.show_waiting_dialog) - if button: - success, message = fp_turbo.install_flatpak(app, selected_repo, self.system_mode) - else: - success, message = fp_turbo.install_flatpakref(app, self.system_mode) - GLib.idle_add(lambda: self.on_task_complete(dialog, success, message)) - - thread = threading.Thread(target=installation_thread) - thread.daemon = True - thread.start() - - def on_task_complete(self, dialog, success, message): - """Handle task completion""" - message_type = Gtk.MessageType.INFO - if not success: - message_type = Gtk.MessageType.ERROR - if message: - finished_dialog = Gtk.MessageDialog( - transient_for=self, - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text=message - ) - finished_dialog.run() - finished_dialog.destroy() - self.refresh_local() - self.refresh_current_page() - self.waiting_dialog.destroy() - - - def on_remove_clicked(self, button, app): - """Handle the Remove button click with removal options""" - details = app.get_details() - - # Create dialog - dialog = Gtk.Dialog( - title=f"Remove {details['name']}?", - transient_for=self, - modal=True, - destroy_with_parent=True, - ) - # Add buttons using the new method - dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) - dialog.add_button("Remove", Gtk.ResponseType.OK) - - # Create content area - content_area = dialog.get_content_area() - content_area.set_spacing(12) - content_area.set_border_width(12) - - content_area.pack_start(Gtk.Label(label=f"Remove: {details['id']}?"), False, False, 0) - - # Show dialog - dialog.show_all() - - # Run dialog - response = dialog.run() - if response == Gtk.ResponseType.OK: - # Perform Removal - def perform_removal(): - # Show waiting dialog - GLib.idle_add(self.show_waiting_dialog, "Removing package...") - - success, message = fp_turbo.remove_flatpak(app, self.system_mode) - - # Update UI on main thread - GLib.idle_add(lambda: self.on_task_complete(dialog, success, message)) - - # Start spinner and begin installation - thread = threading.Thread(target=perform_removal) - thread.daemon = True # Allow program to exit even if thread is still running - thread.start() - - dialog.destroy() - - def _add_bus_section(self, app_id, app, listbox, section_title, perm_type): - """Helper method to add System Bus or Session Bus section""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Get permissions - global_success, global_perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode) - if not global_success: - global_perms = {"paths": []} - success, perms = fp_turbo.list_other_perm_values(app_id, perm_type, self.system_mode) - if not success: - perms = {"paths": []} - - # Add Talks section - talks_row = Gtk.ListBoxRow(selectable=False) - talks_row.get_style_context().add_class("permissions-row") - talks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - talks_box.get_style_context().add_class("permissions-bus-box") - talks_row.add(talks_box) - - talks_header = Gtk.Label(label="Talks", xalign=0) - talks_header.get_style_context().add_class("permissions-item-label") - talks_box.pack_start(talks_header, False, False, 0) - - # Add talk paths - for path in global_perms["paths"]: - if path != "" and "talk" in path: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type) - - # Configure button based on permission type - btn.set_sensitive(False) - btn.get_style_context().add_class("destructive-action") - - btn_box.pack_end(btn, False, False, 0) - indicator_label = Gtk.Label(label="*", xalign=0) - btn_box.pack_end(indicator_label, False, True, 0) - - hbox.pack_end(btn_box, False, False, 0) - talks_box.add(row) - - for path in perms["paths"]: - if path != "" and "talk" in path and path not in global_perms["paths"]: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type) - - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - talks_box.add(row) - - listbox.add(talks_row) - - # Add Owns section - owns_row = Gtk.ListBoxRow(selectable=False) - owns_row.get_style_context().add_class("permissions-row") - owns_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - owns_box.get_style_context().add_class("permissions-bus-box") - owns_row.add(owns_box) - - owns_header = Gtk.Label(label="Owns", xalign=0) - owns_header.get_style_context().add_class("permissions-item-label") - owns_box.pack_start(owns_header, False, False, 0) - - # Add own paths - for path in global_perms["paths"]: - if path != "" and "own" in path: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type) - - # Configure button based on permission type - btn.set_sensitive(False) - btn.get_style_context().add_class("destructive-action") - - btn_box.pack_end(btn, False, False, 0) - indicator_label = Gtk.Label(label="*", xalign=0) - btn_box.pack_end(indicator_label, False, True, 0) - - hbox.pack_end(btn_box, False, False, 0) - owns_box.add(row) - - for path in perms["paths"]: - if path != "" and "own" in path and path not in global_perms["paths"]: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type) - - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - - owns_box.add(row) - - owns_row.show_all() - listbox.add(owns_row) - - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add add button - add_path_row = Gtk.ListBoxRow(selectable=False) - add_path_row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - add_path_row.add(hbox) - - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-add-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_add_path, app_id, app, perm_type) - hbox.pack_end(btn, False, True, 0) - - listbox.add(add_path_row) - - def _add_path_section(self, app_id, app, listbox, section_title, perm_type): - """Helper method to add sections with paths (Persistent, Environment)""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Get permissions - if perm_type == "persistent": - success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode) - else: - success, perms = fp_turbo.list_other_perm_values(app_id, perm_type, self.system_mode) - if not success: - perms = {"paths": []} - - if perm_type == "persistent": - global_success, global_perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode) - else: - global_success, global_perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode) - if not global_success: - global_perms = {"paths": []} - - - # First, create rows for global paths - for path in global_perms["paths"]: - if path != "": - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type) - - # Configure button based on permission type - btn.set_sensitive(False) - btn.get_style_context().add_class("destructive-action") - - btn_box.pack_end(btn, False, False, 0) - indicator_label = Gtk.Label(label="*", xalign=0) - btn_box.pack_end(indicator_label, False, True, 0) - - hbox.pack_end(btn_box, False, False, 0) - listbox.add(row) - - # Then create rows for application-specific paths - for path in perms["paths"]: - if path != "" and path not in global_perms["paths"]: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type) - - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - listbox.add(row) - - # Add add button - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-add-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_add_path, app_id, app) - hbox.pack_end(btn, False, True, 0) - - listbox.add(row) - - def _add_filesystem_section(self, app_id, app, listbox, section_title): - """Helper method to add the Filesystems section""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Get filesystem permissions - global_success, global_perms = fp_turbo.global_list_file_perms(True, self.system_mode) - if not global_success: - global_perms = {"paths": [], "special_paths": []} - success, perms = fp_turbo.list_file_perms(app_id, self.system_mode) - if not success: - perms = {"paths": [], "special_paths": []} - - # Add special paths as toggles - special_paths = [ - ("All user files", "home", "Access to all user files"), - ("All system files", "host", "Access to all system files"), - ("All system libraries, executables and static data", "host-os", "Access to system libraries and executables"), - ("All system configurations", "host-etc", "Access to system configurations") - ] - - for display_text, option, description in special_paths: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox.pack_start(vbox, True, True, 0) - - label = Gtk.Label(label=display_text, xalign=0) - label.get_style_context().add_class("permissions-item-label") - desc = Gtk.Label(label=description, xalign=0) - desc.get_style_context().add_class("permissions-item-summary") - vbox.pack_start(label, True, True, 0) - vbox.pack_start(desc, True, True, 0) - - switch = Gtk.Switch() - switch.props.valign = Gtk.Align.CENTER - - # Add indicator label before switch - switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - in_perms = option in perms["special_paths"] - in_global_perms = option in global_perms["special_paths"] - - switch.set_active(in_global_perms or in_perms) - # Set sensitivity based on your requirements - if in_global_perms: - switch.set_sensitive(False) # Global permissions take precedence - indicator = Gtk.Label(label="*", xalign=1.0) - indicator.get_style_context().add_class("global-indicator") - switch_box.pack_start(indicator, False, True, 0) - - elif in_perms: - switch.set_sensitive(True) # Local permissions enabled and sensitive - - switch_box.pack_start(switch, False, True, 0) - switch.connect("state-set", self._on_switch_toggled, app_id, "filesystems", option) - hbox.pack_end(switch_box, False, True, 0) - - listbox.add(row) - - - # First, create rows for global paths - for path in global_perms["paths"]: - if path != "": - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, "filesystems") - - # Configure button based on permission type - btn.set_sensitive(False) - btn.get_style_context().add_class("destructive-action") - - btn_box.pack_end(btn, False, False, 0) - indicator_label = Gtk.Label(label="*", xalign=0) - btn_box.pack_end(indicator_label, False, True, 0) - - hbox.pack_end(btn_box, False, False, 0) - listbox.add(row) - - # Then create rows for application-specific paths - for path in perms["paths"]: - if path != "" and path not in global_perms["paths"]: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_remove_path, app_id, app, path, "filesystems") - - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - listbox.add(row) - - # Add add button - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-add-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_add_path, app_id, app, "filesystems") - hbox.pack_end(btn, False, True, 0) - - listbox.add(row) - - - def on_app_options_clicked(self, button, app): - """Handle the app options click""" - details = app.get_details() - app_id = details['id'] - - # Create window (as before) - self.options_window = Gtk.Window(title=f"{details['name']} Settings") - self.options_window.set_default_size(600, 800) - - # Set subtitle - header_bar = Gtk.HeaderBar(title=f"{details['name']} Settings", - subtitle="List of resources selectively granted to the application") - header_bar.set_show_close_button(True) - self.options_window.set_titlebar(header_bar) - - # Create main container with padding - box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - box_outer.set_border_width(20) - self.options_window.add(box_outer) - - # Create scrolled window for content - scrolled = Gtk.ScrolledWindow() - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - - # Create list box for options - listbox = Gtk.ListBox() - listbox.set_selection_mode(Gtk.SelectionMode.NONE) - listbox.get_style_context().add_class("permissions-window") - - indicator = Gtk.Label(label="* = global override", xalign=1.0) - indicator.get_style_context().add_class("permissions-global-indicator") - # Add other sections with correct permission types - self._add_section(app_id, listbox, "Shared", "shared", [ - ("Network", "network", "Can communicate over network"), - ("Inter-process communications", "ipc", "Can communicate with other applications") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_section(app_id, listbox, "Sockets", "sockets", [ - ("X11 windowing system", "x11", "Can access X11 display server"), - ("Wayland windowing system", "wayland", "Can access Wayland display server"), - ("Fallback to X11 windowing system", "fallback-x11", "Can fallback to X11 if Wayland unavailable"), - ("PulseAudio sound server", "pulseaudio", "Can access PulseAudio sound system"), - ("D-Bus session bus", "session-bus", "Can communicate with session D-Bus"), - ("D-Bus system bus", "system-bus", "Can communicate with system D-Bus"), - ("Secure Shell agent", "ssh-auth", "Can access SSH authentication agent"), - ("Smart cards", "pcsc", "Can access smart card readers"), - ("Printing system", "cups", "Can access printing subsystem"), - ("GPG-Agent directories", "gpg-agent", "Can access GPG keyring"), - ("Inherit Wayland socket", "inherit-wayland-socket", "Can inherit existing Wayland socket") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_section(app_id, listbox, "Devices", "devices", [ - ("GPU Acceleration", "dri", "Can use hardware graphics acceleration"), - ("Input devices", "input", "Can access input devices"), - ("Virtualization", "kvm", "Can access virtualization services"), - ("Shared memory", "shm", "Can use shared memory"), - ("All devices (e.g. webcam)", "all", "Can access all device files") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_section(app_id, listbox, "Features", "features", [ - ("Development syscalls", "devel", "Can perform development operations"), - ("Programs from other architectures", "multiarch", "Can execute programs from other architectures"), - ("Bluetooth", "bluetooth", "Can access Bluetooth hardware"), - ("Controller Area Network bus", "canbus", "Can access CAN bus"), - ("Application Shared Memory", "per-app-dev-shm", "Can use shared memory for IPC") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add Filesystems section - self._add_filesystem_section(app_id, app, listbox, "Filesystems") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_path_section(app_id, app, listbox, "Persistent", "persistent") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_path_section(app_id, app, listbox, "Environment", "environment") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_bus_section(app_id, app, listbox, "System Bus", "system_bus") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._add_bus_section(app_id, app, listbox, "Session Bus", "session_bus") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add Portals section - self._add_section(app_id, listbox, "Portals", section_options=[ - ("Background", "background", "Can run in the background"), - ("Notifications", "notifications", "Can send notifications"), - ("Microphone", "microphone", "Can listen to your microphone"), - ("Speakers", "speakers", "Can play sounds to your speakers"), - ("Camera", "camera", "Can record videos with your camera"), - ("Location", "location", "Can access your location") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add widgets to container - box_outer.pack_start(indicator, False, False, 0) - box_outer.pack_start(scrolled, True, True, 0) - scrolled.add(listbox) - - # Connect destroy signal - self.options_window.connect("destroy", lambda w: w.destroy()) - - # Show window - self.options_window.show_all() - - def _add_section(self, app_id, listbox, section_title, perm_type=None, section_options=None): - """Helper method to add a section with multiple options""" - - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Handle portal permissions specially - perms = {} - global_perms = {} - if section_title == "Portals": - success, perms = fp_turbo.portal_get_app_permissions(app_id) - if not success: - perms = {} - elif section_title in ["Persistent", "Environment", "System Bus", "Session Bus"]: - global_success, global_perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode) - if not global_success: - global_perms = {"paths": []} - success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode) - if not success: - perms = {"paths": []} - else: - global_success, global_perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode) - if not global_success: - global_perms = {"paths": []} - success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode) - if not success: - perms = {"paths": []} - if section_options: - # Add options - for display_text, option, description in section_options: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox.pack_start(vbox, True, True, 0) - - label = Gtk.Label(label=display_text, xalign=0) - label.get_style_context().add_class("permissions-item-label") - desc = Gtk.Label(label=description, xalign=0) - desc.get_style_context().add_class("permissions-item-summary") - vbox.pack_start(label, True, True, 0) - vbox.pack_start(desc, True, True, 0) - - switch = Gtk.Switch() - switch.props.valign = Gtk.Align.CENTER - - # Add indicator label before switch - switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Handle portal permissions differently - if section_title == "Portals": - if option in perms: - switch.set_active(perms[option] == 'yes') - switch.set_sensitive(True) - else: - switch.set_sensitive(False) - else: - # First check if option exists in either perms or global_perms - in_perms = option.lower() in [p.lower() for p in perms["paths"]] - in_global_perms = option.lower() in [p.lower() for p in global_perms["paths"]] - - # Set active state based on precedence rules - switch.set_active(in_global_perms or in_perms) - - # Set sensitivity based on your requirements - if in_global_perms: - switch.set_sensitive(False) # Global permissions take precedence - indicator = Gtk.Label(label="*", xalign=0) - indicator.get_style_context().add_class("global-indicator") - switch_box.pack_start(indicator, False, True, 0) - - elif in_perms: - switch.set_sensitive(True) # Local permissions enabled and sensitive - - switch_box.pack_start(switch, False, True, 0) - - switch.connect("state-set", self._on_switch_toggled, app_id, perm_type, option) - hbox.pack_end(switch_box, False, True, 0) - - listbox.add(row) - - def _on_switch_toggled(self, switch, state, app_id, perm_type, option): - """Handle switch toggle events""" - if perm_type is None: # Portal section - success, message = fp_turbo.portal_set_app_permissions( - option.lower(), - app_id, - "yes" if state else "no" - ) - else: - success, message = fp_turbo.toggle_other_perms( - app_id, - perm_type, - option.lower(), - state, - self.system_mode - ) - - if not success: - switch.set_active(not state) - print(f"Error: {message}") - - def _on_remove_path(self, button, app_id, app, path, perm_type=None): - """Handle remove path button click""" - if perm_type: - if perm_type == "persistent": - success, message = fp_turbo.remove_file_permissions( - app_id, - path, - "persistent", - self.system_mode - ) - elif perm_type == "filesystems": - success, message = fp_turbo.remove_file_permissions( - app_id, - path, - "filesystems", - self.system_mode - ) - else: - success, message = fp_turbo.remove_permission_value( - app_id, - perm_type, - path, - self.system_mode - ) - else: - success, message = fp_turbo.remove_file_permissions( - app_id, - path, - "filesystems", - self.system_mode - ) - if success: - # Refresh the current window - self.options_window.destroy() - self.on_app_options_clicked(None, app) - - def _on_add_path(self, button, app_id, app, perm_type=None): - """Handle add path button click""" - dialog = Gtk.Dialog( - title="Add Filesystem Path", - parent=self.options_window, - modal=True, - destroy_with_parent=True, - ) - - # Add buttons separately - dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) - dialog.add_button("Add", Gtk.ResponseType.OK) - - entry = Gtk.Entry() - entry.set_placeholder_text("Enter filesystem path") - dialog.vbox.pack_start(entry, True, True, 0) - dialog.show_all() - - response = dialog.run() - if response == Gtk.ResponseType.OK: - path = entry.get_text() - if perm_type: - if perm_type == "persistent": - success, message = fp_turbo.add_file_permissions( - app_id, - path, - "persistent", - self.system_mode - ) - elif perm_type == "filesystems": - success, message = fp_turbo.add_file_permissions( - app_id, - path, - "filesystems", - self.system_mode - ) - else: - success, message = fp_turbo.add_permission_value( - app_id, - perm_type, - path, - self.system_mode - ) - else: - success, message = fp_turbo.add_file_permissions( - app_id, - path, - "filesystems", - self.system_mode - ) - if success: - # Refresh the current window - self.options_window.destroy() - self.on_app_options_clicked(None, app) - message_type = Gtk.MessageType.INFO - else: - message_type = Gtk.MessageType.ERROR - if message: - error_dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text=message - ) - error_dialog.run() - error_dialog.destroy() - dialog.destroy() - - def _add_option(self, parent_box, label_text, description): - """Helper method to add an individual option""" - row = Gtk.ListBoxRow(selectable=False) - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox.pack_start(vbox, True, True, 0) - - label = Gtk.Label(label=label_text, xalign=0) - desc = Gtk.Label(label=description, xalign=0) - vbox.pack_start(label, True, True, 0) - vbox.pack_start(desc, True, True, 0) - - switch = Gtk.Switch() - switch.props.valign = Gtk.Align.CENTER - hbox.pack_end(switch, False, True, 0) - - parent_box.add(row) - return row, switch - - def _global_add_bus_section(self, listbox, section_title, perm_type): - """Helper method to add System Bus or Session Bus section""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Get permissions - success, perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode) - if not success: - perms = {"paths": []} - - # Add Talks section - talks_row = Gtk.ListBoxRow(selectable=False) - talks_row.get_style_context().add_class("permissions-row") - talks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - talks_box.get_style_context().add_class("permissions-bus-box") - talks_row.add(talks_box) - - talks_header = Gtk.Label(label="Talks", xalign=0) - talks_header.get_style_context().add_class("permissions-item-label") - talks_box.pack_start(talks_header, False, False, 0) - - # Add talk paths - for path in perms["paths"]: - if "talk" in path: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._global_on_remove_path, path, perm_type) - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - - talks_box.add(row) - - listbox.add(talks_row) - - # Add Owns section - owns_row = Gtk.ListBoxRow(selectable=False) - owns_row.get_style_context().add_class("permissions-row") - owns_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - owns_box.get_style_context().add_class("permissions-bus-box") - owns_row.add(owns_box) - - owns_header = Gtk.Label(label="Owns", xalign=0) - owns_header.get_style_context().add_class("permissions-item-label") - owns_box.pack_start(owns_header, False, False, 0) - - # Add own paths - for path in perms["paths"]: - if "own" in path: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._on_global_remove_path, path, perm_type) - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - - owns_box.add(row) - - owns_row.show_all() - listbox.add(owns_row) - - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add add button - add_path_row = Gtk.ListBoxRow(selectable=False) - add_path_row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - add_path_row.add(hbox) - - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-add-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._global_on_add_path, perm_type) - hbox.pack_end(btn, False, True, 0) - - listbox.add(add_path_row) - - def _global_add_path_section(self, listbox, section_title, perm_type): - """Helper method to add sections with paths (Persistent, Environment)""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Get permissions - if perm_type == "persistent": - success, perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode) - else: - success, perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode) - if not success: - perms = {"paths": []} - - # Add normal paths with remove buttons - for path in perms["paths"]: - if path != "": - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._global_on_remove_path, path, perm_type) - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - - listbox.add(row) - - # Add add button - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-add-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._global_on_add_path) - hbox.pack_end(btn, False, True, 0) - - listbox.add(row) - - def _global_add_filesystem_section(self, listbox, section_title): - """Helper method to add the Filesystems section""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - # Get filesystem permissions - success, perms = fp_turbo.global_list_file_perms(True, self.system_mode) - if not success: - perms = {"paths": [], "special_paths": []} - - # Add special paths as toggles - special_paths = [ - ("All user files", "home", "Access to all user files"), - ("All system files", "host", "Access to all system files"), - ("All system libraries, executables and static data", "host-os", "Access to system libraries and executables"), - ("All system configurations", "host-etc", "Access to system configurations") - ] - - for display_text, option, description in special_paths: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox.pack_start(vbox, True, True, 0) - - label = Gtk.Label(label=display_text, xalign=0) - label.get_style_context().add_class("permissions-item-label") - desc = Gtk.Label(label=description, xalign=0) - desc.get_style_context().add_class("permissions-item-summary") - vbox.pack_start(label, True, True, 0) - vbox.pack_start(desc, True, True, 0) - - switch = Gtk.Switch() - switch.props.valign = Gtk.Align.CENTER - switch.set_active(option in perms["special_paths"]) - switch.set_sensitive(True) - switch.connect("state-set", self._global_on_switch_toggled, "filesystems", option) - hbox.pack_end(switch, False, True, 0) - - listbox.add(row) - - # Add normal paths with remove buttons - for path in perms["paths"]: - if path != "": - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - #vbox.get_style_context().add_class("permissions-path-vbox") - vbox.set_size_request(400, 30) - hbox.pack_start(vbox, False, True, 0) - - text_view = Gtk.TextView() - text_view.set_size_request(400, 20) - text_view.get_style_context().add_class("permissions-path-text") - text_view.set_editable(False) - text_view.set_cursor_visible(False) - #text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - # Enable horizontal scrolling - scrolled_window = Gtk.ScrolledWindow() - #scrolled_window.get_style_context().add_class("permissions-path-scroll") - scrolled_window.set_hexpand(False) - scrolled_window.set_vexpand(False) - scrolled_window.set_size_request(400, 30) - scrolled_window.set_policy( - Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar - Gtk.PolicyType.NEVER # Disable vertical scrollbar - ) - - # Add TextView to ScrolledWindow - scrolled_window.add(text_view) - - # Add the text - buffer = text_view.get_buffer() - buffer.set_text(path) - - vbox.pack_start(scrolled_window, False, True, 0) - - btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # Create remove button - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-remove-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._global_on_remove_path, path, "filesystems") - btn_box.pack_end(btn, False, False, 0) - - hbox.pack_end(btn_box, False, False, 0) - - listbox.add(row) - - # Add add button - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - btn = Gtk.Button() - btn.set_size_request(26, 26) # 40x40 pixels - btn.get_style_context().add_class("app-action-button") - add_rm_icon = "list-add-symbolic" - use_icon = Gio.Icon.new_for_string(add_rm_icon) - btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - btn.connect("clicked", self._global_on_add_path, "filesystems") - hbox.pack_end(btn, False, True, 0) - - listbox.add(row) - - - def global_on_options_clicked(self, button): - """Handle the app options click""" - - # Create window (as before) - self.global_options_window = Gtk.Window(title="Global Setting Overrides") - self.global_options_window.set_default_size(600, 800) - - # Set subtitle - header_bar = Gtk.HeaderBar(title="Global Setting Overrides", - subtitle="Override list of resources selectively granted to applications") - header_bar.set_show_close_button(True) - self.global_options_window.set_titlebar(header_bar) - - # Create main container with padding - box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - box_outer.set_border_width(20) - self.global_options_window.add(box_outer) - - # Create scrolled window for content - scrolled = Gtk.ScrolledWindow() - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - - # Create list box for options - listbox = Gtk.ListBox() - listbox.set_selection_mode(Gtk.SelectionMode.NONE) - listbox.get_style_context().add_class("permissions-window") - - indicator = Gtk.Label(label="* = global override", xalign=1.0) - indicator.get_style_context().add_class("permissions-global-indicator") - - # No portals section. Portals are only handled on per-user basis. - - # Add other sections with correct permission types - self._global_add_section(listbox, "Shared", "shared", [ - ("Network", "network", "Can communicate over network"), - ("Inter-process communications", "ipc", "Can communicate with other applications") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_section(listbox, "Sockets", "sockets", [ - ("X11 windowing system", "x11", "Can access X11 display server"), - ("Wayland windowing system", "wayland", "Can access Wayland display server"), - ("Fallback to X11 windowing system", "fallback-x11", "Can fallback to X11 if Wayland unavailable"), - ("PulseAudio sound server", "pulseaudio", "Can access PulseAudio sound system"), - ("D-Bus session bus", "session-bus", "Can communicate with session D-Bus"), - ("D-Bus system bus", "system-bus", "Can communicate with system D-Bus"), - ("Secure Shell agent", "ssh-auth", "Can access SSH authentication agent"), - ("Smart cards", "pcsc", "Can access smart card readers"), - ("Printing system", "cups", "Can access printing subsystem"), - ("GPG-Agent directories", "gpg-agent", "Can access GPG keyring"), - ("Inherit Wayland socket", "inherit-wayland-socket", "Can inherit existing Wayland socket") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_section(listbox, "Devices", "devices", [ - ("GPU Acceleration", "dri", "Can use hardware graphics acceleration"), - ("Input devices", "input", "Can access input devices"), - ("Virtualization", "kvm", "Can access virtualization services"), - ("Shared memory", "shm", "Can use shared memory"), - ("All devices (e.g. webcam)", "all", "Can access all device files") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_section(listbox, "Features", "features", [ - ("Development syscalls", "devel", "Can perform development operations"), - ("Programs from other architectures", "multiarch", "Can execute programs from other architectures"), - ("Bluetooth", "bluetooth", "Can access Bluetooth hardware"), - ("Controller Area Network bus", "canbus", "Can access CAN bus"), - ("Application Shared Memory", "per-app-dev-shm", "Can use shared memory for IPC") - ]) - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add Filesystems section - self._global_add_filesystem_section(listbox, "Filesystems") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_path_section(listbox, "Persistent", "persistent") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_path_section(listbox, "Environment", "environment") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_bus_section(listbox, "System Bus", "system_bus") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - self._global_add_bus_section(listbox, "Session Bus", "session_bus") - spacing_box = Gtk.ListBoxRow(selectable=False) - spacing_box.get_style_context().add_class("permissions-spacing-box") - listbox.add(spacing_box) - - # Add widgets to container - box_outer.pack_start(scrolled, True, True, 0) - scrolled.add(listbox) - - # Connect destroy signal - self.global_options_window.connect("destroy", lambda w: w.destroy()) - - # Show window - self.global_options_window.show_all() - - def _global_add_section(self, listbox, section_title, perm_type=None, section_options=None): - """Helper method to add a section with multiple options""" - # Add section header - row_header = Gtk.ListBoxRow(selectable=False) - row_header.get_style_context().add_class("permissions-row") - box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - label_header = Gtk.Label(label=f"{section_title}", - use_markup=True, xalign=0) - label_header.get_style_context().add_class("permissions-header-label") - box_header.pack_start(label_header, True, True, 0) - box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) - row_header.add(box_header) - listbox.add(row_header) - - if section_title in ["Persistent", "Environment", "System Bus", "Session Bus"]: - success, perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode) - if not success: - perms = {"paths": []} - else: - success, perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode) - if not success: - perms = {"paths": []} - - if section_options: - # Add options - for display_text, option, description in section_options: - row = Gtk.ListBoxRow(selectable=False) - row.get_style_context().add_class("permissions-row") - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox.pack_start(vbox, True, True, 0) - - label = Gtk.Label(label=display_text, xalign=0) - label.get_style_context().add_class("permissions-item-label") - desc = Gtk.Label(label=description, xalign=0) - desc.get_style_context().add_class("permissions-item-summary") - vbox.pack_start(label, True, True, 0) - vbox.pack_start(desc, True, True, 0) - - switch = Gtk.Switch() - switch.props.valign = Gtk.Align.CENTER - - # Handle portal permissions differently - if section_title == "Portals": - if option in perms: - switch.set_active(perms[option] == 'yes') - switch.set_sensitive(True) - else: - switch.set_sensitive(False) - else: - switch.set_active(option in [p.lower() for p in perms["paths"]]) - switch.set_sensitive(True) - - switch.connect("state-set", self._global_on_switch_toggled, perm_type, option) - hbox.pack_end(switch, False, True, 0) - - listbox.add(row) - - def _global_on_switch_toggled(self, switch, state, perm_type, option): - """Handle switch toggle events""" - success, message = fp_turbo.global_toggle_other_perms( - perm_type, - option.lower(), - state, - True, - self.system_mode - ) - - if not success: - switch.set_active(not state) - print(f"Error: {message}") - - def _global_on_remove_path(self, button, path, perm_type=None): - """Handle remove path button click""" - if perm_type: - if perm_type == "persistent": - success, message = fp_turbo.global_remove_file_permissions( - path, - "persistent", - True, - self.system_mode - ) - elif perm_type == "filesystems": - success, message = fp_turbo.global_remove_file_permissions( - path, - "filesystems", - True, - self.system_mode - ) - else: - success, message = fp_turbo.global_remove_permission_value( - perm_type, - path, - True, - self.system_mode - ) - else: - success, message = fp_turbo.global_remove_file_permissions( - path, - "filesystems", - True, - self.system_mode - ) - if success: - # Refresh the current window - self.global_options_window.destroy() - self.global_on_options_clicked(None) - - def _global_on_add_path(self, button, perm_type=None): - """Handle add path button click""" - dialog = Gtk.Dialog( - title="Add Filesystem Path", - parent=self.global_options_window, - modal=True, - destroy_with_parent=True, - ) - - # Add buttons separately - dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) - dialog.add_button("Add", Gtk.ResponseType.OK) - - entry = Gtk.Entry() - entry.set_placeholder_text("Enter filesystem path") - dialog.vbox.pack_start(entry, True, True, 0) - dialog.show_all() - - response = dialog.run() - if response == Gtk.ResponseType.OK: - path = entry.get_text() - if perm_type: - if perm_type == "persistent": - success, message = fp_turbo.global_add_file_permissions( - path, - "persistent", - True, - self.system_mode - ) - elif perm_type == "filesystems": - success, message = fp_turbo.global_add_file_permissions( - path, - "filesystems", - True, - self.system_mode - ) - else: - success, message = fp_turbo.global_add_permission_value( - perm_type, - path, - True, - self.system_mode - ) - else: - success, message = fp_turbo.global_add_file_permissions( - path, - "filesystems", - True, - self.system_mode - ) - if success: - # Refresh the current window - self.global_options_window.destroy() - self.global_on_options_clicked(None) - message_type = Gtk.MessageType.INFO - else: - message_type = Gtk.MessageType.ERROR - if message: - error_dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text=message - ) - error_dialog.run() - error_dialog.destroy() - dialog.destroy() - - def _global_add_option(self, parent_box, label_text, description): - """Helper method to add an individual option""" - row = Gtk.ListBoxRow(selectable=False) - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) - row.add(hbox) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox.pack_start(vbox, True, True, 0) - - label = Gtk.Label(label=label_text, xalign=0) - desc = Gtk.Label(label=description, xalign=0) - vbox.pack_start(label, True, True, 0) - vbox.pack_start(desc, True, True, 0) - - switch = Gtk.Switch() - switch.props.valign = Gtk.Align.CENTER - hbox.pack_end(switch, False, True, 0) - - parent_box.add(row) - return row, switch - - - def on_update_clicked(self, button, app): - """Handle the Remove button click with removal options""" - details = app.get_details() - - # Create dialog - dialog = Gtk.Dialog( - title=f"Update {details['name']}?", - transient_for=self, - modal=True, - destroy_with_parent=True, - ) - # Add buttons using the new method - dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) - dialog.add_button("Update", Gtk.ResponseType.OK) - - # Create content area - content_area = dialog.get_content_area() - content_area.set_spacing(12) - content_area.set_border_width(12) - - content_area.pack_start(Gtk.Label(label=f"Update: {details['id']}?"), False, False, 0) - - # Show dialog - dialog.show_all() - - # Run dialog - response = dialog.run() - if response == Gtk.ResponseType.OK: - # Perform Removal - def perform_update(): - # Show waiting dialog - GLib.idle_add(self.show_waiting_dialog, "Updating package...") - - success, message = fp_turbo.update_flatpak(app, self.system_mode) - - # Update UI on main thread - GLib.idle_add(lambda: self.on_task_complete(dialog, success, message)) - - # Start spinner and begin installation - thread = threading.Thread(target=perform_update) - thread.daemon = True # Allow program to exit even if thread is still running - thread.start() - - dialog.destroy() - - def download_screenshot(self, url, local_path): - """Download a screenshot and save it locally""" - try: - # Download the image - response = requests.get(url) - response.raise_for_status() - - # Create the directory if it doesn't exist - os.makedirs(os.path.dirname(local_path), exist_ok=True) - - # Save the image - with open(local_path, 'wb') as f: - f.write(response.content) - - return True - except Exception as e: - print(f"Error downloading screenshot {url}: {e}") - return False - - def create_screenshot_slideshow(self, screenshots, app_id): - # Create main container for slideshow - slideshow_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - slideshow_box.set_border_width(0) - - # Create main frame for the current screenshot (removed border) - main_frame = Gtk.Frame() - main_frame.set_size_request(400, 300) # Adjust size as needed - main_frame.set_shadow_type(Gtk.ShadowType.NONE) - slideshow_box.pack_start(main_frame, True, True, 0) - - # Create image for current screenshot - current_image = Gtk.Image() - current_image.set_size_request(400, 300) # Adjust size as needed - main_frame.add(current_image) - - # Create box for navigation dots - nav_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - nav_box.set_halign(Gtk.Align.CENTER) - nav_box.set_border_width(0) # Remove border - slideshow_box.pack_start(nav_box, False, True, 0) - - # Create navigation dots - dots = [] - for i in range(len(screenshots)): - # Create new EventBox for each dot - event_box = Gtk.EventBox() - event_box.set_border_width(0) - - # Create bullet using Label - bullet = Gtk.Label(label="•") - bullet.get_style_context().add_class("screenshot-bullet") - bullet.set_opacity(0.3 if i > 0 else 1.0) # First dot is active - - # Add bullet to event box - event_box.add(bullet) - - # Connect navigation - event_box.connect('button-release-event', - lambda w, e, idx=i: self._switch_screenshot( - current_image, screenshots, dots, idx, app_id)) - - # Add event box to nav box - nav_box.pack_start(event_box, False, True, 0) - - # Store the event box - dots.append(event_box) - - # Load first screenshot - self._load_screenshot(current_image, screenshots[0], app_id) - - return slideshow_box - - def _load_screenshot(self, image, screenshot, app_id): - """Helper method to load a single screenshot""" - home_dir = os.path.expanduser("~") - - # Get URL using fp_turbo.screenshot_details() like in your original code - image_data = fp_turbo.screenshot_details(screenshot) - url = image_data.get_url() - - local_path = f"{home_dir}/.local/share/flatpost/app-screenshots/{app_id}/{os.path.basename(url)}" - - if os.path.exists(local_path): - image.set_from_file(local_path) - else: - if fp_turbo.check_internet(): - try: - if not self.download_screenshot(url, local_path): - print("Failed to download screenshot") - return - image.set_from_file(local_path) - except Exception: - image.set_from_icon_name('image-x-generic', Gtk.IconSize.MENU) - else: - image.set_from_icon_name('image-x-generic', Gtk.IconSize.MENU) - - def _switch_screenshot(self, image, screenshots, dots, index, app_id): - # Update dots opacity - for i, dot in enumerate(dots): - # Get the bullet label from the event box - bullet = dot.get_children()[0] - bullet.set_opacity(1.0 if i == index else 0.3) - - # Load the new screenshot - self._load_screenshot(image, screenshots[index], app_id) - - def _create_details_window(self, details): - """Create and configure the main details window.""" - self.details_window = Gtk.Window(title=f"{details['name']}") - self.details_window.set_default_size(900, 600) - - # Set header bar - header_bar = Gtk.HeaderBar( - title=f"{details['name']}", - subtitle="List of resources selectively granted to the application" - ) - header_bar.set_show_close_button(True) - self.details_window.set_titlebar(header_bar) - - # Create main container - box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - box_outer.set_border_width(20) - box_outer.set_border_width(0) - self.details_window.add(box_outer) - - return box_outer - - def _create_content_area(self, box_outer): - """Create the scrolled content area.""" - scrolled = Gtk.ScrolledWindow() - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scrolled.set_border_width(0) - - content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - content_box.set_border_width(0) - content_box.get_style_context().add_class("details-window") - scrolled.add(content_box) - - box_outer.pack_start(scrolled, True, True, 0) - return content_box - - def _create_icon_section(self, content_box, details): - """Create the icon section of the details window.""" - icon_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - icon_row.set_border_width(0) - - icon_box = Gtk.Box() - icon_box.set_size_request(88, -1) - - app_icon = Gio.Icon.new_for_string('package-x-generic-symbolic') - icon_widget = self.create_scaled_icon(app_icon, is_themed=True) - - if details['icon_filename'] and Path(details['icon_path_128'] + "/" + details['icon_filename']).exists(): - icon_widget = self.create_scaled_icon( - f"{details['icon_path_128']}/{details['icon_filename']}", - is_themed=False - ) - - icon_widget.set_size_request(64, 64) - icon_box.pack_start(icon_widget, True, True, 0) - - content_box.pack_start(icon_row, False, True, 0) - content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) - - def _create_info_section(self, content_box, details): - """Create the information section with name, version, and developer.""" - info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - - # Middle column - middle_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - name_label = Gtk.Label(label=f"{details['name']}") - name_label.get_style_context().add_class("large-title") - name_label.set_xalign(0) - version_label = Gtk.Label(label=f"Version {details['version']}") - version_label.set_xalign(0) - developer_label = Gtk.Label(label=f"Developer: {details['developer']}") - developer_label.set_xalign(0) - - middle_column.pack_start(name_label, False, True, 0) - middle_column.pack_start(version_label, False, True, 0) - middle_column.pack_start(developer_label, False, True, 0) - - # Right column - right_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - id_label = Gtk.Label(label=f"ID: {details['id']}") - id_label.set_xalign(0) - kind_label = Gtk.Label(label=f"Kind: {details['kind']}") - kind_label.set_xalign(0) - right_column.pack_start(id_label, False, True, 0) - right_column.pack_start(kind_label, False, True, 0) - - info_box.pack_start(middle_column, True, True, 0) - info_box.pack_start(right_column, False, True, 0) - - content_box.pack_start(info_box, False, True, 0) - content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) - - def _create_text_section(self, title, text): - """Create a text section with title and content.""" - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - - title_label = Gtk.Label(label=f"{title}:") - title_label.set_xalign(0) - - text_view = Gtk.TextView() - text_view.set_editable(False) - text_view.set_cursor_visible(False) - text_view.set_wrap_mode(Gtk.WrapMode.WORD) - text_view.get_style_context().add_class("details-textview") - - # Parse HTML and insert into TextView - buffer = text_view.get_buffer() - if title == "Description": - try: - - class TextExtractor(HTMLParser): - def __init__(self): - super().__init__() - self.text = [] - - def handle_data(self, data): - self.text.append(data) - - def handle_starttag(self, tag, attrs): - if tag == 'p': - self.text.append('\n') - elif tag == 'ul': - self.text.append('\n') - elif tag == 'li': - self.text.append('• ') - - def handle_endtag(self, tag): - if tag == 'li': - self.text.append('\n') - elif tag == 'ul': - self.text.append('\n') - - # Parse the HTML - parser = TextExtractor() - parser.feed(text) - parsed_text = ''.join(parser.text) - - # Add basic HTML styling - buffer.set_text(parsed_text) - text_view.set_left_margin(10) - text_view.set_right_margin(10) - text_view.set_pixels_above_lines(4) - text_view.set_pixels_below_lines(4) - - except Exception: - # Fallback to plain text if HTML parsing fails - buffer.set_text(text) - else: - buffer.set_text(text) - - box.pack_start(title_label, False, True, 0) - box.pack_start(text_view, True, True, 0) - return box - - def _create_url_section(self, url_type, url): - """Create a URL section with clickable link.""" - box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - - label_widget = Gtk.Label(label=f"{url_type.capitalize()}:") - label_widget.set_xalign(0) - - url_label = Gtk.Label(label=url) - url_label.set_use_underline(True) - url_label.set_use_markup(True) - url_label.set_markup(f'{url}') - url_label.set_halign(Gtk.Align.START) - - event_box = Gtk.EventBox() - event_box.add(url_label) - event_box.connect("button-release-event", - lambda w, e: Gio.AppInfo.launch_default_for_uri(url)) - - box.pack_start(label_widget, False, True, 0) - box.pack_start(event_box, True, True, 0) - return box - - def on_details_clicked(self, button, app): - """Initialize the details window setup process.""" - details = app.get_details() - - # Create window and main container - box_outer = self._create_details_window(details) - - # Create content area - content_box = self._create_content_area(box_outer) - - # Add icon section - self._create_icon_section(content_box, details) - - # Add info section - self._create_info_section(content_box, details) - - # Add screenshots - screenshot_slideshow = self.create_screenshot_slideshow(details['screenshots'], details['id']) - screenshot_slideshow.set_border_width(0) - content_box.pack_start(screenshot_slideshow, False, True, 0) - - # Add summary section - summary_section = self._create_text_section("Summary", details['summary']) - content_box.pack_start(summary_section, False, True, 0) - content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), - False, False, 0) - - # Add URLs section - urls_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - - for url_type, url in details['urls'].items(): - row = self._create_url_section(url_type, url) - urls_section.pack_start(row, False, True, 0) - urls_section.pack_start(self._create_url_section("Flathub Page", - f"https://flathub.org/apps/details/{details['id']}"), False, True, 0) - content_box.pack_start(urls_section, False, True, 0) - content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), - False, False, 0) - - # Add description section - description_section = self._create_text_section("Description", - details['description']) - content_box.pack_start(description_section, False, True, 0) - - # Connect destroy signal and show window - self.details_window.connect("destroy", lambda w: w.destroy()) - self.details_window.show_all() - # With these lines: - children = self.details_window.get_children() - if children: - first_child = children[0] - if first_child.get_children(): - scrolled = first_child.get_children()[0] - scrolled.get_vadjustment().set_value(0) - else: - scrolled = None - else: - scrolled = None - - - def on_donate_clicked(self, button, app): - """Handle the Donate button click""" - details = app.get_details() - donation_url = details.get('urls', {}).get('donation', '') - if donation_url: - try: - Gio.AppInfo.launch_default_for_uri(donation_url, None) - except Exception as e: - print(f"Error opening donation URL: {str(e)}") - - def on_repo_toggled(self, checkbox, repo): - """Handle repository enable/disable toggle""" - repo.set_disabled(checkbox.get_active()) - # Update the UI to reflect the new state - checkbox.get_parent().set_sensitive(True) - if checkbox.get_active(): - checkbox.get_style_context().remove_class("dim-label") - success, message = fp_turbo.repotoggle(repo.get_name(), True, self.system_mode) - message_type = Gtk.MessageType.INFO - if success: - self.refresh_local() - else: - if message: - message_type = Gtk.MessageType.ERROR - if message: - dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text=message - ) - dialog.run() - dialog.destroy() - else: - checkbox.get_style_context().add_class("dim-label") - success, message = fp_turbo.repotoggle(repo.get_name(), False, self.system_mode) - message_type = Gtk.MessageType.INFO - if success: - self.refresh_local() - else: - if message: - message_type = Gtk.MessageType.ERROR - if message: - dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text=message - ) - dialog.run() - dialog.destroy() - - def on_repo_delete(self, button, repo): - """Handle repository deletion""" - dialog = Gtk.MessageDialog( - transient_for=self, - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.WARNING, - buttons=Gtk.ButtonsType.YES_NO, - text=f"Are you sure you want to delete the '{repo.get_name()}' repository?" - ) - - response = dialog.run() - dialog.destroy() - - if response == Gtk.ResponseType.YES: - try: - fp_turbo.repodelete(repo.get_name(), self.system_mode) - self.refresh_local() - self.show_category_apps('repositories') - except GLib.GError as e: - # Handle polkit authentication failure - if "not allowed for user" in str(e): - error_dialog = Gtk.MessageDialog( - transient_for=self, - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text="You don't have permission to remove this repository. " - "Please try running the application with sudo privileges." - ) - error_dialog.run() - error_dialog.destroy() - else: - # Handle other potential errors - error_dialog = Gtk.MessageDialog( - transient_for=self, - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=f"Failed to remove repository: {str(e)}" - ) - error_dialog.run() - error_dialog.destroy() - - def on_add_flathub_repo_button_clicked(self, button): - """Handle the Add Flathub Repository button click""" - # Add the repository - success, error_message = fp_turbo.repoadd("https://dl.flathub.org/repo/flathub.flatpakrepo", self.system_mode) - if error_message: - error_dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=error_message - ) - error_dialog.run() - error_dialog.destroy() - self.refresh_local() - self.show_category_apps('repositories') - - def on_add_flathub_beta_repo_button_clicked(self, button): - """Handle the Add Flathub Beta Repository button click""" - # Add the repository - success, error_message = fp_turbo.repoadd("https://dl.flathub.org/beta-repo/flathub-beta.flatpakrepo", self.system_mode) - if error_message: - error_dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=error_message - ) - error_dialog.run() - error_dialog.destroy() - self.refresh_local() - self.show_category_apps('repositories') - - def on_add_repo_button_clicked(self, button=None, file_path=None): - """Handle the Add Repository button click""" - response = Gtk.ResponseType.CANCEL - dialog = Gtk.Dialog( - title="Install?", - transient_for=self, - modal=True, - destroy_with_parent=True, - ) - repo_file_path = "" - # Create file chooser dialog - if button and not file_path: - dialog = Gtk.FileChooserDialog( - title="Select Repository File", - parent=self, - action=Gtk.FileChooserAction.OPEN, - flags=0 - ) - - # Add buttons using the new method - dialog.add_buttons( - "Cancel", Gtk.ResponseType.CANCEL, - "Open", Gtk.ResponseType.OK - ) - - # Add filter for .flatpakrepo files - repo_filter = Gtk.FileFilter() - repo_filter.set_name("Flatpak Repository Files") - repo_filter.add_pattern("*.flatpakrepo") - dialog.add_filter(repo_filter) - - # Show all files filter - all_filter = Gtk.FileFilter() - all_filter.set_name("All Files") - all_filter.add_pattern("*") - dialog.add_filter(all_filter) - response = dialog.run() - repo_file_path = dialog.get_filename() - elif file_path and not button: - # Create dialog - dialog = Gtk.Dialog( - title=f"Install {file_path}?", - transient_for=self, - modal=True, - destroy_with_parent=True, - ) - # Add buttons using the new method - dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) - dialog.add_button("Install", Gtk.ResponseType.OK) - - # Create content area - content_area = dialog.get_content_area() - content_area.set_spacing(12) - content_area.set_border_width(12) - - content_area.pack_start(Gtk.Label(label=f"Install {file_path}?"), False, False, 0) - - if self.system_mode is False: - content_area.pack_start(Gtk.Label(label="Installation Type: User"), False, False, 0) - else: - content_area.pack_start(Gtk.Label(label="Installation Type: System"), False, False, 0) - dialog.show_all() - response = dialog.run() - repo_file_path = file_path - dialog.destroy() - - if response == Gtk.ResponseType.OK and repo_file_path: - # Add the repository - success, error_message = fp_turbo.repoadd(repo_file_path, self.system_mode) - if error_message: - error_dialog = Gtk.MessageDialog( - transient_for=None, # Changed from self - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=error_message - ) - error_dialog.run() - error_dialog.destroy() - self.refresh_local() - self.show_category_apps('repositories') - - def select_default_category(self): - # Select Trending by default - if 'collections' in self.category_widgets and self.category_widgets['collections']: - self.on_category_clicked('trending', 'collections') + search_entry = self.gui.create_search_entry() + header.addWidget(search_entry) + + # Add system mode switch + system_switch = self.gui.create_switch("System Mode", self.system_mode) + header.addWidget(system_switch) + + def run(self): + """Run the application.""" + self.gui.run() def main(): - # Initialize GTK before anything else - if not Gtk.init_check(): - print("Failed to initialize GTK") - return 1 - - system_mode = False - system_only_mode = False - # Check for command line argument - if len(sys.argv) > 1: - arg = sys.argv[1] - if arg == '--system-mode': - system_mode = True - if arg == '--system-only-mode': - system_mode = True - system_only_mode = True - if arg.endswith('.flatpakref'): - # Create a temporary window just to handle the installation - app = MainWindow(system_mode=system_mode, system_only_mode=system_only_mode) - app.handle_flatpakref_file(arg) - # Keep the window open for 5 seconds to show the result - GLib.timeout_add_seconds(5, Gtk.main_quit) - Gtk.main() - return - if arg.endswith('.flatpakrepo'): - # Create a temporary window just to handle the installation - app = MainWindow(system_mode=system_mode, system_only_mode=system_only_mode) - app.handle_flatpakrepo_file(arg) - # Keep the window open for 5 seconds to show the result - GLib.timeout_add_seconds(5, Gtk.main_quit) - Gtk.main() - return - - if system_mode or system_only_mode: - if os.getuid() > 0: - script_path = Path(__file__).resolve() - os.execvp( - "pkexec", - [ - "pkexec", - "--disable-internal-agent", - "env", - f"DISPLAY={os.environ['DISPLAY']}", - f"XAUTHORITY={os.environ.get('XAUTHORITY', '')}", - f"XDG_CURRENT_DESKTOP={os.environ.get('XDG_CURRENT_DESKTOP', '').lower()}", - f"ORIG_USER={os.getuid()!s}", - f"PKEXEC_UID={os.getuid()!s}", - "G_MESSAGES_DEBUG=none", - sys.executable, - str(script_path), - arg, - ] - ) - app = MainWindow(system_mode=system_mode, system_only_mode=system_only_mode) - app.connect("destroy", Gtk.main_quit) - app.show_all() - Gtk.main() + # Parse command line arguments + import argparse + parser = argparse.ArgumentParser(description="Flatpost - Flatpak Store") + parser.add_argument("--system", action="store_true", help="Run in system mode") + parser.add_argument("--system-only", action="store_true", help="Run in system-only mode") + parser.add_argument("--gui", choices=GUIFactory.get_available_backends(), default="gtk", + help="GUI backend to use") + args = parser.parse_args() + + # Create and run application + app = FlatpostApp( + system_mode=args.system, + system_only_mode=args.system_only, + gui_backend=args.gui + ) + app.run() if __name__ == "__main__": main() diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..99fb40f --- /dev/null +++ b/src/gui/__init__.py @@ -0,0 +1,4 @@ +""" +Flatpost GUI package. +This package contains the GUI implementations for Flatpost. +""" \ No newline at end of file diff --git a/src/gui/base.py b/src/gui/base.py new file mode 100644 index 0000000..9cd40dc --- /dev/null +++ b/src/gui/base.py @@ -0,0 +1,101 @@ +from abc import ABC, abstractmethod +from typing import List, Dict, Any, Optional + +class BaseGUI(ABC): + """Abstract base class for GUI implementations.""" + + @abstractmethod + def __init__(self, system_mode: bool = False, system_only_mode: bool = False): + """Initialize the GUI with system mode settings.""" + pass + + @abstractmethod + def create_window(self): + """Create the main application window.""" + pass + + @abstractmethod + def create_header_bar(self): + """Create the header bar with controls.""" + pass + + @abstractmethod + def create_panels(self): + """Create the main panels for the application.""" + pass + + @abstractmethod + def create_applications_panel(self, title: str): + """Create the applications panel.""" + pass + + @abstractmethod + def display_apps(self, apps: List[Dict[str, Any]]): + """Display the list of applications.""" + pass + + @abstractmethod + def show_waiting_dialog(self, message: str = "Please wait while task is running..."): + """Show a waiting dialog.""" + pass + + @abstractmethod + def show_error_dialog(self, message: str): + """Show an error dialog.""" + pass + + @abstractmethod + def show_info_dialog(self, message: str): + """Show an information dialog.""" + pass + + @abstractmethod + def create_button(self, callback, app: Optional[Dict[str, Any]] = None, + label: Optional[str] = None, condition: Optional[bool] = None): + """Create a button with the given properties.""" + pass + + @abstractmethod + def create_search_entry(self): + """Create a search entry widget.""" + pass + + @abstractmethod + def create_dropdown(self, items: List[str]): + """Create a dropdown menu with the given items.""" + pass + + @abstractmethod + def create_switch(self, label: str, active: bool = False): + """Create a switch widget.""" + pass + + @abstractmethod + def create_label(self, text: str): + """Create a label widget.""" + pass + + @abstractmethod + def create_image(self, path: str, size: int): + """Create an image widget.""" + pass + + @abstractmethod + def create_scrollable_container(self): + """Create a scrollable container.""" + pass + + @abstractmethod + def create_box(self, orientation: str = "vertical"): + """Create a box container.""" + pass + + @abstractmethod + def create_list_box(self): + """Create a list box widget.""" + pass + + @abstractmethod + def run(self): + """Run the main application loop.""" + pass \ No newline at end of file diff --git a/src/gui/factory.py b/src/gui/factory.py new file mode 100644 index 0000000..55e977c --- /dev/null +++ b/src/gui/factory.py @@ -0,0 +1,49 @@ +import platform +from typing import Optional +from .base import BaseGUI +from .gtk_gui import GTKGUI +from .qt_gui import QtGUI + +class GUIFactory: + """Factory class for creating GUI backends.""" + + @staticmethod + def create_gui(backend: str = None, system_mode: bool = False, system_only_mode: bool = False) -> BaseGUI: + """ + Create a GUI instance with the specified backend. + + Args: + backend: The GUI backend to use ("gtk" or "qt"). If None, will use platform default. + system_mode: Whether to run in system mode + system_only_mode: Whether to run in system-only mode + + Returns: + A GUI instance implementing the BaseGUI interface + + Raises: + ValueError: If an invalid backend is specified + """ + if backend is None: + # Use platform-specific default + if platform.system() == "Windows": + backend = "qt" + else: + backend = "gtk" + + backend = backend.lower() + + if backend == "gtk": + if platform.system() == "Windows": + raise ValueError("GTK backend is not supported on Windows. Please use Qt backend.") + return GTKGUI(system_mode, system_only_mode) + elif backend == "qt": + return QtGUI(system_mode, system_only_mode) + else: + raise ValueError(f"Invalid GUI backend: {backend}. Must be 'gtk' or 'qt'") + + @staticmethod + def get_available_backends() -> list[str]: + """Get a list of available GUI backends for the current platform.""" + if platform.system() == "Windows": + return ["qt"] + return ["gtk", "qt"] \ No newline at end of file diff --git a/src/gui/gtk_gui.py b/src/gui/gtk_gui.py new file mode 100644 index 0000000..dbc4f1c --- /dev/null +++ b/src/gui/gtk_gui.py @@ -0,0 +1,135 @@ +import gi +gi.require_version("Gtk", "3.0") +gi.require_version("GLib", "2.0") +gi.require_version("Flatpak", "1.0") +gi.require_version('GdkPixbuf', '2.0') +from gi.repository import Gtk, Gio, Gdk, GLib, GdkPixbuf +from typing import List, Dict, Any, Optional, Callable +from .base import BaseGUI + +class GTKGUI(BaseGUI): + def __init__(self, system_mode: bool = False, system_only_mode: bool = False): + self.system_mode = system_mode + self.system_only_mode = system_only_mode + self.window = None + self.create_window() + + def create_window(self): + app_title = "Flatpost (user mode)" + if self.system_only_mode: + app_title = "Flatpost (system-only mode)" + elif self.system_mode: + app_title = "Flatpost (system mode)" + + self.window = Gtk.Window(title=app_title) + self.window.set_default_size(1280, 720) + + # Enable drag and drop + self.window.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) + self.window.drag_dest_add_uri_targets() + + def create_header_bar(self): + header = Gtk.HeaderBar() + header.set_show_close_button(True) + self.window.set_titlebar(header) + return header + + def create_panels(self): + main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.window.add(main_box) + return main_box + + def create_applications_panel(self, title: str): + panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + label = Gtk.Label(label=title) + panel.pack_start(label, False, False, 0) + return panel + + def display_apps(self, apps: List[Dict[str, Any]]): + # Implementation will be similar to the original GTK code + pass + + def show_waiting_dialog(self, message: str = "Please wait while task is running..."): + dialog = Gtk.Dialog(title="Please Wait", parent=self.window, flags=0) + dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) + + content_area = dialog.get_content_area() + label = Gtk.Label(label=message) + content_area.add(label) + + dialog.show_all() + return dialog + + def show_error_dialog(self, message: str): + dialog = Gtk.MessageDialog( + parent=self.window, + flags=0, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, + text=message + ) + dialog.run() + dialog.destroy() + + def show_info_dialog(self, message: str): + dialog = Gtk.MessageDialog( + parent=self.window, + flags=0, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text=message + ) + dialog.run() + dialog.destroy() + + def create_button(self, callback: Callable, app: Optional[Dict[str, Any]] = None, + label: Optional[str] = None, condition: Optional[bool] = None): + button = Gtk.Button(label=label if label else "") + if callback: + if app: + button.connect("clicked", callback, app) + else: + button.connect("clicked", callback) + if condition is not None: + button.set_sensitive(condition) + return button + + def create_search_entry(self): + return Gtk.SearchEntry() + + def create_dropdown(self, items: List[str]): + combo = Gtk.ComboBoxText() + for item in items: + combo.append_text(item) + return combo + + def create_switch(self, label: str, active: bool = False): + switch = Gtk.Switch() + switch.set_active(active) + return switch + + def create_label(self, text: str): + return Gtk.Label(label=text) + + def create_image(self, path: str, size: int): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, size, size, True) + image = Gtk.Image.new_from_pixbuf(pixbuf) + return image + + def create_scrollable_container(self): + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + return scrolled + + def create_box(self, orientation: str = "vertical"): + if orientation == "vertical": + return Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + return Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + def create_list_box(self): + return Gtk.ListBox() + + def run(self): + self.window.connect("destroy", Gtk.main_quit) + self.window.show_all() + Gtk.main() \ No newline at end of file diff --git a/src/gui/qt_gui.py b/src/gui/qt_gui.py new file mode 100644 index 0000000..eb6766d --- /dev/null +++ b/src/gui/qt_gui.py @@ -0,0 +1,127 @@ +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QLineEdit, QComboBox, QScrollArea, QListWidget, + QMessageBox, QDialog, QProgressBar, QSwitch, QFrame +) +from PySide6.QtCore import Qt, Signal, Slot +from PySide6.QtGui import QPixmap, QImage +from typing import List, Dict, Any, Optional, Callable +from .base import BaseGUI + +class QtGUI(BaseGUI): + def __init__(self, system_mode: bool = False, system_only_mode: bool = False): + self.system_mode = system_mode + self.system_only_mode = system_only_mode + self.window = None + self.create_window() + + def create_window(self): + app_title = "Flatpost (user mode)" + if self.system_only_mode: + app_title = "Flatpost (system-only mode)" + elif self.system_mode: + app_title = "Flatpost (system mode)" + + self.window = QMainWindow() + self.window.setWindowTitle(app_title) + self.window.resize(1280, 720) + + def create_header_bar(self): + header = QWidget() + layout = QHBoxLayout(header) + header.setLayout(layout) + return header + + def create_panels(self): + central_widget = QWidget() + layout = QHBoxLayout(central_widget) + self.window.setCentralWidget(central_widget) + return layout + + def create_applications_panel(self, title: str): + panel = QWidget() + layout = QVBoxLayout(panel) + label = QLabel(title) + layout.addWidget(label) + return panel + + def display_apps(self, apps: List[Dict[str, Any]]): + # Implementation will be similar to the GTK version but using Qt widgets + pass + + def show_waiting_dialog(self, message: str = "Please wait while task is running..."): + dialog = QDialog(self.window) + dialog.setWindowTitle("Please Wait") + + layout = QVBoxLayout(dialog) + label = QLabel(message) + progress = QProgressBar() + progress.setRange(0, 0) # Indeterminate progress + + layout.addWidget(label) + layout.addWidget(progress) + + dialog.show() + return dialog + + def show_error_dialog(self, message: str): + QMessageBox.critical(self.window, "Error", message) + + def show_info_dialog(self, message: str): + QMessageBox.information(self.window, "Information", message) + + def create_button(self, callback: Callable, app: Optional[Dict[str, Any]] = None, + label: Optional[str] = None, condition: Optional[bool] = None): + button = QPushButton(label if label else "") + if callback: + if app: + button.clicked.connect(lambda: callback(app)) + else: + button.clicked.connect(callback) + if condition is not None: + button.setEnabled(condition) + return button + + def create_search_entry(self): + return QLineEdit() + + def create_dropdown(self, items: List[str]): + combo = QComboBox() + combo.addItems(items) + return combo + + def create_switch(self, label: str, active: bool = False): + switch = QSwitch() + switch.setChecked(active) + return switch + + def create_label(self, text: str): + return QLabel(text) + + def create_image(self, path: str, size: int): + pixmap = QPixmap(path) + pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + label = QLabel() + label.setPixmap(pixmap) + return label + + def create_scrollable_container(self): + scroll = QScrollArea() + scroll.setWidgetResizable(True) + return scroll + + def create_box(self, orientation: str = "vertical"): + widget = QWidget() + if orientation == "vertical": + layout = QVBoxLayout(widget) + else: + layout = QHBoxLayout(widget) + widget.setLayout(layout) + return widget + + def create_list_box(self): + return QListWidget() + + def run(self): + self.window.show() + # Note: The Qt event loop is typically started in the main application \ No newline at end of file