flatpost/main.py
2025-03-29 06:24:13 -06:00

1845 lines
73 KiB
Python
Executable file

#!/usr/bin/python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
gi.require_version("Flatpak", "1.0")
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gtk, Gio, Gdk, GLib, GdkPixbuf
import libflatpak_query
from libflatpak_query import AppStreamComponentKind as AppKind
import json
import threading
import subprocess
from pathlib import Path
class MainWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Flatshop")
# Store search results as an instance variable
self.all_apps = []
self.current_component_type = None
self.category_results = [] # Initialize empty list
self.subcategory_results = [] # Initialize empty list
self.collection_results = [] # Initialize empty list
self.installed_results = [] # Initialize empty list
self.updates_results = [] # Initialize empty list
self.system_mode = False
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)
# 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'
}
}
# Add CSS provider for custom styling
css_provider = Gtk.CssProvider()
css_provider.load_from_data("""
.panel-header {
font-size: 24px;
font-weight: bold;
padding: 12px;
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;
}
.dark-remove-button {
background-color: #ff4444;
color: white;
border: none;
padding: 6px;
margin: 0;
}
.dark-install-button {
background-color: #18A3FF;
color: white;
border: none;
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);
}
.item-repo-label {
background-color: #333333;
color: white;
border-radius: 4px;
margin: 2px;
padding: 2px 4px;
font-size: 0.8em;
}
.dark-category-button {
border: 0px;
padding: 6px;
margin: 0;
background: none;
}
.dark-category-button-active {
background-color: #18A3FF;
color: white;
}
.subcategories-scroll {
border: none;
background-color: transparent;
min-height: 40px;
}
.subcategories-scroll > GtkViewport {
border: none;
background-color: transparent;
}
.no-scroll-bars scrollbar {
min-width: 0px;
opacity: 0;
margin-top: -20px;
}
""")
# Add CSS provider to the default screen
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css_provider,
600
)
# 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()
self.refresh_data()
# Select Trending by default
self.select_default_category()
def create_header_bar(self):
# Create horizontal bar
self.top_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.top_bar.set_hexpand(True)
self.top_bar.set_vexpand(False)
self.top_bar.set_spacing(6)
self.top_bar.set_border_width(0) # Remove border width
self.top_bar.set_margin_top(0) # Remove top margin
self.top_bar.set_margin_bottom(0) # Remove bottom margin
# Add search bar
self.searchbar = Gtk.SearchBar() # Use self.searchbar instead of searchbar
self.searchbar.set_hexpand(False)
self.searchbar.set_vexpand(False)
self.searchbar.set_margin_bottom(6)
self.searchbar.set_margin_bottom(0) # Remove bottom margin
self.searchbar.set_margin_top(0) # Remove top margin
# 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'))
searchentry.set_margin_top(0) # Remove top margin
searchentry.set_margin_bottom(0) # Remove bottom margin
searchentry.set_size_request(-1, 10) # Set specific height
# 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.set_hexpand(False)
self.component_type_combo.set_vexpand(False)
self.component_type_combo.set_size_request(150, -1) # 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 repository button
refresh_metadata_button = Gtk.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.get_style_context().add_class("dark-install-button")
refresh_metadata_button.connect("clicked", self.on_refresh_metadata_button_clicked)
# Create system mode switch box
system_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create system mode switch
self.system_switch = Gtk.Switch()
self.system_switch.connect("notify::active", self.on_system_mode_toggled)
self.system_switch.set_hexpand(False)
self.system_switch.set_vexpand(False)
# Create system mode label
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)
# Add system controls to header
self.top_bar.pack_end(system_box, False, False, 0)
# Add refresh metadata button
self.top_bar.pack_end(refresh_metadata_button, False, False, 0)
# Add the top bar to the main box
self.main_box.pack_start(self.top_bar, False, True, 0)
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 on_system_mode_toggled(self, switch, gparam):
"""Handle system mode toggle switch state changes"""
desired_state = switch.get_active()
if desired_state:
# Request superuser validation
try:
#subprocess.run(['pkexec', 'true'], check=True)
self.system_mode = True
self.refresh_data()
self.refresh_current_page()
except subprocess.CalledProcessError:
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:
if self.system_mode == True:
self.system_mode = False
self.refresh_data()
self.refresh_current_page()
elif self.system_mode == False:
self.system_mode = True
self.refresh_data()
self.refresh_current_page()
def populate_repo_dropdown(self):
# Get list of repositories
libflatpak_query.repolist(self.system_mode)
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_name())
# Connect selection changed signal
self.repo_dropdown.connect("changed", self.on_repo_selected)
def on_repo_selected(self, dropdown):
active_index = dropdown.get_active()
if active_index != -1:
self.selected_repo = dropdown.get_model()[active_index][0]
print(f"Selected repository: {self.selected_repo}")
def refresh_data(self):
# Create dialog and progress bar
dialog = Gtk.Dialog(
title="Fetching metadata, please wait...",
parent=self,
modal=True,
destroy_with_parent=True
)
dialog.set_size_request(400, 100)
progress_bar = Gtk.ProgressBar()
progress_bar.set_text("Initializing...")
progress_bar.set_show_text(True)
dialog.vbox.pack_start(progress_bar, True, True, 0)
dialog.vbox.set_spacing(12)
# Show the dialog
dialog.show_all()
searcher = libflatpak_query.get_reposearcher(self.system_mode)
# Define thread target function
def retrieve_metadata():
try:
category_results, collection_results, subcategory_results, installed_results, updates_results, all_apps = searcher.retrieve_metadata(self.system_mode)
self.category_results = category_results
self.category_results = subcategory_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 = libflatpak_query.get_reposearcher(self.system_mode)
installed_results, updates_results = searcher.refresh_local(self.system_mode)
self.installed_results = installed_results
self.updates_results = updates_results
except Exception as e:
message_type = Gtk.MessageType.ERROR
dialog = Gtk.MessageDialog(
transient_for=None, # Changed from self
modal=True,
destroy_with_parent=True,
message_type=message_type,
buttons=Gtk.ButtonsType.OK,
text=f"Error refreshing local data: {str(e)}"
)
dialog.run()
dialog.destroy()
def create_panels(self):
# Check if panels already exist
if hasattr(self, 'left_panel') and self.left_panel.get_parent():
self.main_box.remove(self.left_panel)
if hasattr(self, 'right_panel') and self.right_panel.get_parent():
self.main_box.remove(self.right_panel)
# Create left panel with grouped categories
self.left_panel = self.create_grouped_category_panel("Categories", self.category_groups)
# Create right panel
self.right_panel = self.create_applications_panel("Applications")
# Create panels container
self.panels_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.panels_box.set_hexpand(True)
# Pack the panels with proper expansion
self.panels_box.pack_start(self.left_panel, False, False, 0) # Left panel doesn't expand
self.panels_box.pack_end(self.right_panel, True, True, 0) # Right panel expands both ways
# Add panels container to main box
self.main_box.pack_start(self.panels_box, True, True, 0)
def create_grouped_category_panel(self, title, groups):
# Create container for categories
panel_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
panel_container.set_spacing(6)
panel_container.set_border_width(6)
panel_container.set_size_request(300, -1) # Set fixed width
panel_container.set_hexpand(False)
panel_container.set_vexpand(True)
panel_container.set_halign(Gtk.Align.FILL) # Fill horizontally
panel_container.set_valign(Gtk.Align.FILL) # Align to top
# Create scrollable area
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_hexpand(True)
scrolled_window.set_vexpand(True) # Expand vertically
scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
# Create container for categories
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
container.set_spacing(6)
container.set_border_width(6)
container.set_halign(Gtk.Align.FILL) # Fill horizontally
container.set_valign(Gtk.Align.START) # Align to top
container.set_hexpand(True)
container.set_vexpand(False) # Expand vertically
# Dictionary to store category widgets
self.category_widgets = {}
# Add group headers and categories
for group_name, categories in groups.items():
# Create a box for the header
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
header_box.get_style_context().add_class("dark-header")
header_box.set_hexpand(True) # Make the box expand horizontally
# Create the label
group_header = Gtk.Label(label=group_name.upper())
group_header.get_style_context().add_class("title-2")
group_header.set_halign(Gtk.Align.START)
# Add the label to the box
header_box.pack_start(group_header, False, False, 0)
# Add the box to the container
container.pack_start(header_box, False, False, 0)
# Store widgets for this group
self.category_widgets[group_name] = []
# Add categories in the group
for category, display_title in categories.items():
# Create a clickable box for each category
category_box = Gtk.EventBox()
category_box.set_hexpand(True)
category_box.set_halign(Gtk.Align.FILL)
category_box.set_margin_top(2)
category_box.set_margin_bottom(2)
# Create label for the category
category_label = Gtk.Label(label=display_title)
category_label.set_halign(Gtk.Align.START)
category_label.set_hexpand(True)
category_label.get_style_context().add_class("dark-category-button")
# Add label to the box
category_box.add(category_label)
# Connect click event
category_box.connect("button-release-event",
lambda widget, event, cat=category, grp=group_name:
self.on_category_clicked(cat, grp))
# Store widget in group
self.category_widgets[group_name].append(category_box)
container.pack_start(category_box, False, False, 0)
# Add container to scrolled window
scrolled_window.add(container)
# Pack the scrolled window directly into main box
panel_container.pack_start(scrolled_window, True, True, 0)
return panel_container
def on_search_changed(self, searchentry):
"""Handle search text changes"""
pass # Don't perform search on every keystroke
def on_search_activate(self, searchentry):
"""Handle Enter key press in search"""
self.update_category_header("Search Results")
search_term = searchentry.get_text().lower()
if not search_term:
# Reset to showing all categories when search is empty
self.show_category_apps(self.current_category)
return
# Combine all searchable fields
searchable_items = []
for app in self.all_apps:
details = app.get_details()
searchable_items.append({
'text': f"{details['name']} {details['description']} {details['categories']}".lower(),
'app': app,
'id': details['id'].lower(),
'name': details['name'].lower()
})
# Filter and rank results
filtered_apps = self.rank_search_results(search_term, searchable_items)
# Show search results
self.show_search_results(filtered_apps)
def rank_search_results(self, search_term, searchable_items):
"""Rank search results based on match type and component type filter"""
exact_id_matches = []
exact_name_matches = []
partial_matches = []
other_matches = []
# Get current component type filter
component_type_filter = self.current_component_type
if component_type_filter is None:
component_type_filter = None # Allow all types
# Process each item
for item in searchable_items:
# Check if component type matches filter
if component_type_filter and item['app'].get_details()['kind'] != component_type_filter:
continue
# Check exact ID match
if item['id'] == search_term:
exact_id_matches.append(item['app'])
continue
# Check exact name match
if item['name'] == search_term:
exact_name_matches.append(item['app'])
continue
# Check for partial matches longer than 5 characters
if len(search_term) > 5:
if search_term in item['id'] or search_term in item['name']:
partial_matches.append(item['app'])
continue
# Check for other matches
if search_term in item['text']:
other_matches.append(item['app'])
# Combine results in order of priority
return exact_id_matches + exact_name_matches + partial_matches + other_matches
def show_search_results(self, apps):
"""Display search results in the right panel"""
self.display_apps(apps)
def on_category_clicked(self, category, group, *args):
# Remove active state from all widgets in all groups
for group_name in self.category_widgets:
for widget in self.category_widgets[group_name]:
widget.get_style_context().remove_class("dark-category-button-active")
# Add active state to the clicked category
display_title = self.category_groups[group][category]
for widget in self.category_widgets[group]:
if widget.get_children()[0].get_label() == display_title:
widget.get_style_context().add_class("dark-category-button-active")
break
self.current_page = category
self.current_group = group
self.update_category_header(category)
self.update_subcategories_bar(category)
self.show_category_apps(category)
def refresh_current_page(self):
"""Refresh the currently displayed page"""
if self.current_page and self.current_group:
self.on_category_clicked(self.current_page, self.current_group)
def update_category_header(self, category):
"""Update the category header text based on the selected category."""
display_title = ""
if category in self.category_groups['system']:
display_title = self.category_groups['system'][category]
if category in self.category_groups['collections']:
display_title = self.category_groups['collections'][category]
elif category in self.category_groups['categories']:
display_title = self.category_groups['categories'][category]
else: # Find the parent category and get the title
for parent_category, subcategories in self.subcategory_groups.items():
if category in subcategories:
display_title = subcategories[category]
break
if display_title == "":
# Fallback if category isn't found
display_title = category
self.category_header.set_label(display_title)
def create_applications_panel(self, title):
# Create right panel
self.right_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.right_panel.set_hexpand(True) # Add this line
self.right_panel.set_vexpand(True) # Add this line
# Add category header
self.category_header = Gtk.Label(label="")
self.category_header.get_style_context().add_class("panel-header")
self.category_header.set_hexpand(True)
self.category_header.set_halign(Gtk.Align.START)
self.right_panel.pack_start(self.category_header, False, False, 0)
# Create subcategories bar (initially hidden)
self.subcategories_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.subcategories_bar.set_hexpand(True)
self.subcategories_bar.set_spacing(6)
self.subcategories_bar.set_border_width(6)
#self.subcategories_bar.get_style_context().add_class("dark-header")
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 scrollable area
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_hexpand(True)
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
scrolled_window.add(self.right_container)
self.right_panel.pack_start(scrolled_window, True, True, 0)
return self.right_panel
def update_subcategories_bar(self, category):
"""Update the subcategories bar based on the current category."""
# Clear existing subcategories
for child in self.subcategories_bar.get_children():
child.destroy()
# Create pan start button
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("dark-category-button")
pan_start.connect("clicked", self.on_pan_start)
# Create scrolled window
self.scrolled_window = Gtk.ScrolledWindow()
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) # Allow shrinking below content size
self.scrolled_window.set_max_content_width(-1) # No artificial width limit
self.scrolled_window.set_overlay_scrolling(False)
self.scrolled_window.get_style_context().add_class("no-scroll-bars")
# Create container for subcategories
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)
# Check if the category has subcategories
if category in self.subcategory_groups:
# Add subcategories
for subcategory, title in self.subcategory_groups[category].items():
# Create clickable box for subcategory
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)
# Create label for subcategory
subcategory_label = Gtk.Label(label=title)
subcategory_label.set_halign(Gtk.Align.START)
subcategory_label.set_hexpand(False)
subcategory_label.get_style_context().add_class("dark-category-button")
# Add label to box
subcategory_box.add(subcategory_label)
# Connect click event
subcategory_box.connect("button-release-event",
lambda widget, event, subcat=subcategory:
self.on_subcategory_clicked(subcat))
# Store widget in group
container.pack_start(subcategory_box, False, False, 0)
# Add container to scrolled window
self.scrolled_window.add(container)
# Create pan end button
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("dark-category-button")
pan_end.connect("clicked", self.on_pan_end)
# Show the bar and force a layout update
self.subcategories_bar.get_style_context().add_class("dark-header")
self.subcategories_bar.set_visible(True)
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.pack_start(container, True, True, 0)
self.subcategories_bar.queue_resize()
self.subcategories_bar.show_all()
else:
# Check if current category is a subcategory
is_subcategory = False
parent_category = None
for parent, subcategories in self.subcategory_groups.items():
if category in subcategories:
is_subcategory = True
parent_category = parent
break
if is_subcategory:
# Add parent category and current subcategory
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)
# Create parent category box
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)
# Create parent label
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("dark-category-button")
# Add label to box
parent_box.add(parent_label)
# Connect click event
parent_box.connect("button-release-event",
lambda widget, event, cat=parent_category, grp='categories':
self.on_category_clicked(cat, grp))
# Add parent box to container
container.pack_start(parent_box, False, False, 0)
# Create current subcategory box
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)
# Create subcategory label
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("dark-category-button")
# Add label to box
subcategory_box.add(subcategory_label)
# Connect click event
subcategory_box.connect("button-release-event",
lambda widget, event, subcat=category:
self.on_subcategory_clicked(subcat))
# Add subcategory box to container
container.pack_start(subcategory_box, False, False, 0)
# Add container to scrolled window
self.scrolled_window.add(container)
self.subcategories_bar.get_style_context().add_class("dark-header")
# Show the bar and force a layout update
self.subcategories_bar.set_visible(True)
self.subcategories_bar.pack_start(self.scrolled_window, True, True, 0)
#self.subcategories_bar.pack_start(container, True, True, 0)
self.subcategories_bar.queue_resize()
self.subcategories_bar.show_all()
else:
self.subcategories_bar.get_style_context().remove_class("dark-header")
# Hide the bar and force a layout update
self.subcategories_bar.set_visible(False)
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 on_subcategory_clicked(self, subcategory):
"""Handle subcategory button clicks."""
# Update the current page to the subcategory
self.current_page = subcategory
self.current_group = 'subcategories'
self.update_category_header(subcategory)
self.update_subcategories_bar(subcategory)
self.show_category_apps(subcategory)
# 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 = []
# 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']))
# Load collections data
try:
with open("collections_data.json", '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:
print(f"Error reading collections data: {str(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 = libflatpak_query.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_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
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_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
# Apply component type filter if set
component_type_filter = self.current_component_type
if component_type_filter:
apps = [app for app in apps if app.get_details()['kind'] == component_type_filter]
self.display_apps(apps)
def create_scaled_icon(self, icon, is_themed=False):
if is_themed:
# For themed icons, create a pixbuf directly using the icon theme
icon_theme = Gtk.IconTheme.get_default()
pb = icon_theme.load_icon(icon.get_names()[0], 64, Gtk.IconLookupFlags.FORCE_SIZE)
else:
# For file-based icons
pb = GdkPixbuf.Pixbuf.new_from_file(icon)
# Scale to 64x64 using high-quality interpolation
scaled_pb = pb.scale_simple(
64, 64, # New dimensions
GdkPixbuf.InterpType.BILINEAR # High-quality scaling
)
# Create the image widget from the scaled pixbuf
return Gtk.Image.new_from_pixbuf(scaled_pb)
def display_apps(self, apps):
for child in self.right_container.get_children():
child.destroy()
# Create a dictionary to group apps by ID
apps_by_id = {}
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()
}
# Add repository to the set
repo_name = details.get('repo', 'unknown')
apps_by_id[app_id]['repos'].add(repo_name)
# 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
# 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(88, -1)
# 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)
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)
icon_widget.set_size_request(64, 64)
icon_box.pack_start(icon_widget, 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 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)
kind_label = Gtk.Label(label=details['kind'])
kind_label.get_style_context().add_class("item-repo-label")
kind_label.set_halign(Gtk.Align.START)
kind_box.pack_end(kind_label, False, False, 0)
# Add repository labels
repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
repo_box.set_spacing(4)
repo_box.set_halign(Gtk.Align.END)
repo_box.set_valign(Gtk.Align.END)
# Add repository labels
for repo in sorted(app_data['repos']):
repo_label = Gtk.Label(label=repo)
repo_label.get_style_context().add_class("item-repo-label")
repo_label.set_halign(Gtk.Align.END)
repo_box.pack_end(repo_label, False, False, 0)
# 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")
# 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
)
add_rm_icon = "list-remove"
add_rm_style = "dark-remove-buton"
else:
button = self.create_button(
self.on_install_clicked,
app,
None,
condition=lambda x: True
)
add_rm_icon = "list-add"
add_rm_style = "dark-install-buton"
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))
button.get_style_context().add_class(add_rm_style)
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
)
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))
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
)
if details_btn:
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(
self.on_donate_clicked,
app,
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
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)
# 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)
self.right_container.show_all() # Show all widgets after adding them
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, app):
"""Handle the Install button click with installation options"""
details = app.get_details()
# Create dialog
dialog = Gtk.Dialog(
title=f"Install {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("Install", Gtk.ResponseType.OK)
# Create 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=f"Install: {details['id']}?"), False, False, 0)
# Search for available repositories containing this app
searcher = libflatpak_query.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)
# Populate repository dropdown
available_repos = set()
repos = libflatpak_query.repolist(self.system_mode)
for repo in repos:
if not repo.get_disabled():
search_results = searcher.search_flatpak(details['id'], repo.get_name())
if search_results:
available_repos.add(repo)
# Add repositories to dropdown
if available_repos:
repo_combo.remove_all() # Clear any existing items
# Add all repositories
for repo in available_repos:
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)
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()
# Run dialog
response = dialog.run()
if response == Gtk.ResponseType.OK:
selected_repo = repo_combo.get_active_text()
# Perform installation
def perform_installation():
# Show waiting dialog
GLib.idle_add(self.show_waiting_dialog)
success, message = libflatpak_query.install_flatpak(app, selected_repo, 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_installation)
thread.daemon = True # Allow program to exit even if thread is still running
thread.start()
dialog.destroy()
def on_task_complete(self, dialog, success, message):
"""Handle tasl completion"""
# Update UI
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 = libflatpak_query.remove_flatpak(app, None, 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 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 = libflatpak_query.update_flatpak(app, None, 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 on_details_clicked(self, button, app):
"""Handle the Details button click"""
details = app.get_details()
print(f"Showing details for: {details['name']}")
# Implement details view here
# Could open a new window with extended information
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 = libflatpak_query.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 = libflatpak_query.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:
libflatpak_query.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 = libflatpak_query.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 = libflatpak_query.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):
"""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, 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():
app = MainWindow()
app.connect("destroy", Gtk.main_quit)
app.show_all()
Gtk.main()
if __name__ == "__main__":
main()