diff --git a/libflatpak_query.py b/libflatpak_query.py index 9efa5d2..25d590d 100755 --- a/libflatpak_query.py +++ b/libflatpak_query.py @@ -8,6 +8,11 @@ from pathlib import Path import logging from enum import IntEnum import argparse +import urllib.parse +import requests +import tempfile +import os +import sys # Set up logging logging.basicConfig(level=logging.INFO) @@ -304,6 +309,140 @@ class AppstreamSearcher: return updates +def reposearcher(): + searcher = AppstreamSearcher() + searcher.add_installation(Flatpak.Installation.new_system()) + return searcher + +def repotoggle(repo, bool=True): + """ + Enable or disable a Flatpak repository + + Args: + repo (str): Name of the repository to toggle + enable (bool): True to enable, False to disable + + Returns: + tuple: (success, error_message) + """ + installation = Flatpak.Installation.new_system() + + try: + remote = installation.get_remote_by_name(repo) + if not remote: + return False, f"Repository '{repo}' not found." + + remote.set_disabled(not bool) + + # Modify the remote's disabled status + success = installation.modify_remote( + remote, + None + ) + if success: + if bool: + message = f"Successfully enabled {repo}." + else: + message = f"Successfully disabled {repo}." + return True, message + + except GLib.GError as e: + return False, f"Failed to toggle repository: {str(e)}." + +def repolist(): + installation = Flatpak.Installation.new_system() + repos = installation.list_remotes() + return repos + +def repodelete(repo): + installation = Flatpak.Installation.new_system() + installation.remove_remote(repo) + +def repoadd(repofile): + """Add a new repository using a .flatpakrepo file""" + # Get existing repositories + installation = Flatpak.Installation.new_system() + existing_repos = installation.list_remotes() + + if not repofile.endswith('.flatpakrepo'): + return False, "Repository file path or URL must end with .flatpakrepo extension." + if repofile_is_url(repofile): + try: + local_path = download_repo(repofile) + repofile = local_path + except: + return False, f"Repository file '{repofile}' could not be downloaded." + if not os.path.exists(repofile): + return False, f"Repository file '{repofile}' does not exist." + + # Get repository title from file name + title = os.path.basename(repofile).replace('.flatpakrepo', '') + + # Check for duplicate title (case insensitive) + existing_titles = [repo.get_name().casefold() for repo in existing_repos] + + if title.casefold() in existing_titles: + return False, "A repository with this title already exists." + + # Read the repository file + try: + with open(repofile, 'rb') as f: + repo_data = f.read() + except IOError as e: + return False, f"Failed to read repository file: {str(e)}" + + # Convert the data to GLib.Bytes + repo_bytes = GLib.Bytes.new(repo_data) + + # Create a new remote from the repository file + try: + remote = Flatpak.Remote.new_from_file(title, repo_bytes) + + # Get URLs and normalize them by removing trailing slashes + new_url = remote.get_url().rstrip('/') + existing_urls = [repo.get_url().rstrip('/') for repo in existing_repos] + + # Check if URL already exists + if new_url in existing_urls: + return False, f"A repository with URL '{new_url}' already exists." + + installation.add_remote(remote, True, None) + except GLib.GError as e: + return False, f"Failed to add repository: {str(e)}" + return True, None + +def repofile_is_url(string): + """Check if a string is a valid URL""" + try: + result = urllib.parse.urlparse(string) + return all([result.scheme, result.netloc]) + except: + return False + +def download_repo(url): + """Download a repository file from URL to /tmp/""" + try: + # Create a deterministic filename based on the URL + url_path = urllib.parse.urlparse(url).path + filename = os.path.basename(url_path) or 'repo' + tmp_path = Path(tempfile.gettempdir()) / f"{filename}.flatpakrepo" + + # Download the file + with requests.get(url, stream=True) as response: + response.raise_for_status() + + # Write the file in chunks, overwriting if it exists + with open(tmp_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + return str(tmp_path) + except requests.RequestException as e: + raise argparse.ArgumentTypeError(f"Failed to download repository file: {str(e)}") + except IOError as e: + raise argparse.ArgumentTypeError(f"Failed to save repository file: {str(e)}") + + def main(): """Main function demonstrating Flatpak information retrieval""" @@ -316,6 +455,15 @@ def main(): help='List all installed Flatpak applications') parser.add_argument('--check-updates', action='store_true', help='Check for available updates') + parser.add_argument('--list-repos', action='store_true', + help='List all configured Flatpak repositories') + parser.add_argument('--add-repo', type=str, metavar='REPO_FILE', + help='Add a new repository from a .flatpakrepo file') + parser.add_argument('--remove-repo', type=str, metavar='REPO_NAME', + help='Remove a Flatpak repository') + parser.add_argument('--toggle-repo', type=str, nargs=2, + metavar=('REPO_NAME', 'ENABLE/DISABLE'), + help='Enable or disable a repository') args = parser.parse_args() app_id = args.id @@ -323,12 +471,41 @@ def main(): list_all = args.list_all show_categories = args.categories - # Create AppstreamSearcher instance - searcher = AppstreamSearcher() + # Repository management operations + if args.toggle_repo: + repo_name, enable_str = args.toggle_repo + if enable_str.lower() not in ['true', 'false', 'enable', 'disable']: + print("Invalid enable/disable value. Use 'true/false' or 'enable/disable'") + sys.exit(1) - # Add installations - installation = Flatpak.Installation.new_system(None) - searcher.add_installation(installation) + enable = enable_str.lower() in ['true', 'enable'] + success, message = repotoggle(repo_name, enable) + print(message) + sys.exit(0 if success else 1) + + if args.list_repos: + repos = repolist() + print("\nConfigured Repositories:") + for repo in repos: + print(f"- {repo.get_name()} ({repo.get_url()})") + return + + if args.add_repo: + success, error_message = repoadd(args.add_repo) + if error_message: + print(error_message) + sys.exit(1) + else: + print(f"\nRepository added successfully: {args.add_repo}") + return + + if args.remove_repo: + repodelete(args.remove_repo) + print(f"\nRepository removed successfully: {args.remove_repo}") + return + + # Create AppstreamSearcher instance + searcher = reposearcher() if args.list_installed: installed_apps = searcher.get_installed_apps() diff --git a/main.py b/main.py index 1786144..38ff58e 100755 --- a/main.py +++ b/main.py @@ -4,12 +4,10 @@ import gi gi.require_version("Gtk", "3.0") gi.require_version("GLib", "2.0") gi.require_version("Flatpak", "1.0") -from gi.repository import Gtk, Gio, Gdk -import sqlite3 +from gi.repository import Gtk, Gio, Gdk, GLib import requests from urllib.parse import quote_plus import libflatpak_query -from libflatpak_query import AppstreamSearcher, Flatpak import json import os import time @@ -70,20 +68,38 @@ class MainWindow(Gtk.Window): padding: 12px; color: white; } - + .repo-list-header { + font-size: 18px; + padding: 5px; + color: white; + } + .app-list-header { + font-size: 18px; + color: white; + padding-top: 4px; + padding-bottom: 4px; + } + .app-list-summary { + padding-top: 2px; + padding-bottom: 2px; + } + .app-page-header { + font-size: 24px; + font-weight: bold; + padding: 12px; + color: white; + } .dark-header { background-color: #333333; padding: 6px; margin: 0; } - .dark-category-button { border: 0px; padding: 6px; margin: 0; background: none; } - .dark-category-button-active { background-color: #18A3FF; color: white; @@ -102,6 +118,28 @@ class MainWindow(Gtk.Window): padding: 6px; margin: 0; } + .repo-item { + padding: 6px; + margin: 2px; + border-bottom: 1px solid #eee; + } + .repo-delete-button { + background-color: #ff4444; + color: white; + border: none; + padding: 6px; + margin-left: 6px; + } + .search-entry { + padding: 5px; + border-radius: 4px; + border: 1px solid #ccc; + } + + .search-entry:focus { + border-color: #18A3FF; + box-shadow: 0 0 0 2px rgba(24, 163, 255, 0.2); + } """) # Add CSS provider to the default screen @@ -113,6 +151,7 @@ class MainWindow(Gtk.Window): # Create main layout self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.add(self.main_box) # Create panels @@ -125,15 +164,15 @@ class MainWindow(Gtk.Window): def populate_repo_dropdown(self): # Get list of repositories - installation = Flatpak.Installation.new_system() - repos = installation.list_remotes() + libflatpak_query.repolist() + repos = libflatpak_query.repolist() # Clear existing items self.repo_dropdown.remove_all() # Add repository names for repo in repos: - self.repo_dropdown.append_text(repo.get_remote_name()) + self.repo_dropdown.append_text(repo.get_name()) # Connect selection changed signal self.repo_dropdown.connect("changed", self.on_repo_selected) @@ -172,6 +211,12 @@ class MainWindow(Gtk.Window): def refresh_data(self): + # make sure to reset these to empty before refreshing. + self.category_results = [] # Initialize empty list + self.collection_results = [] # Initialize empty list + self.installed_results = [] # Initialize empty list + self.updates_results = [] # Initialize empty list + total_categories = sum(len(categories) for categories in self.category_groups.values()) current_category = 0 msg = "Fetching metadata, please wait..." @@ -197,8 +242,7 @@ class MainWindow(Gtk.Window): dialog.show_all() # Search for each app in local repositories - searcher = AppstreamSearcher() - searcher.add_installation(Flatpak.Installation.new_system()) + searcher = libflatpak_query.reposearcher() json_path = "collections_data.json" search_result = [] @@ -294,18 +338,59 @@ class MainWindow(Gtk.Window): dialog.destroy() def create_panels(self): - # Create left panel with grouped categories - self.create_grouped_category_panel("Categories", self.category_groups) + # 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 right panel self.right_panel = self.create_applications_panel("Applications") + # Create left panel with grouped categories + self.left_panel = self.create_grouped_category_panel("Categories", self.category_groups) + + # Pack the panels with proper expansion + self.main_box.pack_end(self.right_panel, True, True, 0) # Right panel expands both ways + self.main_box.pack_start(self.left_panel, False, False, 0) # Left panel doesn't expand + def create_grouped_category_panel(self, title, groups): + + # Create container for categories + panel_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + panel_container.set_spacing(6) + panel_container.set_border_width(6) + panel_container.set_size_request(300, -1) # Set fixed width + panel_container.set_hexpand(False) + panel_container.set_vexpand(True) + panel_container.set_halign(Gtk.Align.FILL) # Fill horizontally + panel_container.set_valign(Gtk.Align.FILL) # Align to top + + # Add search bar + self.searchbar = Gtk.SearchBar() # Use self.searchbar instead of searchbar + self.searchbar.set_hexpand(True) + self.searchbar.set_margin_bottom(6) + + # 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('search')) + + # 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) + # Create scrollable area scrolled_window = Gtk.ScrolledWindow() - scrolled_window.set_size_request(300, -1) # Set fixed width - scrolled_window.set_hexpand(False) # Don't expand horizontally + 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) @@ -313,6 +398,8 @@ class MainWindow(Gtk.Window): 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 = {} @@ -369,7 +456,159 @@ class MainWindow(Gtk.Window): scrolled_window.add(container) # Pack the scrolled window directly into main box - self.main_box.pack_start(scrolled_window, False, False, 0) + panel_container.pack_start(self.searchbar, False, False, 0) + panel_container.pack_start(scrolled_window, True, True, 0) + + + self.searchbar.set_search_mode(True) + return panel_container + #self.searchbar.show_all() + + def on_search_changed(self, searchentry): + """Handle search text changes""" + search_term = searchentry.get_text().lower() + if not search_term: + # Reset to showing all categories when search is empty + self.show_category_apps(self.current_category) + return + + # Combine all searchable fields + searchable_items = [] + for app in self.all_apps: + details = app.get_details() + searchable_items.append({ + 'text': f"{details['name']} {details['description']} {details['categories']}".lower(), + 'app': app + }) + + # Filter results + filtered_apps = [item['app'] for item in searchable_items + if search_term in item['text']] + + # Show search results + self.show_search_results(filtered_apps) + + def on_search_activate(self, searchentry): + """Handle Enter key press in search""" + self.on_search_changed(searchentry) + + def show_search_results(self, apps): + """Display search results in the right panel""" + # Clear existing content + for child in self.right_container.get_children(): + child.destroy() + + # Display each application + for app in apps: + details = app.get_details() + is_installed = details['id'] in installed_package_ids + is_updatable = details['id'] in updatable_package_ids + + # Create application container + app_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + app_container.set_spacing(12) + app_container.set_margin_top(6) + app_container.set_margin_bottom(6) + + # Add icon placeholder + icon_box = Gtk.Box() + icon_box.set_size_request(148, -1) + + # Create and add the icon + icon = Gtk.Image.new_from_file(f"{details['icon_path_64']}/{details['icon_filename']}") + icon.set_size_request(48, 48) + icon_box.pack_start(icon, True, True, 0) + + # Create right side layout for text + right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + right_box.set_spacing(4) + right_box.set_hexpand(True) + + # Add title + title_label = Gtk.Label(label=details['name']) + title_label.get_style_context().add_class("app-list-header") + title_label.set_halign(Gtk.Align.START) + title_label.set_yalign(0.5) # Use yalign instead of valign + title_label.set_hexpand(True) + + # Add summary + desc_label = Gtk.Label(label=details['summary']) + desc_label.set_halign(Gtk.Align.START) + desc_label.set_yalign(0.5) # Use yalign instead of valign + desc_label.set_hexpand(True) + desc_label.set_line_wrap(True) + desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD) + desc_label.get_style_context().add_class("dim-label") + desc_label.get_style_context().add_class("app-list-summary") + + # Create buttons box + buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + buttons_box.set_spacing(6) + buttons_box.set_margin_top(4) + buttons_box.set_halign(Gtk.Align.END) + + # Install/Remove button + if is_installed: + button = self.create_button( + self.on_remove_clicked, + app, + None, + condition=lambda x: True + ) + remove_icon = Gio.Icon.new_for_string('list-remove') + button.set_image(Gtk.Image.new_from_gicon(remove_icon, Gtk.IconSize.BUTTON)) + button.get_style_context().add_class("dark-remove-button") + else: + button = self.create_button( + self.on_install_clicked, + app, + None, + condition=lambda x: True + ) + install_icon = Gio.Icon.new_for_string('list-add') + button.set_image(Gtk.Image.new_from_gicon(install_icon, Gtk.IconSize.BUTTON)) + button.get_style_context().add_class("dark-install-button") + buttons_box.pack_end(button, False, False, 0) + + # Add Update button if available + if is_updatable: + update_button = self.create_button( + self.on_update_clicked, + app, + None, + condition=lambda x: True + ) + update_icon = Gio.Icon.new_for_string('synchronize') + update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON)) + update_button.get_style_context().add_class("dark-install-button") + buttons_box.pack_end(update_button, False, False, 0) + + # Details button + details_btn = self.create_button( + self.on_details_clicked, + app, + None + ) + details_icon = Gio.Icon.new_for_string('question') + details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.BUTTON)) + details_btn.get_style_context().add_class("dark-install-button") + buttons_box.pack_end(details_btn, False, False, 0) + + # Add widgets to right box + right_box.pack_start(title_label, False, False, 0) + right_box.pack_start(desc_label, False, False, 0) + right_box.pack_start(buttons_box, False, True, 0) + + # Add separator + separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + + # Add to container + app_container.pack_start(icon_box, False, False, 0) + app_container.pack_start(right_box, True, True, 0) + self.right_container.pack_start(app_container, False, False, 0) + self.right_container.pack_start(separator, False, False, 0) + + self.right_container.show_all() def on_category_clicked(self, category, group): # Remove active state from all widgets in all groups @@ -423,8 +662,8 @@ class MainWindow(Gtk.Window): scrolled_window.add(self.right_container) self.right_panel.pack_start(scrolled_window, True, True, 0) - self.main_box.pack_end(self.right_panel, True, True, 0) - return self.right_container + + return self.right_panel def check_internet(self): """Check if internet connection is available.""" @@ -471,7 +710,6 @@ class MainWindow(Gtk.Window): def save_collections_data(self, filename='collections_data.json'): """Save all collected collections data to a JSON file.""" if not hasattr(self, 'collections_db') or not self.collections_db: - print("No collections data available to save") return try: @@ -481,9 +719,11 @@ class MainWindow(Gtk.Window): print(f"Error saving collections data: {str(e)}") # Create and connect buttons - def create_button(self, label, callback, app, condition=None): + def create_button(self, callback, app, label=None, condition=None): """Create a button with optional visibility condition""" - button = Gtk.Button(label=label) + 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): @@ -550,6 +790,125 @@ class MainWindow(Gtk.Window): 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 = libflatpak_query.repolist() + + # 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_icon = Gio.Icon.new_for_string('list-add') + add_repo_button.set_image(Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.BUTTON)) + add_repo_button.get_style_context().add_class("dark-install-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.get_style_context().add_class("dark-install-button") + 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.get_style_context().add_class("dark-install-button") + 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 + checkbox = Gtk.CheckButton(label=repo.get_name()) + 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_icon = Gio.Icon.new_for_string('list-remove') + 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 + # Display each application for app in apps: details = app.get_details() @@ -564,11 +923,11 @@ class MainWindow(Gtk.Window): # Add icon placeholder icon_box = Gtk.Box() - icon_box.set_size_request(148, -1) + icon_box.set_size_request(94, -1) # Create and add the icon - icon = Gtk.Image.new_from_file(f"{details['icon_path_64']}/{details['icon_filename']}") - icon.set_size_request(48, 48) + icon = Gtk.Image.new_from_file(f"{details['icon_path_128']}/{details['icon_filename']}") + icon.set_size_request(74, 74) icon_box.pack_start(icon, True, True, 0) # Create right side layout for text @@ -578,13 +937,15 @@ class MainWindow(Gtk.Window): # Add title title_label = Gtk.Label(label=details['name']) - title_label.get_style_context().add_class("title-1") + title_label.get_style_context().add_class("app-list-header") title_label.set_halign(Gtk.Align.START) + title_label.set_valign(Gtk.Align.CENTER) title_label.set_hexpand(True) # Add summary desc_label = Gtk.Label(label=details['summary']) desc_label.set_halign(Gtk.Align.START) + desc_label.set_valign(Gtk.Align.CENTER) desc_label.set_hexpand(True) desc_label.set_line_wrap(True) desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD) @@ -599,49 +960,61 @@ class MainWindow(Gtk.Window): # Install/Remove button if is_installed: button = self.create_button( - "Remove", self.on_remove_clicked, app, + None, condition=lambda x: True ) + remove_icon = Gio.Icon.new_for_string('list-remove') + button.set_image(Gtk.Image.new_from_gicon(remove_icon, Gtk.IconSize.BUTTON)) button.get_style_context().add_class("dark-remove-button") else: button = self.create_button( - "Install", self.on_install_clicked, app, + None, condition=lambda x: True ) + install_icon = Gio.Icon.new_for_string('list-add') + button.set_image(Gtk.Image.new_from_gicon(install_icon, Gtk.IconSize.BUTTON)) button.get_style_context().add_class("dark-install-button") buttons_box.pack_end(button, False, False, 0) # Add Update button if available if is_updatable: update_button = self.create_button( - "Update", self.on_update_clicked, app, + None, condition=lambda x: True ) + update_icon = Gio.Icon.new_for_string('synchronize') + update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON)) update_button.get_style_context().add_class("dark-install-button") buttons_box.pack_end(update_button, False, False, 0) # Details button details_btn = self.create_button( - "Details", self.on_details_clicked, - app + app, + None ) + details_icon = Gio.Icon.new_for_string('question') + details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.BUTTON)) + details_btn.get_style_context().add_class("dark-install-button") buttons_box.pack_end(details_btn, False, False, 0) # Donate button with condition donate_btn = self.create_button( - "Donate", self.on_donate_clicked, app, - lambda x: x.get_details().get('urls', {}).get('donation', '') + None, + condition=lambda x: x.get_details().get('urls', {}).get('donation', '') ) if donate_btn: + donate_icon = Gio.Icon.new_for_string('donate') + donate_btn.set_image(Gtk.Image.new_from_gicon(donate_icon, Gtk.IconSize.BUTTON)) + donate_btn.get_style_context().add_class("dark-install-button") buttons_box.pack_end(donate_btn, False, False, 0) # Add widgets to right box @@ -701,6 +1074,184 @@ class MainWindow(Gtk.Window): 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 = libflatpak_query.repotoggle(repo.get_name(), True) + message_type = Gtk.MessageType.INFO + if success: + self.refresh_data() + 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 = libflatpak_query.repotoggle(repo.get_name(), False) + message_type = Gtk.MessageType.INFO + if success: + self.refresh_data() + 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: + libflatpak_query.repodelete(repo.get_name()) + self.refresh_data() + 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 = libflatpak_query.repoadd("https://dl.flathub.org/repo/flathub.flatpakrepo") + 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_data() + 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 = libflatpak_query.repoadd("https://dl.flathub.org/beta-repo/flathub-beta.flatpakrepo") + 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_data() + self.show_category_apps('repositories') + + def on_add_repo_button_clicked(self, button): + """Handle the Add Repository button click""" + # Create file chooser dialog + 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) + + # Run the dialog + response = dialog.run() + repo_file_path = dialog.get_filename() + dialog.destroy() + + if response == Gtk.ResponseType.OK and repo_file_path: + # Add the repository + success, error_message = libflatpak_query.repoadd(repo_file_path) + 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_data() + 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']: