#!/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 json import threading import subprocess from pathlib import Path from html.parser import HTMLParser import requests import os import pwd import atexit from datetime import datetime APP_ID = "com.flatpost.flatpostapp" ICON_NAME = APP_ID settings = Gtk.Settings.get_default() settings.set_property("gtk-theme-name", "adw-gtk3-dark") # Replace with the exact theme name if different settings.set_property("gtk-application-prefer-dark-theme", True) class MainWindow(Gtk.ApplicationWindow): 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) 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/64x64/apps/com.flatpost.flatpostapp.svg" 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) Gtk.Window.set_default_icon(self.pixbuf64) # Set the icon list self.set_icon_list([self.pixbuf16, self.pixbuf24, self.pixbuf32, self.pixbuf48, self.pixbuf64]) except Exception as e: print(f"ERROR loading icon: {str(e)}") # Store search results as an instance variable self.all_apps = [] self.current_component_type = None self.category_results = [] # Initialize empty list 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.category_groups = { 'system': { 'installed': 'Installed', 'updates': 'Updates', 'repositories': 'Repositories' }, 'collections': { 'trending': 'Trending', 'popular': 'Popular', 'recently-added': 'New', 'recently-updated': 'Updated' }, 'categories': { 'office': 'Productivity', 'graphics': 'Graphics & Photography', 'audiovideo': 'Audio & Video', 'education': 'Education', 'network': 'Networking', 'game': 'Games', 'development': 'Developer Tools', 'science': 'Science', 'system': 'System', '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' } } # Initialize mouse cursor test # display = Gdk.Display.get_default() # cursor = Gdk.Cursor.new_from_name(display, "pointer"); # Add CSS provider for custom styling css_provider = Gtk.CssProvider() css_provider.load_from_data(""" .large-title { font-weight: 800 } .top-bar { margin: 0px; padding: 0px; border: 0px; background-color: @sidebar_backdrop_color; border-bottom: 1px solid mix(currentColor,@window_bg_color,0.86); } dialog button label { padding: 8px 0; } button.titlebutton { min-height: 24px; min-width: 24px; } # 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-panel { padding: 0 6px; margin: 12px; border-radius: 4px; background-color: @sidebar_backdrop_color; border: 1px solid mix(currentColor,@window_bg_color,0.86); } .category-group-header { margin: 0; font-weight: bold; } .category-button { border: 0px; padding: 12px 8px; margin: 0; background: none; transition: margin-left 0.2s cubic-bezier(0.040, 0.455, 0.215, 0.995), padding 0.2s cubic-bezier(0.040, 0.455, 0.215, 0.995); } .category-button.active { padding: 12px 14px; margin-left: 4px; background-color: mix(currentColor,@window_bg_color,0.9); border-radius: 4px; font-weight: bold; } .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 { margin: 2px; background-color: @sidebar_backdrop_color; border-radius: 4px; } .subcategory-button { border: 0px; padding: 10px; margin: 0; background: none; transition: all 0.2s cubic-bezier(0.040, 0.455, 0.215, 0.995), font-weight 0s; } .subcategory-button.active { font-weight: bold; background-color: @window_bg_color; padding: 10px 24px; border-radius: 4px; } .subcategories-scroll { border: none; background-color: transparent; min-height: 32px; } .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-panel { margin: 12px; } .app-panel-header { padding: 16px 8px; } .app-list { border: 0px; background: none; } .app-list-item { padding: 24px 32px; margin: 5px 0; border-radius: 16px; background-color: alpha(@card_bg_color,0.6); border: 1px solid mix(currentColor,@window_bg_color,0.9); transition: background-color 0.2s ease-out; } .app-list-item.hover-event { background-color: alpha(@card_bg_color,0.2); box-shadow: 2px 2px 6px rgba(0,0,0,0.05); } .app-list-header { font-size: 18px; font-weight: bold; } .app-list-developer, .app-list-misc { font-size: 13px; } .app-list-summary { padding-top: 6px; padding-bottom: 2px; } .app-page-header { font-size: 24px; font-weight: bold; padding: 12px; } .app-repo-label { font-size: 0.8em; color: @search_bg_color } .app-type-label { font-size: 0.8em; } .app-panel-nothing { transition: opacity 2s ease; } .updates_available_bar { background-color: @accent_bg_color; padding: 4px; border-radius: 4px; } .updates_available_bar_label { color: @accent_fg_color; } .details-window { border: 0px; margin: 0px; background: none; } .details-content { margin: 0px 32px; } .details-gallery { background-color: @shade_color; padding: 16px 0px; } details-gallery-screenshot { border-radius: 4px; } .details-gallery-arrow { transition: all 0.2s cubic-bezier(0.040, 0.455, 0.215, 0.995); padding: 16px; border-radius: 50%; } .details-gallery-arrow.hover-event { background-color: mix(@window_bg_color,currentColor,0.1); color: @accent_bg_color; } .details-gallery-bullet { color: @accent_fg_color; font-size: 6px; padding: 6px; min-width: 13px; min-height: 12.5px; border-radius: 50%; transition: all 0.2s cubic-bezier(0.040, 0.455, 0.215, 0.995); opacity: 0.3; } .details-gallery-bullet.hover-event { background-color: mix(@window_bg_color,currentColor,0.1); opacity: 1; } .details-gallery-bullet.active { color: @accent_bg_color; opacity: 1; font-size: 9px; } .details-textview text { background-color: @window_bg_color; border-width: 0; border-radius: 4px; } .url-list { background-color: @card_bg_color; border-radius: 4px; border: 1px solid mix(currentColor,@window_bg_color,0.86); } .url-list-item { padding: 10px 16px; transition: all 0.2s cubic-bezier(0.040, 0.455, 0.215, 0.995), background-color 0.2s ease-out; } .url-list-item image { color: mix(currentColor,@window_bg_color,0.3); } .url-list-item.hover-event { background-color: @sidebar_backdrop_color; } .url-list-item-title { font-size: 14px; font-weight: 500; } .url-list-item-url { font-size: 13px; } .url { color: @accent_bg_color; } .url.hover-event { text-decoration-line: underline; } .permissions-window { border: 0px; margin: 0px; padding: 20px; background: none; } .permissions-header-label { font-weight: bold; font-size: 24px; } .permissions-row { padding: 6px; } .permissions-item-label { font-size: 14px; } .permissions-item-summary { font-size: 12px; color: #8c8c8c; } .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.icon_label { padding: 8px 14px; } .app-action-button:hover { box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .app-url-label { color: @accent_bg_color; 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) # 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) parent_system_box.set_valign(Gtk.Align.CENTER) # 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 enter_hover_event(self, content, position=None, dots=None): # Use this function with "enter-notify-event" signals to handle hover state if content.get_style_context().has_class("details-gallery-arrow") == True and content.get_style_context().has_class("dim-label") == True: return elif content.get_style_context().has_class("details-gallery-bullet") == True: for i, dot in enumerate(dots): # Get the bullet label from the event box bullet_selected = dot.get_children()[0] if i == position: bullet_selected.get_style_context().add_class("hover-event") return # if content.get_style_context().has_class("url") == True: # Gdk.Window.set_cursor(content, cursor) # I would like to create a way to change cursor for URLs, but idk how to use GDK without recoding everything content.get_style_context().add_class("hover-event") def leave_hover_event(self, content, position=None, dots=None): # Use this function with "leave-notify-event" signals to handle hover state # if content.get_style_context().has_class("url") == True: # print("Leave: class detected") if content.get_style_context().has_class("details-gallery-bullet") == True: for i, dot in enumerate(dots): # Get the bullet label from the event box bullet_selected = dot.get_children()[0] if i == position: bullet_selected.get_style_context().remove_class("hover-event") return content.get_style_context().remove_class("hover-event") 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=0) 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.1.1") 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.get_style_context().add_class("url") 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")) license_event_box.connect("enter-notify-event", lambda w, e: self.enter_hover_event(license_url)); license_event_box.connect("leave-notify-event", lambda w, e: self.leave_hover_event(license_url)); 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.get_style_context().add_class("url") 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")) issue_event_box.connect("enter-notify-event", lambda w, e: self.enter_hover_event(issue_url)); # These connect signals handles hover issue_event_box.connect("leave-notify-event", lambda w, e: self.leave_hover_event(issue_url)); # 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() subprocess.run(["xhost", "si:localuser:root"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) 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) progress_bar.set_margin_start(32) progress_bar.set_margin_end(32) dialog.vbox.pack_start(progress_bar, True, True, 32) # Padding necessary dialog.vbox.set_valign(Gtk.Align.FILL) # Make it use all window space # 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_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 panel_container.get_style_context().add_class("category-panel") # 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(2) 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 = {} first_category_group = False # 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.capitalize()) category_group_icon = "" # May be better as an array ? if "system" in group_name: category_group_icon = 'applications-system-symbolic' elif "collections" in group_name: category_group_icon = 'system-file-manager-symbolic' elif "categories" in group_name: category_group_icon = 'application-x-addon-symbolic' else: category_group_icon = 'user-bookmarks-symbolic' group_header_icon = Gtk.Image.new_from_gicon(Gio.Icon.new_for_string(category_group_icon), 2) group_header_icon.set_size_request(-1, 32) group_header_icon.set_valign(Gtk.Align.CENTER) group_header.get_style_context().add_class("title-3") group_header.set_halign(Gtk.Align.START) # Add the label to the box header_box.pack_start(group_header_icon, False, False, 7) header_box.pack_start(group_header, False, False, 0) # Add the box to the container if first_category_group == False: first_category_group = True else: container.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 8) container.pack_start(header_box, False, False, 4) # 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.get_style_context().add_class("category-box") # 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({ 'app': app, 'id': details['id'].lower(), 'text': f"{details['name']} {details['description']} {details['categories']}".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(): if safe_val == "Updates" and len(self.updates_results) != 0: markup_updates = f"{safe_val} ({len(self.updates_results)})" label.set_markup(markup_updates) else: label.set_label(val) label.get_style_context().remove_class("active") 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 display_title in label.get_text(): safe_title = GLib.markup_escape_text(display_title) markup_selected = f" " if "Updates" in display_title and len(self.updates_results) != 0: markup_updates = f" ({len(self.updates_results)})" label.set_markup(safe_title+markup_updates+markup_selected) else: label.set_markup(safe_title+markup_selected) label.get_style_context().add_class("active") 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) try: if self.details_window: # box_outer = self.details_window.get_children(self.details_window.get_child()) # print(box_outer) self.details_window.destroy() # Temporary solution for action buttons not updating on details window except: pass 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 self.right_panel.get_style_context().add_class("app-panel") # Add category header self.category_header = Gtk.Label(label="") self.category_header.get_style_context().add_class("title-1") self.category_header.get_style_context().add_class("app-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-list") 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) self.updates_available_bar.set_valign(Gtk.Align.CENTER) buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) buttons_box.set_spacing(8) buttons_box.set_halign(Gtk.Align.END) buttons_box.set_valign(Gtk.Align.CENTER) update_all_button = Gtk.Button() update_all_button.set_label(" "+"Update all") 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('software-update-available-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) update_all_button.set_always_show_image(True) buttons_box.pack_end(update_all_button, False, False, 0) # Create left label left_label = Gtk.Label(label="New updates are available") left_label.set_halign(Gtk.Align.CENTER) left_label.get_style_context().add_class("updates_available_bar_label") self.updates_available_bar.pack_end(buttons_box, False, False, 0) self.updates_available_bar.pack_start(left_label, False, False, 8) 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, size=64, 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], size, 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( size, size, # 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) if apps_by_id == {}: self._create_nothing_to_show() def _create_nothing_to_show(self): """Create and add a row for a single application.""" container = self._create_app_container() content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True) nothing_icon = Gtk.Image.new_from_gicon(Gio.Icon.new_for_string("preferences-desktop-screensaver-symbolic"), 6) nothing_icon.set_pixel_size(128) nothing_title = Gtk.Label(label="It's empty here...", halign=Gtk.Align.CENTER) nothing_title.get_style_context().add_class('title-2') nothing_label = Gtk.Label(label="We couldn't find any applications corresponding to your demand.") nothing_label.set_halign(Gtk.Align.CENTER) nothing_label.get_style_context().add_class('dim-label') content_box.pack_start(nothing_icon, False, False, 16) content_box.pack_start(nothing_title, False, False, 0) content_box.pack_start(nothing_label, False, False, 0) content_box.set_opacity(0.5) content_box.set_margin_bottom(64) content_box.get_style_context().add_class('app-panel-nothing') event_box = Gtk.EventBox() event_box.add(content_box) container.add(event_box) 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 _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() content_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) self._setup_icon(content_box, details) self._setup_text_layout(content_box, details, app_data['repos']) self._setup_buttons(content_box, status, app) content_box.get_style_context().add_class('app-list-item') event_box = Gtk.EventBox() event_box.connect("button-release-event", lambda w, e: self.click_event(app, content_box)) event_box.connect("enter-notify-event", lambda w, e: self.enter_hover_event(content_box)); # These connect signals handles hover event_box.connect("leave-notify-event", lambda w, e: self.leave_hover_event(content_box)); event_box.add(content_box) container.add(event_box) 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 click_event(self, app=None, content=None): if content.get_style_context().has_class("hover-event") == True: if content.get_style_context().has_class("app-list-item") == True: self.on_details_clicked(Gtk.Button, app) 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) 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, False, 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_hexpand(True) right_box.pack_start(title_label, False, False, 0) #Developer developer_label = Gtk.Label(label=f"{details['developer']}") developer_label.set_halign(Gtk.Align.START) developer_label.set_hexpand(True) developer_label.set_line_wrap(True) developer_label.set_line_wrap_mode(Gtk.WrapMode.WORD) developer_label.get_style_context().add_class("dim-label") developer_label.get_style_context().add_class("app-list-developer") right_box.pack_start(developer_label, False, False, 0) # Description desc_label = Gtk.Label(label=details['summary']) desc_label.set_halign(Gtk.Align.START) desc_label.set_yalign(1) 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("app-list-summary") right_box.pack_start(desc_label, False, False, 0) # Kind label (disabled) 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_box.get_style_context().add_class("dim-label") kind_box.get_style_context().add_class("app-list-misc") 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) # Kind & Repositories Label (in one line) 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_box.get_style_context().add_class("dim-label") repo_box.get_style_context().add_class("app-list-misc") repo_list_label = Gtk.Label(label=f"Type: {details['kind']} • 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) container.pack_start(right_box, True, True, 0) def _setup_buttons(self, container, status, app, panel=None): """Set up action buttons for the application.""" buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) buttons_box.set_spacing(8) buttons_box.set_halign(Gtk.Align.END) buttons_box.set_valign(Gtk.Align.CENTER) # Center vertically update_button = False # Add install/remove buttons separately if status['is_updatable'] and self.current_page != "installed": self._add_action_button( buttons_box, True, app, self.on_update_clicked, 'software-update-available-symbolic', None, "Update" ) update_button = True elif status['is_installed']: self._add_action_button( buttons_box, True, app, self.on_remove_clicked, "list-remove-symbolic", None, "Uninstall" ) else: self._add_action_button( buttons_box, True, app, self.on_install_clicked, "list-add-symbolic", None, "Install", True ) if status['is_installed']: self._add_action_button( buttons_box, True, app, self.on_app_options_clicked, "applications-system-symbolic", "Manage permissions" ) # if panel != "info": # self._add_action_button( # buttons_box, # True, # app, # self.on_details_clicked, # 'help-about-symbolic', # "Show details" # ) if status['has_donation_url']: if panel == "info": self._add_action_button( buttons_box, True, app, self.on_donate_clicked, 'emote-love-symbolic', None, "Donate" ) else: self._add_action_button( buttons_box, True, app, self.on_donate_clicked, 'emote-love-symbolic', "Donate" ) if status['is_updatable'] and update_button != True: self._add_action_button( buttons_box, True, app, self.on_update_clicked, 'software-update-available-symbolic', "Update" ) container.pack_end(buttons_box, False, False, 0) def _add_action_button(self, parent, visible, app, callback, icon_name, tooltip=None, label_name=None, accent=False): """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) button.set_always_show_image(True) if tooltip: button.set_tooltip_text(tooltip) if label_name: button.set_label(" "+label_name) button.get_style_context().add_class("icon_label") if accent is True: button.get_style_context().add_class("suggested-action") 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, perm_type) 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, perm_type) 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=16) slideshow_box.get_style_context().add_class("details-gallery") slideshow_content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.FILL, spacing=32, border_width=0) self.index_current = 0 # First screenshot is active self.screenshot_loop = True # Option to loop the screenshots instead of having limits on the first and last screenshot. nav_previous_icon = Gtk.Image.new_from_gicon(Gio.Icon.new_for_string("go-previous-symbolic"), 3) nav_previous_icon.set_halign(Gtk.Align.END) nav_previous_icon.set_valign(Gtk.Align.CENTER) nav_previous_icon.get_style_context().add_class("details-gallery-arrow") nav_previous_event_box = Gtk.EventBox() nav_previous_event_box.set_size_request(96,-1) # Arrow size (48px + padding 16px) + The margin you want (here 32px) nav_previous_event_box.connect('button-release-event', lambda w, e: self._switch_screenshot( current_image, screenshots, dots, self.index_current, app_id, nav_previous_icon, nav_next_icon, "previous")) nav_previous_event_box.connect("enter-notify-event", lambda w, e: self.enter_hover_event(nav_previous_icon)); nav_previous_event_box.connect("leave-notify-event", lambda w, e: self.leave_hover_event(nav_previous_icon)); nav_previous_event_box.add(nav_previous_icon) slideshow_content.pack_start(nav_previous_event_box, False, False, 0) # Create main frame for the current screenshot (removed border) main_frame = Gtk.Frame(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) main_frame.set_shadow_type(Gtk.ShadowType.NONE) main_frame.set_size_request(-1,300) # Minimum size (for when images don't load proprely) slideshow_content.pack_start(main_frame, False, False, 0) nav_next_icon = Gtk.Image.new_from_gicon(Gio.Icon.new_for_string("go-next-symbolic"), 3) nav_next_icon.set_halign(Gtk.Align.START) nav_next_icon.set_valign(Gtk.Align.CENTER) nav_next_icon.get_style_context().add_class("details-gallery-arrow") nav_next_event_box = Gtk.EventBox() nav_next_event_box.set_size_request(96,-1) nav_next_event_box.connect('button-release-event', lambda w, e: self._switch_screenshot( current_image, screenshots, dots, self.index_current, app_id, nav_previous_icon, nav_next_icon, "next")) nav_next_event_box.connect("enter-notify-event", lambda w, e: self.enter_hover_event(nav_next_icon)); nav_next_event_box.connect("leave-notify-event", lambda w, e: self.leave_hover_event(nav_next_icon)); nav_next_event_box.add(nav_next_icon) slideshow_content.pack_end(nav_next_event_box, False, False, 0) slideshow_box.pack_start(slideshow_content, False, False, 0) # Create image for current screenshot current_image = Gtk.Image(hexpand=True, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) current_image.get_style_context().add_class("details-gallery-screenshot") main_frame.add(current_image) # Create navigation dots dots = [] if len(screenshots) > 1: # Create box for navigation dots nav_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6, halign=Gtk.Align.CENTER) nav_box.set_border_width(0) # Remove border slideshow_box.pack_start(nav_box, False, True, 0) for i in range(len(screenshots)): # Create new EventBox for each dot event_box = Gtk.EventBox(valign=Gtk.Align.CENTER) event_box.set_border_width(0) # Create bullet using Label bullet = Gtk.Label(label="⬤", justify=Gtk.Justification.CENTER, yalign=0.58) bullet.get_style_context().add_class("details-gallery-bullet") if i == self.index_current: bullet.get_style_context().add_class("active") # 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, nav_previous_icon, nav_next_icon)) event_box.connect("enter-notify-event", lambda w, e, idx=i: self.enter_hover_event(bullet, idx, dots)); event_box.connect("leave-notify-event", lambda w, e, idx=i: self.leave_hover_event(bullet, idx, dots)); # Add event box to nav box nav_box.pack_start(event_box, False, True, 0) # Store the event box dots.append(event_box) # Updates navigation (arrow & dots) self._screenshot_navigation_update(screenshots, nav_previous_icon, nav_next_icon, dots) # 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): pb = GdkPixbuf.Pixbuf.new_from_file_at_size(local_path, -1, 336) # Resizes the screenshot height while preserving aspect ratio image.set_from_pixbuf(pb) else: if fp_turbo.check_internet(): try: if not self.download_screenshot(url, local_path): print("Failed to download screenshot") return pb = GdkPixbuf.Pixbuf.new_from_file_at_size(local_path, -1, 336) # Resizes the screenshot height while preserving aspect ratio image.set_from_pixbuf(pb) except Exception: image.set_from_icon_name('image-missing-symbolic', 6) else: image.set_from_icon_name('image-missing-symbolic', 6) def _switch_screenshot(self, image, screenshots, dots, index_current, app_id, icon_previous, icon_next, direction=None): self.index_current = index_current if direction == "previous": self.index_current -= 1 if direction == "next": self.index_current += 1 if self.screenshot_loop == True: if self.index_current < 0: self.index_current = len(screenshots)-1 if self.index_current > len(screenshots)-1: self.index_current = 0 self.index_current = max(0, min(self.index_current, len(screenshots)-1)) # Security in case the position is not in the list self._screenshot_navigation_update(screenshots, icon_previous, icon_next, dots) # Load the new screenshot self._load_screenshot(image, screenshots[self.index_current], app_id) print("Page",self.index_current,"out of",len(screenshots)-1) def _screenshot_navigation_update(self, screenshots, icon_previous, icon_next, dots): for i, dot in enumerate(dots): # Get the bullet label from the event box bullet = dot.get_children()[0] if i == self.index_current: bullet.get_style_context().add_class("active") else: bullet.get_style_context().remove_class("active") if self.screenshot_loop == False: if self.index_current == 0: icon_previous.get_style_context().add_class("dim-label") icon_previous.get_style_context().remove_class("hover-event") else: icon_previous.get_style_context().remove_class("dim-label") if self.index_current == len(screenshots)-1: icon_next.get_style_context().add_class("dim-label") icon_next.get_style_context().remove_class("hover-event") else: icon_next.get_style_context().remove_class("dim-label") if len(screenshots)-1 == 0: icon_previous.set_opacity(0) icon_next.set_opacity(0) 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, 700) # Set header bar header_bar = Gtk.HeaderBar( title=f"About {details['name']}" ) 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=16) 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_box, False, True, 0) content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) def _create_info_section(self, content_box, details, app): """Create the information section with name, version, and developer.""" info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=28) status = self._get_app_status(app) # Create the icon section of the details window. icon_box = Gtk.Box() icon_box.set_size_request(-1, 128) app_icon = Gio.Icon.new_for_string('package-x-generic-symbolic') icon_widget = self.create_scaled_icon(app_icon, 128, 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']}", 128, is_themed=False ) icon_widget.set_size_request(128, 128) icon_box.pack_start(icon_widget, False, True, 0) # Middle column middle_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, valign=Gtk.Align.CENTER) name_label = Gtk.Label(label=f"{details['name']}") name_label.get_style_context().add_class("title-1") 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"{details['developer']}") developer_label.set_xalign(0) developer_label.get_style_context().add_class("dim-label") middle_column.pack_start(name_label, False, True, 0) middle_column.pack_start(developer_label, False, True, 0) middle_column.pack_start(version_label, False, True, 0) # Right column right_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, valign=Gtk.Align.CENTER) right_column.set_valign(Gtk.Align.CENTER) id_label = Gtk.Label(label=f"ID: {details['id']}") id_label.set_xalign(1) id_label.get_style_context().add_class("dim-label") kind_label = Gtk.Label(label=f"Kind: {details['kind']}") kind_label.set_xalign(1) kind_label.get_style_context().add_class("dim-label") # right_column.pack_start(id_label, False, True, 0) # right_column.pack_start(kind_label, False, True, 0) container = right_column self._setup_buttons(container, status, app, "info") info_box.pack_start(icon_box, 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) def _create_text_section(self, title, text): """Create a text section with title and content.""" box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) if not isinstance(text,str): text = "No description provided.\n" title_label = Gtk.Label(label=f"{title}") title_label.get_style_context().add_class("title-3") title_label.set_xalign(0) box.pack_start(title_label, False, True, 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 == 'li': self.text.append('• ') def handle_endtag(self, tag): if tag == 'p': self.text.append('\n') elif tag == 'li': self.text.append('\n') # Parse the HTML parser = TextExtractor() parser.text.append('\n') # For some reasons, description doesn't appear when there's only one line of paragraph, so I added this as a temporary fix. print(text) parser.feed(text) parsed_text = ''.join(parser.text) # Use [:-1] to remove last line if it's /n print(TextExtractor()) # Add basic HTML styling buffer.set_text(parsed_text) print(parsed_text) text_view.set_pixels_below_lines(3) except Exception as e: # Fallback to plain text if HTML parsing fails buffer.set_text(text) # buffer.set_text(e) 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.""" content_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) content_box.get_style_context().add_class("url-list-item") text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) url_type_icon_name = "" # May be better as an array ? if "donation" in url_type: url_type_icon_name = 'emote-love-symbolic' elif "homepage" in url_type: url_type_icon_name = 'go-home-symbolic' elif "bugtracker" in url_type: url_type_icon_name = 'dialog-warning-symbolic' elif "Flathub Page" in url_type: url_type_icon_name = 'help-about-symbolic' else: url_type_icon_name = 'user-bookmarks-symbolic' url_type_icon = Gtk.Image.new_from_gicon(Gio.Icon.new_for_string(url_type_icon_name), 3) url_type_icon.set_size_request(-1, 28) url_type_icon.set_valign(Gtk.Align.CENTER) title_label = Gtk.Label(label=f"{url_type.capitalize()}", xalign=0) title_label.get_style_context().add_class("url-list-item-title") 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) url_label.get_style_context().add_class("url-list-item-url") url_label.get_style_context().add_class("dim-label") url_open_icon = Gtk.Image.new_from_gicon(Gio.Icon.new_for_string("send-to-symbolic"), 2) url_open_icon.set_size_request(-1, 24) url_open_icon.set_valign(Gtk.Align.CENTER) event_box = Gtk.EventBox() event_box.connect("button-release-event", lambda w, e: Gio.AppInfo.launch_default_for_uri(url)) event_box.connect("enter-notify-event", lambda w, e: self.enter_hover_event(content_box)); # These connect signals handles hover event_box.connect("leave-notify-event", lambda w, e: self.leave_hover_event(content_box)); if url: content_box.pack_start(url_type_icon, False, True, 0) text_box.pack_start(title_label, False, True, 0) text_box.pack_start(url_label, False, True, 0) content_box.pack_start(text_box, False, True, 8) content_box.pack_end(url_open_icon, False, True, 0) event_box.add(content_box) 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) content_main = Gtk.Box(margin_top=32, margin_bottom=16, orientation=Gtk.Orientation.VERTICAL, valign=True) content_main.get_style_context().add_class("details-content") content_info = Gtk.Box(margin_top=16, margin_bottom=32, orientation=Gtk.Orientation.VERTICAL, valign=True) content_info.get_style_context().add_class("details-content") # self._create_icon_section(content_box, details) # Add info section self._create_info_section(content_main, details, app) content_box.pack_start(content_main, False, True, 0) # Add screenshots if len(details['screenshots']) >= 1: # To prevent apps with no screenshot from crashing Flatpost 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(details['summary'], details['description']) content_info.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=0) title_label = Gtk.Label(label=f"Links") title_label.get_style_context().add_class("title-3") title_label.set_xalign(0) for url_type, url in details['urls'].items(): row = self._create_url_section(url_type, url) if url == "": pass else: urls_section.pack_start(row, False, True, 0) urls_section.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) urls_section.remove(Gtk.Separator()) urls_section.pack_start(self._create_url_section("Flathub Page", f"https://flathub.org/apps/details/{details['id']}"), False, True, 0) urls_section.get_style_context().add_class("url-list") # content_box.pack_start(title_label, False, True, 4) content_info.pack_start(urls_section, False, True, 0) content_box.pack_end(content_info, 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') 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: subprocess.run(["xhost", "si:localuser:root"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) 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, ] ) GLib.set_prgname(APP_ID) app = MainWindow(system_mode=system_mode, system_only_mode=system_only_mode) app.connect("destroy", Gtk.main_quit) app.show_all() Gtk.main() def cleanup_xhost(): """Cleanup function to run xhost on exit""" try: subprocess.run(["xhost", "-si:localuser:root"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except Exception as e: logger.error(f"Failed to run xhost cleanup: {e}") if __name__ == "__main__": try: # Your main application code here main() finally: # This ensures cleanup runs even if main() throws an exception cleanup_xhost()