diff --git a/README.md b/README.md index 5a4bc26..15f2660 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ DONE: - Cleanup permissions GUI - Add permissions override viewing inside per-app permissions view. - Add 'update all' functionality. - -TODO: - add about section - General GUI layout/theming improvements + +TODO: - Document fp_turbo functions Usage (Temporary until proper packaging is added): diff --git a/fp_turbo.py b/fp_turbo.py index abc2e80..f77f4d4 100755 --- a/fp_turbo.py +++ b/fp_turbo.py @@ -899,11 +899,16 @@ def install_flatpak(app: AppStreamPackage, repo_name=None, system=False) -> tupl transaction = Flatpak.Transaction.new_for_installation(installation) available_apps = installation.list_remote_refs_sync(repo_name) + match_found = None for available_app in available_apps: - if app.id in available_app.get_name(): + if app.id == available_app.get_name(): + match_found = 1 # Add the install operation transaction.add_install(repo_name, available_app.format_ref(), None) - # Run the transaction + + if not match_found: + return False, f"No available package named {app.id} found in any repositories." + try: transaction.run() except GLib.Error as e: @@ -962,10 +967,14 @@ def remove_flatpak(app: AppStreamPackage, system=False) -> tuple[bool, str]: installed = installation.list_installed_refs(None) # Create a new transaction for removal transaction = Flatpak.Transaction.new_for_installation(installation) + match_found = None for installed_ref in installed: if app.id in installed_ref.get_name(): + match_found = 1 transaction.add_uninstall(installed_ref.format_ref()) - # Run the transaction + + if not match_found: + return False, f"No installed package named {app.id} found." try: transaction.run() except GLib.Error as e: @@ -989,10 +998,14 @@ def update_flatpak(app: AppStreamPackage, system=False) -> tuple[bool, str]: updates = installation.list_installed_refs_for_update(None) # Create a new transaction for removal transaction = Flatpak.Transaction.new_for_installation(installation) - + match_found = None for update in updates: if app.id == update.get_name(): + match_found = 1 transaction.add_update(update.format_ref()) + + if not match_found: + return False, f"No updateable package named {app.id} found." # Run the transaction try: transaction.run() diff --git a/main.py b/main.py index 3c4b4b7..6c802ba 100755 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ from pathlib import Path from html.parser import HTMLParser import requests import os +from datetime import datetime class MainWindow(Gtk.Window): def __init__(self): @@ -183,7 +184,7 @@ class MainWindow(Gtk.Window): } .top-bar { margin: 0px; - padding: 10px; + padding: 0px; border: 0px; } @@ -393,6 +394,22 @@ class MainWindow(Gtk.Window): margin: 0px; min-height: 0px; } + .app-action-button { + border-radius: 4px; + padding: 8px; + transition: all 0.2s ease; + } + .app-action-button:hover { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .app-url-label { + color: #18A3FF; + text-decoration: underline; + } + + .app-url-label:hover { + text-decoration: none; + } """) # Add CSS provider to the default screen @@ -447,9 +464,13 @@ class MainWindow(Gtk.Window): 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() # Use self.searchbar instead of searchbar + self.searchbar = Gtk.SearchBar() self.searchbar.set_show_close_button(False) self.searchbar.set_hexpand(False) self.searchbar.set_vexpand(False) @@ -490,10 +511,11 @@ class MainWindow(Gtk.Window): 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, -1) # Set width in pixels + 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 @@ -511,8 +533,20 @@ class MainWindow(Gtk.Window): 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 global overrides 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("Global Setting Overrides") + 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)) @@ -520,16 +554,27 @@ class MainWindow(Gtk.Window): # 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('system-reboot-symbolic') refresh_metadata_button.set_image(Gtk.Image.new_from_gicon(refresh_metadata_button_icon, Gtk.IconSize.BUTTON)) refresh_metadata_button.connect("clicked", self.on_refresh_metadata_button_clicked) + parent_system_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + parent_system_box.set_vexpand(True) # Create system mode switch box system_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + system_box.set_hexpand(False) + system_box.set_vexpand(False) + system_box.set_margin_top(0) + system_box.set_margin_bottom(0) + system_box.set_margin_start(0) + system_box.set_margin_end(0) + system_box.set_halign(Gtk.Align.CENTER) - # Create system mode switch self.system_switch = Gtk.Switch() + 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) @@ -538,21 +583,80 @@ class MainWindow(Gtk.Window): system_label = Gtk.Label(label="System") # Pack switch and label - system_box.pack_end(system_label, False, False, 0) system_box.pack_end(self.system_switch, False, False, 0) - + system_box.pack_end(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(system_box, False, False, 0) - - # Add refresh metadata button - self.top_bar.pack_end(global_overrides_button, False, False, 0) - - # Add refresh metadata button - self.top_bar.pack_end(refresh_metadata_button, False, False, 0) + self.top_bar.pack_end(parent_system_box, False, False, 0) # Add the top bar to the main box self.main_box.pack_start(self.top_bar, False, True, 0) + def on_about_clicked(self, button): + """Show the about dialog with version and license information.""" + # Create the dialog + about_dialog = Gtk.Dialog( + title="About Flatshop", + parent=self, + modal=True, + destroy_with_parent=True + ) + + # Set size + about_dialog.set_default_size(400, 200) + + # Create content area + content_area = about_dialog.get_content_area() + + # Create main box for content + main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + main_box.set_border_width(12) + + # Version label + version_label = Gtk.Label(label="Version 1.0.0") + copyright_label = Gtk.Label(label=f"Copyright © 2025-{datetime.now().year} Thomas Crider") + program_label = Gtk.Label(label="This program comes with absolutely no warranty.") + + license_label = Gtk.Label(label="License:") + license_url = Gtk.Label(label="BSD 2-Clause License") + license_url.set_use_underline(True) + license_url.set_use_markup(True) + license_url.set_markup('BSD 2-Clause License') + license_event_box = Gtk.EventBox() + license_event_box.add(license_url) + license_event_box.connect("button-release-event", + lambda w, e: Gio.AppInfo.launch_default_for_uri("https://github.com/GloriousEggroll/flatshop/blob/main/LICENSE")) + + issue_label = Gtk.Label(label="Report an Issue:") + issue_url = Gtk.Label(label="https://github.com/GloriousEggroll/flatshop/issue") + issue_url.set_use_underline(True) + issue_url.set_use_markup(True) + issue_url.set_markup('https://github.com/GloriousEggroll/flatshop/issue') + issue_event_box = Gtk.EventBox() + issue_event_box.add(issue_url) + issue_event_box.connect("button-release-event", + lambda w, e: Gio.AppInfo.launch_default_for_uri("https://github.com/GloriousEggroll/flatshop/issues")) + + + + # Add all widgets + content_area.add(main_box) + 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() @@ -1179,6 +1283,8 @@ class MainWindow(Gtk.Window): buttons_box.set_halign(Gtk.Align.END) update_all_button = Gtk.Button() + update_all_button.set_size_request(26, 26) # 40x40 pixels + update_all_button.get_style_context().add_class("app-action-button") update_all_icon = Gio.Icon.new_for_string('system-software-update-symbolic') update_all_button.set_image(Gtk.Image.new_from_gicon(update_all_icon, Gtk.IconSize.BUTTON)) update_all_button.connect("clicked", self.on_update_all_button_clicked) @@ -1410,6 +1516,8 @@ class MainWindow(Gtk.Window): # 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) @@ -1452,6 +1560,8 @@ class MainWindow(Gtk.Window): # 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") @@ -1502,203 +1612,208 @@ class MainWindow(Gtk.Window): return Gtk.Image.new_from_pixbuf(scaled_pb) def display_apps(self, apps): + """Display applications in the right container.""" + self._clear_container() + apps_by_id = self._group_apps_by_id(apps) + for app_id, app_data in apps_by_id.items(): + self._create_and_add_app_row(app_data) + + def _clear_container(self): + """Clear all children from the right container.""" for child in self.right_container.get_children(): child.destroy() - # Create a dictionary to group apps by ID - apps_by_id = {} + + 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 isn't in dictionary, add it - if app_id not in apps_by_id: - apps_by_id[app_id] = { - 'app': app, - 'repos': set() - } + if app_id not in apps_dict: + apps_dict[app_id] = {'app': app, 'repos': set()} - # Add repository to the set - repo_name = details.get('repo', 'unknown') - apps_by_id[app_id]['repos'].add(repo_name) + apps_dict[app_id]['repos'].add(details.get('repo', 'unknown')) + return apps_dict - # Display each unique application - for app_id, app_data in apps_by_id.items(): - app = app_data['app'] - details = app.get_details() - is_installed = False - for package in self.installed_results: - if details['id'] == package.id: - is_installed = True - break - is_updatable = False - for package in self.updates_results: - if details['id'] == package.id: - is_updatable = True - break + 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() - # 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) + status = self._get_app_status(app) + container = self._create_app_container() + self._setup_icon(container, details) + self._setup_text_layout(container, details, app_data['repos']) + self._setup_buttons(container, status, app) - # Add icon placeholder - icon_box = Gtk.Box() - icon_box.set_size_request(88, -1) + self.right_container.pack_start(container, False, False, 0) + self.right_container.pack_start(Gtk.Separator(), False, False, 0) + self.right_container.show_all() - # Create and add the icon - app_icon = Gio.Icon.new_for_string('package-x-generic-symbolic') - icon_widget = self.create_scaled_icon(app_icon, is_themed=True) + 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')) + } - if details['icon_filename']: - if 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) + def _create_app_container(self): + """Create the horizontal container for an application row.""" + container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + container.set_spacing(12) + container.set_margin_top(6) + container.set_margin_bottom(6) + return container - icon_widget.set_size_request(64, 64) - icon_box.pack_start(icon_widget, True, True, 0) + 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) - # Create right side layout for text - right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - right_box.set_spacing(4) - right_box.set_hexpand(True) + icon_widget = self.create_scaled_icon( + Gio.Icon.new_for_string('package-x-generic-symbolic'), + is_themed=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) + 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) - # Add repository labels - 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) + icon_widget.set_size_request(64, 64) + icon_box.pack_start(icon_widget, True, True, 0) + container.pack_start(icon_box, False, False, 0) - kind_label = Gtk.Label(label=f"Type: {details['kind']}") - kind_label.get_style_context().add_class("app-type-label") - kind_label.set_halign(Gtk.Align.START) - kind_box.pack_end(kind_label, 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) - # Add repository labels - 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) + # Title + title_label = Gtk.Label(label=details['name']) + title_label.get_style_context().add_class("app-list-header") + title_label.set_halign(Gtk.Align.START) + title_label.set_yalign(0.5) + title_label.set_hexpand(True) + right_box.pack_start(title_label, False, False, 0) - # Add repository labels - for repo in sorted(app_data['repos']): - repo_label = Gtk.Label(label=f"{repo}") - #repo_label.get_style_context().add_class("app-repo-label") - repo_label.set_halign(Gtk.Align.START) - repo_box.pack_end(repo_label, False, False, 0) - repo_list_label = Gtk.Label(label="Sources: ") - repo_box.pack_end(repo_list_label, False, False, 0) + # Kind label + kind_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + kind_box.set_spacing(4) + kind_box.set_halign(Gtk.Align.START) + kind_box.set_valign(Gtk.Align.START) - # Add summary - desc_label = Gtk.Label(label=details['summary']) - desc_label.set_halign(Gtk.Align.START) - desc_label.set_yalign(0.5) - desc_label.set_hexpand(True) - desc_label.set_line_wrap(True) - desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD) - desc_label.get_style_context().add_class("dim-label") - desc_label.get_style_context().add_class("app-list-summary") + 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) - # 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) + # Repositories + repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + repo_box.set_spacing(4) + repo_box.set_halign(Gtk.Align.START) + repo_box.set_valign(Gtk.Align.START) - # Install/Remove button - if is_installed: - button = self.create_button( - self.on_remove_clicked, - app, - None, - condition=lambda x: True - ) - add_rm_icon = "list-remove-symbolic" - else: - button = self.create_button( - self.on_install_clicked, - app, - None, - condition=lambda x: True - ) - add_rm_icon = "list-add-symbolic" + repo_list_label = Gtk.Label(label="Sources: ") + repo_box.pack_start(repo_list_label, False, False, 0) - if button: - use_icon = Gio.Icon.new_for_string(add_rm_icon) - button.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - buttons_box.pack_end(button, 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) - # App options button - if is_installed: - button = self.create_button( - self.on_app_options_clicked, - app, - None, - condition=lambda x: True - ) - add_options_icon = "applications-system-symbolic" + right_box.pack_start(repo_box, False, False, 0) - if button: - use_icon = Gio.Icon.new_for_string(add_options_icon) - button.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON)) - buttons_box.pack_end(button, False, False, 0) + # Description + desc_label = Gtk.Label(label=details['summary']) + desc_label.set_halign(Gtk.Align.START) + desc_label.set_yalign(0.5) + desc_label.set_hexpand(True) + desc_label.set_line_wrap(True) + desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD) + desc_label.get_style_context().add_class("dim-label") + desc_label.get_style_context().add_class("app-list-summary") + right_box.pack_start(desc_label, False, False, 0) - # Add Update button if available - if is_updatable: - update_button = self.create_button( - self.on_update_clicked, - app, - None, - condition=lambda x: True - ) - if update_button: - update_icon = Gio.Icon.new_for_string('system-software-update-symbolic') - update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON)) - buttons_box.pack_end(update_button, False, False, 0) + container.pack_start(right_box, True, True, 0) - # Details button - details_btn = self.create_button( - self.on_details_clicked, + def _setup_buttons(self, container, status, app): + """Set up action buttons for the application.""" + buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + buttons_box.set_spacing(6) + buttons_box.set_margin_top(4) + buttons_box.set_halign(Gtk.Align.END) + buttons_box.set_valign(Gtk.Align.CENTER) # Center vertically + + self._add_action_button( + buttons_box, + status['is_installed'], + app, + self.on_remove_clicked if status['is_installed'] else self.on_install_clicked, + "list-remove-symbolic" if status['is_installed'] else "list-add-symbolic", + "remove" if status['is_installed'] else "install" + ) + + if status['is_installed']: + self._add_action_button( + buttons_box, + True, app, - None + self.on_app_options_clicked, + "applications-system-symbolic", + "options" ) - if details_btn: - details_icon = Gio.Icon.new_for_string('help-about-symbolic') - details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.BUTTON)) - buttons_box.pack_end(details_btn, False, False, 0) - # Donate button with condition - donate_btn = self.create_button( + if status['is_updatable']: + self._add_action_button( + buttons_box, + True, + app, + self.on_update_clicked, + 'system-software-update-symbolic', + "update" + ) + + self._add_action_button( + buttons_box, + True, + app, + self.on_details_clicked, + 'help-about-symbolic', + "details" + ) + + if status['has_donation_url']: + self._add_action_button( + buttons_box, + True, + app, self.on_donate_clicked, - app, - None, - condition=lambda x: x.get_details().get('urls', {}).get('donation', '') + 'donate-symbolic', + "donate" ) - if donate_btn: - donate_icon = Gio.Icon.new_for_string('donate-symbolic') - donate_btn.set_image(Gtk.Image.new_from_gicon(donate_icon, Gtk.IconSize.BUTTON)) - buttons_box.pack_end(donate_btn, False, False, 0) - # Add widgets to right box - right_box.pack_start(title_label, False, False, 0) - right_box.pack_start(kind_box, False, False, 0) - right_box.pack_start(repo_box, False, False, 0) - right_box.pack_start(desc_label, False, False, 0) - right_box.pack_start(buttons_box, False, True, 0) + container.pack_end(buttons_box, False, False, 0) - # 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(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) + def _add_action_button(self, parent, visible, app, callback, icon_name, tooltip=None): + """Helper method to add a consistent action button.""" + if not visible: + return - self.right_container.show_all() # Show all widgets after adding them + button = self.create_button(callback, app) + if button: + # Set consistent size + button.set_size_request(26, 26) # 40x40 pixels + + # Set consistent style + button.get_style_context().add_class("app-action-button") + + icon = Gio.Icon.new_for_string(icon_name) + button.set_image(Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)) + parent.pack_end(button, False, False, 0) def show_waiting_dialog(self, message="Please wait while task is running..."): """Show a modal dialog with a spinner""" @@ -1727,32 +1842,34 @@ class MainWindow(Gtk.Window): def on_install_clicked(self, button=None, app=None): """Handle the Install button click with installation options""" - id="" - if button and app: - details = app.get_details() - title=f"Install {details['name']}?" - label=f"Install: {details['id']}?" - id=details['id'] - # this is a stupid workaround for our button creator - # so that we can use the same function in drag and drop - # which of course does not have a button object - elif app and not button: - title=f"Install {app}?" - label=f"Install: {app}?" - else: - message_type=Gtk.MessageType.ERROR - finished_dialog = Gtk.MessageDialog( - transient_for=self, - modal=True, - destroy_with_parent=True, - message_type=message_type, - buttons=Gtk.ButtonsType.OK, - text="Error: No app specified" - ) - finished_dialog.run() - finished_dialog.destroy() + if not app: + self._show_error("Error: No app specified") return - # Create dialog + + 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, @@ -1762,89 +1879,85 @@ class MainWindow(Gtk.Window): # Add buttons using the new method dialog.add_button("Cancel", Gtk.ResponseType.CANCEL) dialog.add_button("Install", Gtk.ResponseType.OK) + return dialog - # Create content area + 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) - - # Create repository dropdown - repo_combo = Gtk.ComboBoxText() - 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) - 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) + repos = fp_turbo.repolist(self.system_mode) - # Populate repository dropdown - if button and app: - available_repos = set() - repos = fp_turbo.repolist(self.system_mode) - for repo in repos: - if not repo.get_disabled(): - search_results = searcher.search_flatpak(id, repo.get_name()) - if search_results: - available_repos.add(repo) + # 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()) + } - # Add repositories to dropdown - if available_repos: - repo_combo.remove_all() # Clear any existing items + if available_repos: + self.repo_combo.remove_all() # Clear any existing items - # Add all repositories - for repo in available_repos: - repo_combo.append_text(repo.get_name()) + # 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(repo_combo, False, False, 0) - repo_combo.set_button_sensitivity(Gtk.SensitivityType.AUTO) - repo_combo.set_active(0) - else: - # Remove and re-add without dropdown - content_area.remove(repo_combo) - repo_combo.set_active(0) + # 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: - repo_combo.remove_all() # Clear any existing items - repo_combo.append_text("No repositories available") - content_area.remove(repo_combo) - # Show dialog - dialog.show_all() + # Remove and re-add without dropdown + content_area.remove(self.repo_combo) + self.repo_combo.set_active(0) else: - # Show dialog - dialog.show_all() + self.repo_combo.remove_all() # Clear any existing items + self.repo_combo.append_text("No repositories available") + content_area.remove(self.repo_combo) - # Run dialog - response = dialog.run() - if response == Gtk.ResponseType.OK: - selected_repo = None - if button and app: - selected_repo = repo_combo.get_active_text() - # Perform installation - def perform_installation(): - # Show waiting dialog - GLib.idle_add(self.show_waiting_dialog) - if button and app: - 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)) - # Start spinner and begin installation - thread = threading.Thread(target=perform_installation) - thread.daemon = True # Allow program to exit even if thread is still running - thread.start() - dialog.destroy() + def _perform_installation(self, dialog, app, button): + """Handle the installation process""" + selected_repo = None + if button: + selected_repo = self.repo_combo.get_active_text() + print(selected_repo) + + 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 tasl completion""" - # Update UI - message_type=Gtk.MessageType.INFO + """Handle task completion""" + message_type = Gtk.MessageType.INFO if not success: - message_type=Gtk.MessageType.ERROR + message_type = Gtk.MessageType.ERROR if message: finished_dialog = Gtk.MessageDialog( transient_for=self, @@ -1981,6 +2094,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2038,6 +2153,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2103,6 +2220,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2160,6 +2279,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2185,6 +2306,8 @@ class MainWindow(Gtk.Window): 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)) @@ -2265,6 +2388,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2323,6 +2448,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2340,6 +2467,8 @@ class MainWindow(Gtk.Window): 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)) @@ -2463,6 +2592,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2521,6 +2652,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -2538,6 +2671,8 @@ class MainWindow(Gtk.Window): 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)) @@ -2976,6 +3111,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -3041,6 +3178,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -3065,6 +3204,8 @@ class MainWindow(Gtk.Window): 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)) @@ -3137,6 +3278,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -3154,6 +3297,8 @@ class MainWindow(Gtk.Window): 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)) @@ -3256,6 +3401,8 @@ class MainWindow(Gtk.Window): # 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)) @@ -3273,6 +3420,8 @@ class MainWindow(Gtk.Window): 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)) @@ -3737,10 +3886,8 @@ class MainWindow(Gtk.Window): # Load the new screenshot self._load_screenshot(image, screenshots[index], app_id) - def on_details_clicked(self, button, app): - details = app.get_details() - - # Create window + def _create_details_window(self, details): + """Create and configure the main details window.""" self.details_window = Gtk.Window(title=f"{details['name']}") self.details_window.set_default_size(900, 600) @@ -3752,25 +3899,30 @@ class MainWindow(Gtk.Window): header_bar.set_show_close_button(True) self.details_window.set_titlebar(header_bar) - # Main container with padding + # Create main container box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) box_outer.set_border_width(20) box_outer.set_border_width(0) self.details_window.add(box_outer) - # Scrolled window for content + 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) - box_outer.pack_start(scrolled, True, True, 0) - # Content box 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) - # Icon section - New implementation + 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) @@ -3780,17 +3932,23 @@ class MainWindow(Gtk.Window): 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']: - if 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 - ) + 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) - # Middle column - Name, Version, Developer + content_box.pack_start(icon_row, False, True, 0) + content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) + + def _create_info_section(self, content_box, details): + """Create the information section with name, version, and developer.""" + info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + + # Middle column middle_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) name_label = Gtk.Label(label=f"{details['name']}") name_label.get_style_context().add_class("large-title") @@ -3804,7 +3962,7 @@ class MainWindow(Gtk.Window): middle_column.pack_start(version_label, False, True, 0) middle_column.pack_start(developer_label, False, True, 0) - # Right column - ID and Kind + # Right column right_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) id_label = Gtk.Label(label=f"ID: {details['id']}") id_label.set_xalign(0) @@ -3813,129 +3971,154 @@ class MainWindow(Gtk.Window): right_column.pack_start(id_label, False, True, 0) right_column.pack_start(kind_label, False, True, 0) - # Assemble the row - icon_row.pack_start(icon_box, False, False, 0) - icon_row.pack_start(middle_column, True, True, 0) - icon_row.pack_start(right_column, False, True, 0) - - content_box.pack_start(icon_row, False, True, 0) + info_box.pack_start(middle_column, True, True, 0) + info_box.pack_start(right_column, False, True, 0) + content_box.pack_start(info_box, False, True, 0) content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) - # Add the slideshow after the icon + def _create_text_section(self, title, text): + """Create a text section with title and content.""" + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + + title_label = Gtk.Label(label=f"{title}:") + title_label.set_xalign(0) + + text_view = Gtk.TextView() + text_view.set_editable(False) + text_view.set_cursor_visible(False) + text_view.set_wrap_mode(Gtk.WrapMode.WORD) + text_view.get_style_context().add_class("details-textview") + + # Parse HTML and insert into TextView + buffer = text_view.get_buffer() + if title == "Description": + try: + + class TextExtractor(HTMLParser): + def __init__(self): + super().__init__() + self.text = [] + + def handle_data(self, data): + self.text.append(data) + + def handle_starttag(self, tag, attrs): + if tag == 'p': + self.text.append('\n') + elif tag == 'ul': + self.text.append('\n') + elif tag == 'li': + self.text.append('• ') + + def handle_endtag(self, tag): + if tag == 'li': + self.text.append('\n') + elif tag == 'ul': + self.text.append('\n') + + # Parse the HTML + parser = TextExtractor() + parser.feed(text) + parsed_text = ''.join(parser.text) + + # Add basic HTML styling + buffer.set_text(parsed_text) + text_view.set_left_margin(10) + text_view.set_right_margin(10) + text_view.set_pixels_above_lines(4) + text_view.set_pixels_below_lines(4) + + except Exception: + # Fallback to plain text if HTML parsing fails + buffer.set_text(text) + else: + buffer.set_text(text) + + box.pack_start(title_label, False, True, 0) + box.pack_start(text_view, True, True, 0) + return box + + def _create_url_section(self, url_type, url): + """Create a URL section with clickable link.""" + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + + label_widget = Gtk.Label(label=f"{url_type.capitalize()}:") + label_widget.set_xalign(0) + + url_label = Gtk.Label(label=url) + url_label.set_use_underline(True) + url_label.set_use_markup(True) + url_label.set_markup(f'{url}') + url_label.set_halign(Gtk.Align.START) + + event_box = Gtk.EventBox() + event_box.add(url_label) + event_box.connect("button-release-event", + lambda w, e: Gio.AppInfo.launch_default_for_uri(url)) + + box.pack_start(label_widget, False, True, 0) + box.pack_start(event_box, True, True, 0) + return box + + def on_details_clicked(self, button, app): + """Initialize the details window setup process.""" + details = app.get_details() + + # Create window and main container + box_outer = self._create_details_window(details) + + # Create content area + content_box = self._create_content_area(box_outer) + + # Add icon section + self._create_icon_section(content_box, details) + + # Add info section + self._create_info_section(content_box, details) + + # Add screenshots screenshot_slideshow = self.create_screenshot_slideshow(details['screenshots'], details['id']) screenshot_slideshow.set_border_width(0) content_box.pack_start(screenshot_slideshow, False, True, 0) - def create_text_section(title, text): - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - - title_label = Gtk.Label(label=f"{title}:") - title_label.set_xalign(0) - - # Create a TextView for HTML content - text_view = Gtk.TextView() - text_view.set_editable(False) - text_view.set_cursor_visible(False) - text_view.set_wrap_mode(Gtk.WrapMode.WORD) - - # Remove background - text_view.get_style_context().add_class("details-textview") - - # Parse HTML and insert into TextView - buffer = text_view.get_buffer() - if title == "Description": - try: - - class TextExtractor(HTMLParser): - def __init__(self): - super().__init__() - self.text = [] - - def handle_data(self, data): - self.text.append(data) - - def handle_starttag(self, tag, attrs): - if tag == 'p': - self.text.append('\n') - elif tag == 'ul': - self.text.append('\n') - elif tag == 'li': - self.text.append('• ') - - def handle_endtag(self, tag): - if tag == 'li': - self.text.append('\n') - elif tag == 'ul': - self.text.append('\n') - - # Parse the HTML - parser = TextExtractor() - parser.feed(text) - parsed_text = ''.join(parser.text) - - # Add basic HTML styling - buffer.set_text(parsed_text) - text_view.set_left_margin(10) - text_view.set_right_margin(10) - text_view.set_pixels_above_lines(4) - text_view.set_pixels_below_lines(4) - - except Exception: - # Fallback to plain text if HTML parsing fails - buffer.set_text(text) - else: - buffer.set_text(text) - - box.pack_start(title_label, False, True, 0) - box.pack_start(text_view, True, True, 0) - return box - - def create_url_section(url_type, url): - box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - - label_widget = Gtk.Label(label=f"{url_type.capitalize()}:") - label_widget.set_xalign(0) - - # Create a clickable URL label - 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) - - # Connect click event - event_box = Gtk.EventBox() - event_box.add(url_label) - event_box.connect("button-release-event", - lambda w, e: Gio.AppInfo.launch_default_for_uri(url)) - - box.pack_start(label_widget, False, True, 0) - box.pack_start(event_box, True, True, 0) - return box - - summary_section = create_text_section("Summary", details['summary']) + # Add summary section + summary_section = self._create_text_section("Summary", details['summary']) content_box.pack_start(summary_section, False, True, 0) - content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) + content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), + False, False, 0) - # URLs section + # Add URLs section urls_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) for url_type, url in details['urls'].items(): - row = create_url_section(url_type, url) + row = self._create_url_section(url_type, url) urls_section.pack_start(row, False, True, 0) - urls_section.pack_start(create_url_section("Flathub Page", f"https://flathub.org/apps/details/{details['id']}"), False, True, 0) - + urls_section.pack_start(self._create_url_section("Flathub Page", + f"https://flathub.org/apps/details/{details['id']}"), False, True, 0) content_box.pack_start(urls_section, False, True, 0) + content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), + False, False, 0) - content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) - - description_section = create_text_section("Description", details['description']) + # Add description section + description_section = self._create_text_section("Description", + details['description']) content_box.pack_start(description_section, False, True, 0) + # Connect destroy signal and show window self.details_window.connect("destroy", lambda w: w.destroy()) self.details_window.show_all() - scrolled.get_vadjustment().set_value(0) + # 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):