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