yay, repository management is done

This commit is contained in:
GloriousEggroll 2025-03-23 21:47:18 -06:00
parent 8709766655
commit 57e6f363d4
2 changed files with 765 additions and 37 deletions

View file

@ -8,6 +8,11 @@ from pathlib import Path
import logging
from enum import IntEnum
import argparse
import urllib.parse
import requests
import tempfile
import os
import sys
# Set up logging
logging.basicConfig(level=logging.INFO)
@ -304,6 +309,140 @@ class AppstreamSearcher:
return updates
def reposearcher():
searcher = AppstreamSearcher()
searcher.add_installation(Flatpak.Installation.new_system())
return searcher
def repotoggle(repo, bool=True):
"""
Enable or disable a Flatpak repository
Args:
repo (str): Name of the repository to toggle
enable (bool): True to enable, False to disable
Returns:
tuple: (success, error_message)
"""
installation = Flatpak.Installation.new_system()
try:
remote = installation.get_remote_by_name(repo)
if not remote:
return False, f"Repository '{repo}' not found."
remote.set_disabled(not bool)
# Modify the remote's disabled status
success = installation.modify_remote(
remote,
None
)
if success:
if bool:
message = f"Successfully enabled {repo}."
else:
message = f"Successfully disabled {repo}."
return True, message
except GLib.GError as e:
return False, f"Failed to toggle repository: {str(e)}."
def repolist():
installation = Flatpak.Installation.new_system()
repos = installation.list_remotes()
return repos
def repodelete(repo):
installation = Flatpak.Installation.new_system()
installation.remove_remote(repo)
def repoadd(repofile):
"""Add a new repository using a .flatpakrepo file"""
# Get existing repositories
installation = Flatpak.Installation.new_system()
existing_repos = installation.list_remotes()
if not repofile.endswith('.flatpakrepo'):
return False, "Repository file path or URL must end with .flatpakrepo extension."
if repofile_is_url(repofile):
try:
local_path = download_repo(repofile)
repofile = local_path
except:
return False, f"Repository file '{repofile}' could not be downloaded."
if not os.path.exists(repofile):
return False, f"Repository file '{repofile}' does not exist."
# Get repository title from file name
title = os.path.basename(repofile).replace('.flatpakrepo', '')
# Check for duplicate title (case insensitive)
existing_titles = [repo.get_name().casefold() for repo in existing_repos]
if title.casefold() in existing_titles:
return False, "A repository with this title already exists."
# Read the repository file
try:
with open(repofile, 'rb') as f:
repo_data = f.read()
except IOError as e:
return False, f"Failed to read repository file: {str(e)}"
# Convert the data to GLib.Bytes
repo_bytes = GLib.Bytes.new(repo_data)
# Create a new remote from the repository file
try:
remote = Flatpak.Remote.new_from_file(title, repo_bytes)
# Get URLs and normalize them by removing trailing slashes
new_url = remote.get_url().rstrip('/')
existing_urls = [repo.get_url().rstrip('/') for repo in existing_repos]
# Check if URL already exists
if new_url in existing_urls:
return False, f"A repository with URL '{new_url}' already exists."
installation.add_remote(remote, True, None)
except GLib.GError as e:
return False, f"Failed to add repository: {str(e)}"
return True, None
def repofile_is_url(string):
"""Check if a string is a valid URL"""
try:
result = urllib.parse.urlparse(string)
return all([result.scheme, result.netloc])
except:
return False
def download_repo(url):
"""Download a repository file from URL to /tmp/"""
try:
# Create a deterministic filename based on the URL
url_path = urllib.parse.urlparse(url).path
filename = os.path.basename(url_path) or 'repo'
tmp_path = Path(tempfile.gettempdir()) / f"{filename}.flatpakrepo"
# Download the file
with requests.get(url, stream=True) as response:
response.raise_for_status()
# Write the file in chunks, overwriting if it exists
with open(tmp_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return str(tmp_path)
except requests.RequestException as e:
raise argparse.ArgumentTypeError(f"Failed to download repository file: {str(e)}")
except IOError as e:
raise argparse.ArgumentTypeError(f"Failed to save repository file: {str(e)}")
def main():
"""Main function demonstrating Flatpak information retrieval"""
@ -316,6 +455,15 @@ def main():
help='List all installed Flatpak applications')
parser.add_argument('--check-updates', action='store_true',
help='Check for available updates')
parser.add_argument('--list-repos', action='store_true',
help='List all configured Flatpak repositories')
parser.add_argument('--add-repo', type=str, metavar='REPO_FILE',
help='Add a new repository from a .flatpakrepo file')
parser.add_argument('--remove-repo', type=str, metavar='REPO_NAME',
help='Remove a Flatpak repository')
parser.add_argument('--toggle-repo', type=str, nargs=2,
metavar=('REPO_NAME', 'ENABLE/DISABLE'),
help='Enable or disable a repository')
args = parser.parse_args()
app_id = args.id
@ -323,12 +471,41 @@ def main():
list_all = args.list_all
show_categories = args.categories
# Create AppstreamSearcher instance
searcher = AppstreamSearcher()
# Repository management operations
if args.toggle_repo:
repo_name, enable_str = args.toggle_repo
if enable_str.lower() not in ['true', 'false', 'enable', 'disable']:
print("Invalid enable/disable value. Use 'true/false' or 'enable/disable'")
sys.exit(1)
# Add installations
installation = Flatpak.Installation.new_system(None)
searcher.add_installation(installation)
enable = enable_str.lower() in ['true', 'enable']
success, message = repotoggle(repo_name, enable)
print(message)
sys.exit(0 if success else 1)
if args.list_repos:
repos = repolist()
print("\nConfigured Repositories:")
for repo in repos:
print(f"- {repo.get_name()} ({repo.get_url()})")
return
if args.add_repo:
success, error_message = repoadd(args.add_repo)
if error_message:
print(error_message)
sys.exit(1)
else:
print(f"\nRepository added successfully: {args.add_repo}")
return
if args.remove_repo:
repodelete(args.remove_repo)
print(f"\nRepository removed successfully: {args.remove_repo}")
return
# Create AppstreamSearcher instance
searcher = reposearcher()
if args.list_installed:
installed_apps = searcher.get_installed_apps()

