flatpost/main.py

1463 lines
56 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")
from gi.repository import Gtk, Gio, Gdk, GLib
import libflatpak_query
import json
import threading
class MainWindow(Gtk.Window):
def __init__(self):
super().__init__()
# Store search results as an instance variable
self.all_apps = []
self.category_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 for Games
self.subcategories = {
'Emulator': 'Emulators',
'Launcher': 'Game Launchers',
'Tool': 'Game 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;
}
""")
# 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.HORIZONTAL)
self.add(self.main_box)
# Create panels
self.create_panels()
self.refresh_data()
# Select Trending by default
self.select_default_category()
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 refresh_target():
try:
category_results, collection_results, installed_results, updates_results, all_apps = searcher.retrieve_metadata(self.system_mode)
self.category_results = category_results
self.collection_results = collection_results
self.installed_results = installed_results
self.updates_results = updates_results
self.all_apps = all_apps
except Exception as e:
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 updating progress: {str(e)}"
)
dialog.run()
dialog.destroy()
# Start the refresh thread
refresh_thread = threading.Thread(target=refresh_target)
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):
# Create dialog and progress bar
dialog = Gtk.Dialog(
title="Refreshing local data, 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 refresh_target():
try:
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 updating progress: {str(e)}"
)
dialog.run()
dialog.destroy()
# Start the refresh thread
refresh_thread = threading.Thread(target=refresh_target)
refresh_thread.start()
def update_progress():
while refresh_thread.is_alive():
progress_bar.set_text("Refreshing...")
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 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 right panel
self.right_panel = self.create_applications_panel("Applications")
# Create left panel with grouped categories
self.left_panel = self.create_grouped_category_panel("Categories", self.category_groups)
# Pack the panels with proper expansion
self.main_box.pack_end(self.right_panel, True, True, 0) # Right panel expands both ways
self.main_box.pack_start(self.left_panel, False, False, 0) # Left panel doesn't expand
def create_grouped_category_panel(self, title, groups):
# Create container for categories
panel_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
panel_container.set_spacing(6)
panel_container.set_border_width(6)
panel_container.set_size_request(300, -1) # Set fixed width
panel_container.set_hexpand(False)
panel_container.set_vexpand(True)
panel_container.set_halign(Gtk.Align.FILL) # Fill horizontally
panel_container.set_valign(Gtk.Align.FILL) # Align to top
# Add search bar
self.searchbar = Gtk.SearchBar() # Use self.searchbar instead of searchbar
self.searchbar.set_hexpand(True)
self.searchbar.set_margin_bottom(6)
# Create search entry with icon
searchentry = Gtk.SearchEntry()
searchentry.set_placeholder_text("Search applications...")
searchentry.set_icon_from_gicon(Gtk.EntryIconPosition.PRIMARY,
Gio.Icon.new_for_string('search'))
# Connect search entry signals
searchentry.connect("search-changed", self.on_search_changed)
searchentry.connect("activate", self.on_search_activate)
# Connect search entry to search bar
self.searchbar.connect_entry(searchentry)
self.searchbar.add(searchentry)
# Create scrollable area
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_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(self.searchbar, False, False, 0)
panel_container.pack_start(scrolled_window, True, True, 0)
self.searchbar.set_search_mode(True)
return panel_container
#self.searchbar.show_all()
def on_search_changed(self, searchentry):
"""Handle search text changes"""
pass # Don't perform search on every keystroke
def on_search_activate(self, searchentry):
"""Handle Enter key press in search"""
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"""
exact_id_matches = []
exact_name_matches = []
partial_matches = []
other_matches = []
# Process each item
for item in searchable_items:
# 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"""
# Clear existing content
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 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(148, -1)
# Create and add the icon
icon = Gtk.Image.new_from_file(f"{details['icon_path_128']}/{details['icon_filename']}")
icon.set_size_request(48, 48)
icon_box.pack_start(icon, True, True, 0)
# Create right side layout for text
right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
right_box.set_spacing(4)
right_box.set_hexpand(True)
# Add title
title_label = Gtk.Label(label=details['name'])
title_label.get_style_context().add_class("app-list-header")
title_label.set_halign(Gtk.Align.START)
title_label.set_yalign(0.5) # Use yalign instead of valign
title_label.set_hexpand(True)
# Add 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('synchronize')
update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON))
update_button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(update_button, False, False, 0)
# Details button
details_btn = self.create_button(
self.on_details_clicked,
app,
None
)
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(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()
def on_category_clicked(self, category, group):
# 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.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."""
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]
elif category in self.subcategories:
display_title = self.subcategories[category]
else:
display_title = category.capitalize()
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)
# 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 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)
scrolled_window.add(self.right_container)
self.right_panel.pack_start(scrolled_window, True, True, 0)
return self.right_panel
# 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 show_category_apps(self, category):
# Clear existing content properly
for child in self.right_container.get_children():
child.destroy()
# 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])
# 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
# 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(148, -1)
# Create and add the icon
icon = Gtk.Image.new_from_file(f"{details['icon_path_128']}/{details['icon_filename']}")
icon.set_size_request(48, 48)
icon_box.pack_start(icon, True, True, 0)
# Create right side layout for text
right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
right_box.set_spacing(4)
right_box.set_hexpand(True)
# Add title
title_label = Gtk.Label(label=details['name'])
title_label.get_style_context().add_class("app-list-header")
title_label.set_halign(Gtk.Align.START)
title_label.set_yalign(0.5)
title_label.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.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
)
if button:
remove_icon = Gio.Icon.new_for_string('list-remove')
button.set_image(Gtk.Image.new_from_gicon(remove_icon, Gtk.IconSize.BUTTON))
button.get_style_context().add_class("dark-remove-button")
else:
button = self.create_button(
self.on_install_clicked,
app,
None,
condition=lambda x: True
)
if button:
install_icon = Gio.Icon.new_for_string('list-add')
button.set_image(Gtk.Image.new_from_gicon(install_icon, Gtk.IconSize.BUTTON))
button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(button, False, False, 0)
# Add Update button if available
if is_updatable:
update_button = self.create_button(
self.on_update_clicked,
app,
None,
condition=lambda x: True
)
if update_button:
update_icon = Gio.Icon.new_for_string('synchronize')
update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON))
update_button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(update_button, False, False, 0)
# Details button
details_btn = self.create_button(
self.on_details_clicked,
app,
None
)
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(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 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()
repo_combo.set_hexpand(True)
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
# Get selected values
if self.system_mode is False:
print(f"Installing {details['name']} for User")
else:
print(f"Installing {details['name']} for System")
success, message = libflatpak_query.install_flatpak(app, selected_repo, self.system_mode)
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
)
self.refresh_local()
self.refresh_current_page()
finished_dialog.run()
finished_dialog.destroy()
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
# Get selected values
if self.system_mode is False:
print(f"Removing {details['name']} for User.")
else:
print(f"Removing {details['name']} for System.")
success, message = libflatpak_query.remove_flatpak(app, self.system_mode)
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
)
self.refresh_local()
self.refresh_current_page()
finished_dialog.run()
finished_dialog.destroy()
dialog.destroy()
def on_update_clicked(self, button, app):
"""Handle the Update button click"""
details = app.get_details()
print(f"Updating application: {details['name']}")
# Implement update logic here
# Example:
# Flatpak.update(app_id=details['id'])
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()