615
main.py
View file

@ -4,12 +4,10 @@ import gi
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
gi.require_version("Flatpak", "1.0")
from gi.repository import Gtk, Gio, Gdk
import sqlite3
from gi.repository import Gtk, Gio, Gdk, GLib
import requests
from urllib.parse import quote_plus
import libflatpak_query
from libflatpak_query import AppstreamSearcher, Flatpak
import json
import os
import time
@ -70,20 +68,38 @@ class MainWindow(Gtk.Window):
padding: 12px;
color: white;
}
.repo-list-header {
font-size: 18px;
padding: 5px;
color: white;
}
.app-list-header {
font-size: 18px;
color: white;
padding-top: 4px;
padding-bottom: 4px;
}
.app-list-summary {
padding-top: 2px;
padding-bottom: 2px;
}
.app-page-header {
font-size: 24px;
font-weight: bold;
padding: 12px;
color: white;
}
.dark-header {
background-color: #333333;
padding: 6px;
margin: 0;
}
.dark-category-button {
border: 0px;
padding: 6px;
margin: 0;
background: none;
}
.dark-category-button-active {
background-color: #18A3FF;
color: white;
@ -102,6 +118,28 @@ class MainWindow(Gtk.Window):
padding: 6px;
margin: 0;
}
.repo-item {
padding: 6px;
margin: 2px;
border-bottom: 1px solid #eee;
}
.repo-delete-button {
background-color: #ff4444;
color: white;
border: none;
padding: 6px;
margin-left: 6px;
}
.search-entry {
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
}
.search-entry:focus {
border-color: #18A3FF;
box-shadow: 0 0 0 2px rgba(24, 163, 255, 0.2);
}
""")
# Add CSS provider to the default screen
@ -113,6 +151,7 @@ class MainWindow(Gtk.Window):
# Create main layout
self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.add(self.main_box)
# Create panels
@ -125,15 +164,15 @@ class MainWindow(Gtk.Window):
def populate_repo_dropdown(self):
# Get list of repositories
installation = Flatpak.Installation.new_system()
repos = installation.list_remotes()
libflatpak_query.repolist()
repos = libflatpak_query.repolist()
# Clear existing items
self.repo_dropdown.remove_all()
# Add repository names
for repo in repos:
self.repo_dropdown.append_text(repo.get_remote_name())
self.repo_dropdown.append_text(repo.get_name())
# Connect selection changed signal
self.repo_dropdown.connect("changed", self.on_repo_selected)
@ -172,6 +211,12 @@ class MainWindow(Gtk.Window):
def refresh_data(self):
# make sure to reset these to empty before refreshing.
self.category_results = [] # Initialize empty list
self.collection_results = [] # Initialize empty list
self.installed_results = [] # Initialize empty list
self.updates_results = [] # Initialize empty list
total_categories = sum(len(categories) for categories in self.category_groups.values())
current_category = 0
msg = "Fetching metadata, please wait..."
@ -197,8 +242,7 @@ class MainWindow(Gtk.Window):
dialog.show_all()
# Search for each app in local repositories
searcher = AppstreamSearcher()
searcher.add_installation(Flatpak.Installation.new_system())
searcher = libflatpak_query.reposearcher()
json_path = "collections_data.json"
search_result = []
@ -294,18 +338,59 @@ class MainWindow(Gtk.Window):
dialog.destroy()
def create_panels(self):
# Create left panel with grouped categories
self.create_grouped_category_panel("Categories", self.category_groups)
# Check if panels already exist
if hasattr(self, 'left_panel') and self.left_panel.get_parent():
self.main_box.remove(self.left_panel)
if hasattr(self, 'right_panel') and self.right_panel.get_parent():
self.main_box.remove(self.right_panel)
# Create right panel
self.right_panel = self.create_applications_panel("Applications")
# Create left panel with grouped categories
self.left_panel = self.create_grouped_category_panel("Categories", self.category_groups)
# Pack the panels with proper expansion
self.main_box.pack_end(self.right_panel, True, True, 0) # Right panel expands both ways
self.main_box.pack_start(self.left_panel, False, False, 0) # Left panel doesn't expand
def create_grouped_category_panel(self, title, groups):
# Create container for categories
panel_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
panel_container.set_spacing(6)
panel_container.set_border_width(6)
panel_container.set_size_request(300, -1) # Set fixed width
panel_container.set_hexpand(False)
panel_container.set_vexpand(True)
panel_container.set_halign(Gtk.Align.FILL) # Fill horizontally
panel_container.set_valign(Gtk.Align.FILL) # Align to top
# Add search bar
self.searchbar = Gtk.SearchBar() # Use self.searchbar instead of searchbar
self.searchbar.set_hexpand(True)
self.searchbar.set_margin_bottom(6)
# Create search entry with icon
searchentry = Gtk.SearchEntry()
searchentry.set_placeholder_text("Search applications...")
searchentry.set_icon_from_gicon(Gtk.EntryIconPosition.PRIMARY,
Gio.Icon.new_for_string('search'))
# Connect search entry signals
searchentry.connect("search-changed", self.on_search_changed)
searchentry.connect("activate", self.on_search_activate)
# Connect search entry to search bar
self.searchbar.connect_entry(searchentry)
self.searchbar.add(searchentry)
# Create scrollable area
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_size_request(300, -1) # Set fixed width
scrolled_window.set_hexpand(False) # Don't expand horizontally
scrolled_window.set_hexpand(True)
scrolled_window.set_vexpand(True) # Expand vertically
scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
# Create container for categories
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
@ -313,6 +398,8 @@ class MainWindow(Gtk.Window):
container.set_border_width(6)
container.set_halign(Gtk.Align.FILL) # Fill horizontally
container.set_valign(Gtk.Align.START) # Align to top
container.set_hexpand(True)
container.set_vexpand(False) # Expand vertically
# Dictionary to store category widgets
self.category_widgets = {}
@ -369,7 +456,159 @@ class MainWindow(Gtk.Window):
scrolled_window.add(container)
# Pack the scrolled window directly into main box
self.main_box.pack_start(scrolled_window, False, False, 0)
panel_container.pack_start(self.searchbar, False, False, 0)
panel_container.pack_start(scrolled_window, True, True, 0)
self.searchbar.set_search_mode(True)
return panel_container
#self.searchbar.show_all()
def on_search_changed(self, searchentry):
"""Handle search text changes"""
search_term = searchentry.get_text().lower()
if not search_term:
# Reset to showing all categories when search is empty
self.show_category_apps(self.current_category)
return
# Combine all searchable fields
searchable_items = []
for app in self.all_apps:
details = app.get_details()
searchable_items.append({
'text': f"{details['name']} {details['description']} {details['categories']}".lower(),
'app': app
})
# Filter results
filtered_apps = [item['app'] for item in searchable_items
if search_term in item['text']]
# Show search results
self.show_search_results(filtered_apps)
def on_search_activate(self, searchentry):
"""Handle Enter key press in search"""
self.on_search_changed(searchentry)
def show_search_results(self, apps):
"""Display search results in the right panel"""
# Clear existing content
for child in self.right_container.get_children():
child.destroy()
# Display each application
for app in apps:
details = app.get_details()
is_installed = details['id'] in installed_package_ids
is_updatable = details['id'] in updatable_package_ids
# Create application container
app_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
app_container.set_spacing(12)
app_container.set_margin_top(6)
app_container.set_margin_bottom(6)
# Add icon placeholder
icon_box = Gtk.Box()
icon_box.set_size_request(148, -1)
# Create and add the icon
icon = Gtk.Image.new_from_file(f"{details['icon_path_64']}/{details['icon_filename']}")
icon.set_size_request(48, 48)
icon_box.pack_start(icon, True, True, 0)
# Create right side layout for text
right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
right_box.set_spacing(4)
right_box.set_hexpand(True)
# Add title
title_label = Gtk.Label(label=details['name'])
title_label.get_style_context().add_class("app-list-header")
title_label.set_halign(Gtk.Align.START)
title_label.set_yalign(0.5) # Use yalign instead of valign
title_label.set_hexpand(True)
# Add summary
desc_label = Gtk.Label(label=details['summary'])
desc_label.set_halign(Gtk.Align.START)
desc_label.set_yalign(0.5) # Use yalign instead of valign
desc_label.set_hexpand(True)
desc_label.set_line_wrap(True)
desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD)
desc_label.get_style_context().add_class("dim-label")
desc_label.get_style_context().add_class("app-list-summary")
# Create buttons box
buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
buttons_box.set_spacing(6)
buttons_box.set_margin_top(4)
buttons_box.set_halign(Gtk.Align.END)
# Install/Remove button
if is_installed:
button = self.create_button(
self.on_remove_clicked,
app,
None,
condition=lambda x: True
)
remove_icon = Gio.Icon.new_for_string('list-remove')
button.set_image(Gtk.Image.new_from_gicon(remove_icon, Gtk.IconSize.BUTTON))
button.get_style_context().add_class("dark-remove-button")
else:
button = self.create_button(
self.on_install_clicked,
app,
None,
condition=lambda x: True
)
install_icon = Gio.Icon.new_for_string('list-add')
button.set_image(Gtk.Image.new_from_gicon(install_icon, Gtk.IconSize.BUTTON))
button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(button, False, False, 0)
# Add Update button if available
if is_updatable:
update_button = self.create_button(
self.on_update_clicked,
app,
None,
condition=lambda x: True
)
update_icon = Gio.Icon.new_for_string('synchronize')
update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON))
update_button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(update_button, False, False, 0)
# Details button
details_btn = self.create_button(
self.on_details_clicked,
app,
None
)
details_icon = Gio.Icon.new_for_string('question')
details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.BUTTON))
details_btn.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(details_btn, False, False, 0)
# Add widgets to right box
right_box.pack_start(title_label, False, False, 0)
right_box.pack_start(desc_label, False, False, 0)
right_box.pack_start(buttons_box, False, True, 0)
# Add separator
separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
# Add to container
app_container.pack_start(icon_box, False, False, 0)
app_container.pack_start(right_box, True, True, 0)
self.right_container.pack_start(app_container, False, False, 0)
self.right_container.pack_start(separator, False, False, 0)
self.right_container.show_all()
def on_category_clicked(self, category, group):
# Remove active state from all widgets in all groups
@ -423,8 +662,8 @@ class MainWindow(Gtk.Window):
scrolled_window.add(self.right_container)
self.right_panel.pack_start(scrolled_window, True, True, 0)
self.main_box.pack_end(self.right_panel, True, True, 0)
return self.right_container
return self.right_panel
def check_internet(self):
"""Check if internet connection is available."""
@ -471,7 +710,6 @@ class MainWindow(Gtk.Window):
def save_collections_data(self, filename='collections_data.json'):
"""Save all collected collections data to a JSON file."""
if not hasattr(self, 'collections_db') or not self.collections_db:
print("No collections data available to save")
return
try:
@ -481,9 +719,11 @@ class MainWindow(Gtk.Window):
print(f"Error saving collections data: {str(e)}")
# Create and connect buttons
def create_button(self, label, callback, app, condition=None):
def create_button(self, callback, app, label=None, condition=None):
"""Create a button with optional visibility condition"""
button = Gtk.Button(label=label)
button = Gtk.Button()
if label:
button = Gtk.Button(label=label)
button.get_style_context().add_class("app-button")
if condition is not None:
if not condition(app):
@ -550,6 +790,125 @@ class MainWindow(Gtk.Window):
if category in app.get_details()['categories']
])
if 'repositories' in category:
# Clear existing content
for child in self.right_container.get_children():
child.destroy()
# Create header bar
header_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
header_bar.set_hexpand(True)
header_bar.set_spacing(6)
header_bar.set_border_width(6)
# Create left label
left_label = Gtk.Label(label="On/Off")
left_label.get_style_context().add_class("repo-list-header")
left_label.set_halign(Gtk.Align.START) # Align left
header_bar.pack_start(left_label, True, True, 0)
# Center container to fix "URL" label alignment
center_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
center_container.set_halign(Gtk.Align.START) # Align left
# Create center label
center_label = Gtk.Label(label="URL")
center_label.get_style_context().add_class("repo-list-header")
center_label.set_halign(Gtk.Align.START) # Align center
center_container.pack_start(center_label, True, True, 0)
header_bar.pack_start(center_container, True, True, 0)
# Create right label
right_label = Gtk.Label(label="+/-")
right_label.get_style_context().add_class("repo-list-header")
right_label.set_halign(Gtk.Align.END) # Align right
header_bar.pack_end(right_label, False, False, 0)
# Add header bar to container
self.right_container.pack_start(header_bar, False, False, 0)
# Get list of repositories
repos = libflatpak_query.repolist()
# Create a scrolled window for repositories
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_hexpand(True)
scrolled_window.set_vexpand(True)
# Create container for repositories
repo_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
repo_container.set_spacing(6)
repo_container.set_border_width(6)
# Add repository button
add_repo_button = Gtk.Button()
add_icon = Gio.Icon.new_for_string('list-add')
add_repo_button.set_image(Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.BUTTON))
add_repo_button.get_style_context().add_class("dark-install-button")
add_repo_button.connect("clicked", self.on_add_repo_button_clicked)
add_flathub_repo_button = Gtk.Button(label="Add Flathub Repo")
add_flathub_repo_button.get_style_context().add_class("dark-install-button")
add_flathub_repo_button.connect("clicked", self.on_add_flathub_repo_button_clicked)
add_flathub_beta_repo_button = Gtk.Button(label="Add Flathub Beta Repo")
add_flathub_beta_repo_button.get_style_context().add_class("dark-install-button")
add_flathub_beta_repo_button.connect("clicked", self.on_add_flathub_beta_repo_button_clicked)
# Check for existing Flathub repositories and disable buttons accordingly
flathub_url = "https://dl.flathub.org/repo/"
flathub_beta_url = "https://dl.flathub.org/beta-repo/"
existing_urls = [repo.get_url().rstrip('/') for repo in repos]
add_flathub_repo_button.set_sensitive(flathub_url.rstrip('/') not in existing_urls)
add_flathub_beta_repo_button.set_sensitive(flathub_beta_url.rstrip('/') not in existing_urls)
# Add repositories to container
for repo in repos:
repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
repo_box.set_spacing(6)
repo_box.set_hexpand(True)
# Create checkbox
checkbox = Gtk.CheckButton(label=repo.get_name())
checkbox.set_active(not repo.get_disabled())
if not repo.get_disabled():
checkbox.get_style_context().remove_class("dim-label")
else:
checkbox.get_style_context().add_class("dim-label")
checkbox.connect("toggled", self.on_repo_toggled, repo)
checkbox_url_label = Gtk.Label(label=repo.get_url())
checkbox_url_label.set_halign(Gtk.Align.START)
checkbox_url_label.set_hexpand(True)
checkbox_url_label.get_style_context().add_class("dim-label")
# Create delete button
delete_button = Gtk.Button()
delete_icon = Gio.Icon.new_for_string('list-remove')
delete_button.set_image(Gtk.Image.new_from_gicon(delete_icon, Gtk.IconSize.BUTTON))
delete_button.get_style_context().add_class("destructive-action")
delete_button.connect("clicked", self.on_repo_delete, repo)
# Add widgets to box
repo_box.pack_start(checkbox, False, False, 0)
repo_box.pack_start(checkbox_url_label, False, False, 0)
repo_box.pack_end(delete_button, False, False, 0)
# Add box to container
repo_container.pack_start(repo_box, False, False, 0)
repo_container.pack_start(add_repo_button, False, False, 0)
repo_container.pack_start(add_flathub_repo_button, False, False, 0)
repo_container.pack_start(add_flathub_beta_repo_button, False, False, 0)
# Add container to scrolled window
scrolled_window.add(repo_container)
self.right_container.pack_start(scrolled_window, True, True, 0)
self.right_container.show_all()
return
# Display each application
for app in apps:
details = app.get_details()
@ -564,11 +923,11 @@ class MainWindow(Gtk.Window):
# Add icon placeholder
icon_box = Gtk.Box()
icon_box.set_size_request(148, -1)
icon_box.set_size_request(94, -1)
# Create and add the icon
icon = Gtk.Image.new_from_file(f"{details['icon_path_64']}/{details['icon_filename']}")
icon.set_size_request(48, 48)
icon = Gtk.Image.new_from_file(f"{details['icon_path_128']}/{details['icon_filename']}")
icon.set_size_request(74, 74)
icon_box.pack_start(icon, True, True, 0)
# Create right side layout for text
@ -578,13 +937,15 @@ class MainWindow(Gtk.Window):
# Add title
title_label = Gtk.Label(label=details['name'])
title_label.get_style_context().add_class("title-1")
title_label.get_style_context().add_class("app-list-header")
title_label.set_halign(Gtk.Align.START)
title_label.set_valign(Gtk.Align.CENTER)
title_label.set_hexpand(True)
# Add summary
desc_label = Gtk.Label(label=details['summary'])
desc_label.set_halign(Gtk.Align.START)
desc_label.set_valign(Gtk.Align.CENTER)
desc_label.set_hexpand(True)
desc_label.set_line_wrap(True)
desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD)
@ -599,49 +960,61 @@ class MainWindow(Gtk.Window):
# Install/Remove button
if is_installed:
button = self.create_button(
"Remove",
self.on_remove_clicked,
app,
None,
condition=lambda x: True
)
remove_icon = Gio.Icon.new_for_string('list-remove')
button.set_image(Gtk.Image.new_from_gicon(remove_icon, Gtk.IconSize.BUTTON))
button.get_style_context().add_class("dark-remove-button")
else:
button = self.create_button(
"Install",
self.on_install_clicked,
app,
None,
condition=lambda x: True
)
install_icon = Gio.Icon.new_for_string('list-add')
button.set_image(Gtk.Image.new_from_gicon(install_icon, Gtk.IconSize.BUTTON))
button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(button, False, False, 0)
# Add Update button if available
if is_updatable:
update_button = self.create_button(
"Update",
self.on_update_clicked,
app,
None,
condition=lambda x: True
)
update_icon = Gio.Icon.new_for_string('synchronize')
update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON))
update_button.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(update_button, False, False, 0)
# Details button
details_btn = self.create_button(
"Details",
self.on_details_clicked,
app
app,
None
)
details_icon = Gio.Icon.new_for_string('question')
details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.BUTTON))
details_btn.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(details_btn, False, False, 0)
# Donate button with condition
donate_btn = self.create_button(
"Donate",
self.on_donate_clicked,
app,
lambda x: x.get_details().get('urls', {}).get('donation', '')
None,
condition=lambda x: x.get_details().get('urls', {}).get('donation', '')
)
if donate_btn:
donate_icon = Gio.Icon.new_for_string('donate')
donate_btn.set_image(Gtk.Image.new_from_gicon(donate_icon, Gtk.IconSize.BUTTON))
donate_btn.get_style_context().add_class("dark-install-button")
buttons_box.pack_end(donate_btn, False, False, 0)
# Add widgets to right box
@ -701,6 +1074,184 @@ class MainWindow(Gtk.Window):
except Exception as e:
print(f"Error opening donation URL: {str(e)}")
def on_repo_toggled(self, checkbox, repo):
"""Handle repository enable/disable toggle"""
repo.set_disabled(checkbox.get_active())
# Update the UI to reflect the new state
checkbox.get_parent().set_sensitive(True)
if checkbox.get_active():
checkbox.get_style_context().remove_class("dim-label")
success, message = libflatpak_query.repotoggle(repo.get_name(), True)
message_type = Gtk.MessageType.INFO
if success:
self.refresh_data()
else:
if message:
message_type = Gtk.MessageType.ERROR
if message:
dialog = Gtk.MessageDialog(
transient_for=None, # Changed from self
modal=True,
destroy_with_parent=True,
message_type=message_type,
buttons=Gtk.ButtonsType.OK,
text=message
)
dialog.run()
dialog.destroy()
else:
checkbox.get_style_context().add_class("dim-label")
success, message = libflatpak_query.repotoggle(repo.get_name(), False)
message_type = Gtk.MessageType.INFO
if success:
self.refresh_data()
else:
if message:
message_type = Gtk.MessageType.ERROR
if message:
dialog = Gtk.MessageDialog(
transient_for=None, # Changed from self
modal=True,
destroy_with_parent=True,
message_type=message_type,
buttons=Gtk.ButtonsType.OK,
text=message
)
dialog.run()
dialog.destroy()
def on_repo_delete(self, button, repo):
"""Handle repository deletion"""
dialog = Gtk.MessageDialog(
transient_for=self,
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.YES_NO,
text=f"Are you sure you want to delete the '{repo.get_name()}' repository?"
)
response = dialog.run()
dialog.destroy()
if response == Gtk.ResponseType.YES:
try:
libflatpak_query.repodelete(repo.get_name())
self.refresh_data()
self.show_category_apps('repositories')
except GLib.GError as e:
# Handle polkit authentication failure
if "not allowed for user" in str(e):
error_dialog = Gtk.MessageDialog(
transient_for=self,
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text="You don't have permission to remove this repository. "
"Please try running the application with sudo privileges."
)
error_dialog.run()
error_dialog.destroy()
else:
# Handle other potential errors
error_dialog = Gtk.MessageDialog(
transient_for=self,
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=f"Failed to remove repository: {str(e)}"
)
error_dialog.run()
error_dialog.destroy()
def on_add_flathub_repo_button_clicked(self, button):
"""Handle the Add Flathub Repository button click"""
# Add the repository
success, error_message = libflatpak_query.repoadd("https://dl.flathub.org/repo/flathub.flatpakrepo")
if error_message:
error_dialog = Gtk.MessageDialog(
transient_for=None, # Changed from self
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=error_message
)
error_dialog.run()
error_dialog.destroy()
self.refresh_data()
self.show_category_apps('repositories')
def on_add_flathub_beta_repo_button_clicked(self, button):
"""Handle the Add Flathub Beta Repository button click"""
# Add the repository
success, error_message = libflatpak_query.repoadd("https://dl.flathub.org/beta-repo/flathub-beta.flatpakrepo")
if error_message:
error_dialog = Gtk.MessageDialog(
transient_for=None, # Changed from self
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=error_message
)
error_dialog.run()
error_dialog.destroy()
self.refresh_data()
self.show_category_apps('repositories')
def on_add_repo_button_clicked(self, button):
"""Handle the Add Repository button click"""
# Create file chooser dialog
dialog = Gtk.FileChooserDialog(
title="Select Repository File",
parent=self,
action=Gtk.FileChooserAction.OPEN,
flags=0
)
# Add buttons using the new method
dialog.add_buttons(
"Cancel", Gtk.ResponseType.CANCEL,
"Open", Gtk.ResponseType.OK
)
# Add filter for .flatpakrepo files
repo_filter = Gtk.FileFilter()
repo_filter.set_name("Flatpak Repository Files")
repo_filter.add_pattern("*.flatpakrepo")
dialog.add_filter(repo_filter)
# Show all files filter
all_filter = Gtk.FileFilter()
all_filter.set_name("All Files")
all_filter.add_pattern("*")
dialog.add_filter(all_filter)
# Run the dialog
response = dialog.run()
repo_file_path = dialog.get_filename()
dialog.destroy()
if response == Gtk.ResponseType.OK and repo_file_path:
# Add the repository
success, error_message = libflatpak_query.repoadd(repo_file_path)
if error_message:
error_dialog = Gtk.MessageDialog(
transient_for=None, # Changed from self
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=error_message
)
error_dialog.run()
error_dialog.destroy()
self.refresh_data()
self.show_category_apps('repositories')
def select_default_category(self):
# Select Trending by default
if 'collections' in self.category_widgets and self.category_widgets['collections']: