3333 lines
132 KiB
Python
Executable file
3333 lines
132 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
import gi
|
|
import sys
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version("GLib", "2.0")
|
|
gi.require_version("Flatpak", "1.0")
|
|
gi.require_version('GdkPixbuf', '2.0')
|
|
from gi.repository import Gtk, Gio, Gdk, GLib, GdkPixbuf
|
|
import fp_turbo
|
|
from fp_turbo import AppStreamComponentKind as AppKind
|
|
import json
|
|
import threading
|
|
import subprocess
|
|
from pathlib import Path
|
|
from html.parser import HTMLParser
|
|
import requests
|
|
import os
|
|
|
|
class MainWindow(Gtk.Window):
|
|
def __init__(self):
|
|
super().__init__(title="Flatshop")
|
|
# Store search results as an instance variable
|
|
self.all_apps = []
|
|
self.current_component_type = None
|
|
self.category_results = [] # Initialize empty list
|
|
self.subcategory_results = [] # Initialize empty list
|
|
self.collection_results = [] # Initialize empty list
|
|
self.installed_results = [] # Initialize empty list
|
|
self.updates_results = [] # Initialize empty list
|
|
self.system_mode = False
|
|
self.current_page = None # Track current page
|
|
self.current_group = None # Track current group (system/collections/categories)
|
|
|
|
# Set window size
|
|
self.set_default_size(1280, 720)
|
|
|
|
# Enable drag and drop
|
|
self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
|
self.drag_dest_add_uri_targets()
|
|
self.connect("drag-data-received", self.on_drag_data_received)
|
|
|
|
# Define category groups and their titles
|
|
self.category_groups = {
|
|
'system': {
|
|
'installed': 'Installed',
|
|
'updates': 'Updates',
|
|
'repositories': 'Repositories'
|
|
},
|
|
'collections': {
|
|
'trending': 'Trending',
|
|
'popular': 'Popular',
|
|
'recently-added': 'New',
|
|
'recently-updated': 'Updated'
|
|
},
|
|
'categories': {
|
|
'office': 'Productivity',
|
|
'graphics': 'Graphics & Photography',
|
|
'audiovideo': 'Audio & Video',
|
|
'education': 'Education',
|
|
'network': 'Networking',
|
|
'game': 'Games',
|
|
'development': 'Developer Tools',
|
|
'science': 'Science',
|
|
'system': 'System',
|
|
'utility': 'Utilities'
|
|
}
|
|
}
|
|
|
|
# Define subcategories
|
|
self.subcategory_groups = {
|
|
'audiovideo': {
|
|
'audiovideoediting': 'Audio & Video Editing',
|
|
'discburning': 'Disc Burning',
|
|
'midi': 'Midi',
|
|
'mixer': 'Mixer',
|
|
'player': 'Player',
|
|
'recorder': 'Recorder',
|
|
'sequencer': 'Sequencer',
|
|
'tuner': 'Tuner',
|
|
'tv': 'TV'
|
|
},
|
|
'development': {
|
|
'building': 'Building',
|
|
'database': 'Database',
|
|
'debugger': 'Debugger',
|
|
'guidesigner': 'GUI Designer',
|
|
'ide': 'IDE',
|
|
'profiling': 'Profiling',
|
|
'revisioncontrol': 'Revision Control',
|
|
'translation': 'Translation',
|
|
'webdevelopment': 'Web Development'
|
|
},
|
|
'game': {
|
|
'actiongame': 'Action Games',
|
|
'adventuregame': 'Adventure Games',
|
|
'arcadegame': 'Arcade Games',
|
|
'blocksgame': 'Blocks Games',
|
|
'boardgame': 'Board Games',
|
|
'cardgame': 'Card Games',
|
|
'emulator': 'Emulators',
|
|
'kidsgame': 'Kids\' Games',
|
|
'logicgame': 'Logic Games',
|
|
'roleplaying': 'Role Playing',
|
|
'shooter': 'Shooter',
|
|
'simulation': 'Simulation',
|
|
'sportsgame': 'Sports Games',
|
|
'strategygame': 'Strategy Games'
|
|
},
|
|
'graphics': {
|
|
'2dgraphics': '2D Graphics',
|
|
'3dgraphics': '3D Graphics',
|
|
'ocr': 'OCR',
|
|
'photography': 'Photography',
|
|
'publishing': 'Publishing',
|
|
'rastergraphics': 'Raster Graphics',
|
|
'scanning': 'Scanning',
|
|
'vectorgraphics': 'Vector Graphics',
|
|
'viewer': 'Viewer'
|
|
},
|
|
'network': {
|
|
'chat': 'Chat',
|
|
'email': 'Email',
|
|
'feed': 'Feed',
|
|
'filetransfer': 'File Transfer',
|
|
'hamradio': 'Ham Radio',
|
|
'instantmessaging': 'Instant Messaging',
|
|
'ircclient': 'IRC Client',
|
|
'monitor': 'Monitor',
|
|
'news': 'News',
|
|
'p2p': 'P2P',
|
|
'remoteaccess': 'Remote Access',
|
|
'telephony': 'Telephony',
|
|
'videoconference': 'Video Conference',
|
|
'webbrowser': 'Web Browser',
|
|
'webdevelopment': 'Web Development'
|
|
},
|
|
'office': {
|
|
'calendar': 'Calendar',
|
|
'chart': 'Chart',
|
|
'contactmanagement': 'Contact Management',
|
|
'database': 'Database',
|
|
'dictionary': 'Dictionary',
|
|
'email': 'Email',
|
|
'finance': 'Finance',
|
|
'presentation': 'Presentation',
|
|
'projectmanagement': 'Project Management',
|
|
'publishing': 'Publishing',
|
|
'spreadsheet': 'Spreadsheet',
|
|
'viewer': 'Viewer',
|
|
'wordprocessor': 'Word Processor'
|
|
},
|
|
'system': {
|
|
'emulator': 'Emulators',
|
|
'filemanager': 'File Manager',
|
|
'filesystem': 'Filesystem',
|
|
'filetools': 'File Tools',
|
|
'monitor': 'Monitor',
|
|
'security': 'Security',
|
|
'terminalemulator': 'Terminal Emulator'
|
|
},
|
|
'utility': {
|
|
'accessibility': 'Accessibility',
|
|
'archiving': 'Archiving',
|
|
'calculator': 'Calculator',
|
|
'clock': 'Clock',
|
|
'compression': 'Compression',
|
|
'filetools': 'File Tools',
|
|
'telephonytools': 'Telephony Tools',
|
|
'texteditor': 'Text Editor',
|
|
'texttools': 'Text Tools'
|
|
}
|
|
}
|
|
|
|
# Add CSS provider for custom styling
|
|
css_provider = Gtk.CssProvider()
|
|
css_provider.load_from_data("""
|
|
.panel-header {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
padding: 12px;
|
|
color: white;
|
|
}
|
|
.repo-list-header {
|
|
font-size: 18px;
|
|
padding: 5px;
|
|
color: white;
|
|
}
|
|
.app-list-header {
|
|
font-size: 18px;
|
|
color: white;
|
|
padding-top: 4px;
|
|
padding-bottom: 4px;
|
|
}
|
|
.app-list-summary {
|
|
padding-top: 2px;
|
|
padding-bottom: 2px;
|
|
}
|
|
.app-page-header {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
padding: 12px;
|
|
color: white;
|
|
}
|
|
.dark-header {
|
|
background-color: #333333;
|
|
padding: 6px;
|
|
margin: 0;
|
|
}
|
|
.dark-category-button {
|
|
border: 0px;
|
|
padding: 6px;
|
|
margin: 0;
|
|
background: none;
|
|
}
|
|
.dark-category-button-active {
|
|
background-color: #18A3FF;
|
|
color: white;
|
|
}
|
|
.dark-remove-button {
|
|
background-color: #ff4444;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px;
|
|
margin: 0;
|
|
}
|
|
.dark-install-button {
|
|
background-color: #18A3FF;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px;
|
|
margin: 0;
|
|
}
|
|
.repo-item {
|
|
padding: 6px;
|
|
margin: 2px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.repo-delete-button {
|
|
background-color: #ff4444;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px;
|
|
margin-left: 6px;
|
|
}
|
|
.search-entry {
|
|
padding: 5px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
}
|
|
|
|
.search-entry:focus {
|
|
border-color: #18A3FF;
|
|
box-shadow: 0 0 0 2px rgba(24, 163, 255, 0.2);
|
|
}
|
|
.item-repo-label {
|
|
background-color: #333333;
|
|
color: white;
|
|
border-radius: 4px;
|
|
margin: 2px;
|
|
padding: 2px 4px;
|
|
font-size: 0.8em;
|
|
}
|
|
.dark-category-button {
|
|
border: 0px;
|
|
padding: 6px;
|
|
margin: 0;
|
|
background: none;
|
|
}
|
|
|
|
.dark-category-button-active {
|
|
background-color: #18A3FF;
|
|
color: white;
|
|
}
|
|
|
|
.subcategories-scroll {
|
|
border: none;
|
|
background-color: transparent;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.subcategories-scroll > GtkViewport {
|
|
border: none;
|
|
background-color: transparent;
|
|
}
|
|
.no-scroll-bars scrollbar {
|
|
min-width: 0px;
|
|
opacity: 0;
|
|
margin-top: -20px;
|
|
}
|
|
.app-window {
|
|
border: 0px;
|
|
margin: 0px;
|
|
padding-right: 20px;
|
|
background: none;
|
|
}
|
|
.screenshot-bullet {
|
|
color: #18A3FF;
|
|
font-size: 30px;
|
|
padding: 4px;
|
|
border-radius: 50%;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.screenshot-bullet:hover {
|
|
background-color: rgba(24, 163, 255, 0.2);
|
|
}
|
|
.details-window {
|
|
border: 0px;
|
|
margin: 0px;
|
|
padding: 20px;
|
|
background: none;
|
|
}
|
|
.details-textview {
|
|
background-color: transparent;
|
|
border-width: 0;
|
|
border-radius: 0;
|
|
}
|
|
.permissions-window {
|
|
border: 0px;
|
|
margin: 0px;
|
|
padding: 20px;
|
|
background: none;
|
|
}
|
|
""")
|
|
|
|
# Add CSS provider to the default screen
|
|
Gtk.StyleContext.add_provider_for_screen(
|
|
Gdk.Screen.get_default(),
|
|
css_provider,
|
|
600
|
|
)
|
|
|
|
# Create main layout
|
|
self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
self.add(self.main_box)
|
|
|
|
# Create_header_bar
|
|
self.create_header_bar()
|
|
|
|
# Create panels
|
|
self.create_panels()
|
|
|
|
self.refresh_data()
|
|
#self.refresh_local()
|
|
|
|
# Select Trending by default
|
|
self.select_default_category()
|
|
|
|
def on_drag_data_received(self, widget, context, x, y, data, info, time):
|
|
"""Handle drag and drop events"""
|
|
# Check if data is a URI list
|
|
if isinstance(data, int):
|
|
return
|
|
uri = data.get_uris()[0]
|
|
file_path = Gio.File.new_for_uri(uri).get_path()
|
|
if file_path and file_path.endswith('.flatpakref'):
|
|
self.handle_flatpakref_file(file_path)
|
|
if file_path and file_path.endswith('.flatpakrepo'):
|
|
self.handle_flatpakrepo_file(file_path)
|
|
context.finish(True, False, time)
|
|
|
|
def handle_flatpakref_file(self, file_path):
|
|
"""Handle .flatpakref file installation"""
|
|
self.on_install_clicked(None, file_path)
|
|
|
|
def handle_flatpakrepo_file(self, file_path):
|
|
"""Handle .flatpakrepo file installation"""
|
|
self.on_add_repo_button_clicked(None, file_path)
|
|
|
|
def create_header_bar(self):
|
|
# Create horizontal bar
|
|
self.top_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
self.top_bar.set_hexpand(True)
|
|
self.top_bar.set_vexpand(False)
|
|
self.top_bar.set_spacing(6)
|
|
self.top_bar.set_border_width(0) # Remove border width
|
|
self.top_bar.set_margin_top(0) # Remove top margin
|
|
self.top_bar.set_margin_bottom(0) # Remove bottom margin
|
|
|
|
# Add search bar
|
|
self.searchbar = Gtk.SearchBar() # Use self.searchbar instead of searchbar
|
|
self.searchbar.set_hexpand(False)
|
|
self.searchbar.set_vexpand(False)
|
|
self.searchbar.set_margin_bottom(6)
|
|
self.searchbar.set_margin_bottom(0) # Remove bottom margin
|
|
self.searchbar.set_margin_top(0) # Remove top margin
|
|
|
|
# Create search entry with icon
|
|
searchentry = Gtk.SearchEntry()
|
|
searchentry.set_placeholder_text("Search applications...")
|
|
searchentry.set_icon_from_gicon(Gtk.EntryIconPosition.PRIMARY,
|
|
Gio.Icon.new_for_string('search'))
|
|
searchentry.set_margin_top(0) # Remove top margin
|
|
searchentry.set_margin_bottom(0) # Remove bottom margin
|
|
searchentry.set_size_request(-1, 10) # Set specific height
|
|
|
|
# Connect search entry signals
|
|
searchentry.connect("search-changed", self.on_search_changed)
|
|
searchentry.connect("activate", self.on_search_activate)
|
|
|
|
# Connect search entry to search bar
|
|
self.searchbar.connect_entry(searchentry)
|
|
self.searchbar.add(searchentry)
|
|
self.searchbar.set_search_mode(True)
|
|
|
|
self.top_bar.pack_start(self.searchbar, False, False, 0)
|
|
|
|
self.component_type_combo_label = Gtk.Label(label="Search Type:")
|
|
# Create component type dropdown
|
|
self.component_type_combo = Gtk.ComboBoxText()
|
|
self.component_type_combo.set_hexpand(False)
|
|
self.component_type_combo.set_vexpand(False)
|
|
self.component_type_combo.set_size_request(150, -1) # Set width in pixels
|
|
self.component_type_combo.connect("changed", self.on_component_type_changed)
|
|
|
|
# Add "ALL" option first
|
|
self.component_type_combo.append_text("ALL")
|
|
|
|
# Add all component types
|
|
for kind in AppKind:
|
|
if kind != AppKind.UNKNOWN:
|
|
self.component_type_combo.append_text(kind.name)
|
|
|
|
# Select "ALL" by default
|
|
self.component_type_combo.set_active(0)
|
|
|
|
# Add dropdown to header bar
|
|
self.top_bar.pack_start(self.component_type_combo_label, False, False, 0)
|
|
self.top_bar.pack_start(self.component_type_combo, False, False, 0)
|
|
|
|
# Add global overrides button
|
|
global_overrides_button = Gtk.Button()
|
|
global_overrides_button.set_tooltip_text("Global Setting Overrides")
|
|
global_overrides_button_icon = Gio.Icon.new_for_string('system-run')
|
|
global_overrides_button.set_image(Gtk.Image.new_from_gicon(global_overrides_button_icon, Gtk.IconSize.BUTTON))
|
|
global_overrides_button.get_style_context().add_class("dark-install-button")
|
|
global_overrides_button.connect("clicked", self.global_on_options_clicked)
|
|
|
|
# Add refresh metadata button
|
|
refresh_metadata_button = Gtk.Button()
|
|
refresh_metadata_button.set_tooltip_text("Refresh metadata")
|
|
refresh_metadata_button_icon = Gio.Icon.new_for_string('system-reboot-symbolic')
|
|
refresh_metadata_button.set_image(Gtk.Image.new_from_gicon(refresh_metadata_button_icon, Gtk.IconSize.BUTTON))
|
|
refresh_metadata_button.get_style_context().add_class("dark-install-button")
|
|
refresh_metadata_button.connect("clicked", self.on_refresh_metadata_button_clicked)
|
|
|
|
# Create system mode switch box
|
|
system_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
|
|
# Create system mode switch
|
|
self.system_switch = Gtk.Switch()
|
|
self.system_switch.connect("notify::active", self.on_system_mode_toggled)
|
|
self.system_switch.set_hexpand(False)
|
|
self.system_switch.set_vexpand(False)
|
|
|
|
# Create system mode label
|
|
system_label = Gtk.Label(label="System")
|
|
|
|
# Pack switch and label
|
|
system_box.pack_end(system_label, False, False, 0)
|
|
system_box.pack_end(self.system_switch, False, False, 0)
|
|
|
|
# Add system controls to header
|
|
self.top_bar.pack_end(system_box, False, False, 0)
|
|
|
|
# Add refresh metadata button
|
|
self.top_bar.pack_end(global_overrides_button, False, False, 0)
|
|
|
|
# Add refresh metadata button
|
|
self.top_bar.pack_end(refresh_metadata_button, False, False, 0)
|
|
|
|
# Add the top bar to the main box
|
|
self.main_box.pack_start(self.top_bar, False, True, 0)
|
|
|
|
def on_refresh_metadata_button_clicked(self, button):
|
|
self.refresh_data()
|
|
self.refresh_current_page()
|
|
|
|
def on_component_type_changed(self, combo):
|
|
"""Handle component type filter changes"""
|
|
selected_type = combo.get_active_text()
|
|
if selected_type:
|
|
if selected_type == "ALL":
|
|
self.current_component_type = None
|
|
else:
|
|
self.current_component_type = selected_type
|
|
else:
|
|
self.current_component_type = None
|
|
self.refresh_current_page()
|
|
|
|
def on_system_mode_toggled(self, switch, gparam):
|
|
"""Handle system mode toggle switch state changes"""
|
|
desired_state = switch.get_active()
|
|
|
|
if desired_state:
|
|
# Request superuser validation
|
|
try:
|
|
#subprocess.run(['pkexec', 'true'], check=True)
|
|
self.system_mode = True
|
|
self.refresh_data()
|
|
self.refresh_current_page()
|
|
except subprocess.CalledProcessError:
|
|
switch.set_active(False)
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=self,
|
|
message_type=Gtk.MessageType.ERROR,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text="Authentication failed",
|
|
secondary_text="Could not enable system mode"
|
|
)
|
|
dialog.connect("response", lambda d, r: d.destroy())
|
|
dialog.show()
|
|
else:
|
|
if self.system_mode == True:
|
|
self.system_mode = False
|
|
self.refresh_data()
|
|
self.refresh_current_page()
|
|
elif self.system_mode == False:
|
|
self.system_mode = True
|
|
self.refresh_data()
|
|
self.refresh_current_page()
|
|
|
|
def populate_repo_dropdown(self):
|
|
# Get list of repositories
|
|
fp_turbo.repolist(self.system_mode)
|
|
repos = fp_turbo.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 = fp_turbo.get_reposearcher(self.system_mode)
|
|
|
|
# Define thread target function
|
|
def retrieve_metadata():
|
|
try:
|
|
category_results, collection_results, subcategory_results, installed_results, updates_results, all_apps = searcher.retrieve_metadata(self.system_mode)
|
|
self.category_results = category_results
|
|
self.category_results = subcategory_results
|
|
self.collection_results = collection_results
|
|
self.installed_results = installed_results
|
|
self.updates_results = updates_results
|
|
self.all_apps = all_apps
|
|
except Exception as e:
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=None, # Changed from self
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
message_type=Gtk.MessageType.ERROR,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text=f"Error retrieving metadata: {str(e)}"
|
|
)
|
|
dialog.run()
|
|
dialog.destroy()
|
|
# Start the refresh thread
|
|
refresh_thread = threading.Thread(target=retrieve_metadata)
|
|
refresh_thread.start()
|
|
def update_progress():
|
|
while refresh_thread.is_alive():
|
|
progress_bar.set_text("Fetching...")
|
|
progress = searcher.refresh_progress
|
|
progress_bar.set_fraction(progress / 100)
|
|
return True
|
|
else:
|
|
progress_bar.set_fraction(100 / 100)
|
|
dialog.destroy()
|
|
|
|
# Start the progress update timer
|
|
GLib.timeout_add_seconds(0.5, update_progress)
|
|
dialog.run()
|
|
if not refresh_thread.is_alive() and dialog.is_active():
|
|
dialog.destroy()
|
|
|
|
def refresh_local(self):
|
|
try:
|
|
searcher = fp_turbo.get_reposearcher(self.system_mode)
|
|
installed_results, updates_results = searcher.refresh_local(self.system_mode)
|
|
self.installed_results = installed_results
|
|
self.updates_results = updates_results
|
|
except Exception as e:
|
|
message_type = Gtk.MessageType.ERROR
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=None, # Changed from self
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
message_type=message_type,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text=f"Error refreshing local data: {str(e)}"
|
|
)
|
|
dialog.run()
|
|
dialog.destroy()
|
|
|
|
|
|
def create_panels(self):
|
|
# Check if panels already exist
|
|
if hasattr(self, 'left_panel') and self.left_panel.get_parent():
|
|
self.main_box.remove(self.left_panel)
|
|
|
|
if hasattr(self, 'right_panel') and self.right_panel.get_parent():
|
|
self.main_box.remove(self.right_panel)
|
|
|
|
# Create left panel with grouped categories
|
|
self.left_panel = self.create_grouped_category_panel("Categories", self.category_groups)
|
|
|
|
# Create right panel
|
|
self.right_panel = self.create_applications_panel("Applications")
|
|
|
|
# Create panels container
|
|
self.panels_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
self.panels_box.set_hexpand(True)
|
|
|
|
# Pack the panels with proper expansion
|
|
self.panels_box.pack_start(self.left_panel, False, False, 0) # Left panel doesn't expand
|
|
self.panels_box.pack_end(self.right_panel, True, True, 0) # Right panel expands both ways
|
|
|
|
# Add panels container to main box
|
|
self.main_box.pack_start(self.panels_box, True, True, 0)
|
|
|
|
def create_grouped_category_panel(self, title, groups):
|
|
|
|
# Create container for categories
|
|
panel_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
panel_container.set_spacing(6)
|
|
panel_container.set_border_width(6)
|
|
panel_container.set_size_request(300, -1) # Set fixed width
|
|
panel_container.set_hexpand(False)
|
|
panel_container.set_vexpand(True)
|
|
panel_container.set_halign(Gtk.Align.FILL) # Fill horizontally
|
|
panel_container.set_valign(Gtk.Align.FILL) # Align to top
|
|
|
|
# Create scrollable area
|
|
scrolled_window = Gtk.ScrolledWindow()
|
|
scrolled_window.set_hexpand(True)
|
|
scrolled_window.set_vexpand(True) # Expand vertically
|
|
scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
# Create container for categories
|
|
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
container.set_spacing(6)
|
|
container.set_border_width(6)
|
|
container.set_halign(Gtk.Align.FILL) # Fill horizontally
|
|
container.set_valign(Gtk.Align.START) # Align to top
|
|
container.set_hexpand(True)
|
|
container.set_vexpand(False) # Expand vertically
|
|
|
|
# Dictionary to store category widgets
|
|
self.category_widgets = {}
|
|
|
|
# Add group headers and categories
|
|
for group_name, categories in groups.items():
|
|
# Create a box for the header
|
|
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
header_box.get_style_context().add_class("dark-header")
|
|
header_box.set_hexpand(True) # Make the box expand horizontally
|
|
|
|
# Create the label
|
|
group_header = Gtk.Label(label=group_name.upper())
|
|
group_header.get_style_context().add_class("title-2")
|
|
group_header.set_halign(Gtk.Align.START)
|
|
|
|
# Add the label to the box
|
|
header_box.pack_start(group_header, False, False, 0)
|
|
|
|
# Add the box to the container
|
|
container.pack_start(header_box, False, False, 0)
|
|
|
|
# Store widgets for this group
|
|
self.category_widgets[group_name] = []
|
|
|
|
# Add categories in the group
|
|
for category, display_title in categories.items():
|
|
# Create a clickable box for each category
|
|
category_box = Gtk.EventBox()
|
|
category_box.set_hexpand(True)
|
|
category_box.set_halign(Gtk.Align.FILL)
|
|
category_box.set_margin_top(2)
|
|
category_box.set_margin_bottom(2)
|
|
|
|
# Create label for the category
|
|
category_label = Gtk.Label(label=display_title)
|
|
category_label.set_halign(Gtk.Align.START)
|
|
category_label.set_hexpand(True)
|
|
category_label.get_style_context().add_class("dark-category-button")
|
|
|
|
# Add label to the box
|
|
category_box.add(category_label)
|
|
|
|
# Connect click event
|
|
category_box.connect("button-release-event",
|
|
lambda widget, event, cat=category, grp=group_name:
|
|
self.on_category_clicked(cat, grp))
|
|
|
|
# Store widget in group
|
|
self.category_widgets[group_name].append(category_box)
|
|
container.pack_start(category_box, False, False, 0)
|
|
|
|
# Add container to scrolled window
|
|
scrolled_window.add(container)
|
|
|
|
# Pack the scrolled window directly into main box
|
|
panel_container.pack_start(scrolled_window, True, True, 0)
|
|
|
|
return panel_container
|
|
|
|
def on_search_changed(self, searchentry):
|
|
"""Handle search text changes"""
|
|
pass # Don't perform search on every keystroke
|
|
|
|
|
|
def on_search_activate(self, searchentry):
|
|
"""Handle Enter key press in search"""
|
|
self.update_category_header("Search Results")
|
|
search_term = searchentry.get_text().lower()
|
|
if not search_term:
|
|
# Reset to showing all categories when search is empty
|
|
self.show_category_apps(self.current_category)
|
|
return
|
|
|
|
# Combine all searchable fields
|
|
searchable_items = []
|
|
for app in self.all_apps:
|
|
details = app.get_details()
|
|
searchable_items.append({
|
|
'text': f"{details['name']} {details['description']} {details['categories']}".lower(),
|
|
'app': app,
|
|
'id': details['id'].lower(),
|
|
'name': details['name'].lower()
|
|
})
|
|
|
|
# Filter and rank results
|
|
filtered_apps = self.rank_search_results(search_term, searchable_items)
|
|
|
|
# Show search results
|
|
self.show_search_results(filtered_apps)
|
|
|
|
def rank_search_results(self, search_term, searchable_items):
|
|
"""Rank search results based on match type and component type filter"""
|
|
exact_id_matches = []
|
|
exact_name_matches = []
|
|
partial_matches = []
|
|
other_matches = []
|
|
|
|
# Get current component type filter
|
|
component_type_filter = self.current_component_type
|
|
if component_type_filter is None:
|
|
component_type_filter = None # Allow all types
|
|
|
|
# Process each item
|
|
for item in searchable_items:
|
|
# Check if component type matches filter
|
|
if component_type_filter and item['app'].get_details()['kind'] != component_type_filter:
|
|
continue
|
|
|
|
# Check exact ID match
|
|
if item['id'] == search_term:
|
|
exact_id_matches.append(item['app'])
|
|
continue
|
|
|
|
# Check exact name match
|
|
if item['name'] == search_term:
|
|
exact_name_matches.append(item['app'])
|
|
continue
|
|
|
|
# Check for partial matches longer than 5 characters
|
|
if len(search_term) > 5:
|
|
if search_term in item['id'] or search_term in item['name']:
|
|
partial_matches.append(item['app'])
|
|
continue
|
|
|
|
# Check for other matches
|
|
if search_term in item['text']:
|
|
other_matches.append(item['app'])
|
|
|
|
# Combine results in order of priority
|
|
return exact_id_matches + exact_name_matches + partial_matches + other_matches
|
|
|
|
def show_search_results(self, apps):
|
|
"""Display search results in the right panel"""
|
|
self.display_apps(apps)
|
|
|
|
def on_category_clicked(self, category, group, *args):
|
|
# Remove active state from all widgets in all groups
|
|
for group_name in self.category_widgets:
|
|
for widget in self.category_widgets[group_name]:
|
|
widget.get_style_context().remove_class("dark-category-button-active")
|
|
|
|
# Add active state to the clicked category
|
|
display_title = self.category_groups[group][category]
|
|
for widget in self.category_widgets[group]:
|
|
if widget.get_children()[0].get_label() == display_title:
|
|
widget.get_style_context().add_class("dark-category-button-active")
|
|
break
|
|
self.current_page = category
|
|
self.current_group = group
|
|
self.update_category_header(category)
|
|
self.update_subcategories_bar(category)
|
|
self.show_category_apps(category)
|
|
|
|
def refresh_current_page(self):
|
|
"""Refresh the currently displayed page"""
|
|
if self.current_page and self.current_group:
|
|
self.on_category_clicked(self.current_page, self.current_group)
|
|
|
|
def update_category_header(self, category):
|
|
"""Update the category header text based on the selected category."""
|
|
display_title = ""
|
|
if category in self.category_groups['system']:
|
|
display_title = self.category_groups['system'][category]
|
|
if category in self.category_groups['collections']:
|
|
display_title = self.category_groups['collections'][category]
|
|
elif category in self.category_groups['categories']:
|
|
display_title = self.category_groups['categories'][category]
|
|
else: # Find the parent category and get the title
|
|
for parent_category, subcategories in self.subcategory_groups.items():
|
|
if category in subcategories:
|
|
display_title = subcategories[category]
|
|
break
|
|
if display_title == "":
|
|
# Fallback if category isn't found
|
|
display_title = category
|
|
self.category_header.set_label(display_title)
|
|
|
|
def create_applications_panel(self, title):
|
|
# Create right panel
|
|
self.right_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
self.right_panel.set_hexpand(True) # Add this line
|
|
self.right_panel.set_vexpand(True) # Add this line
|
|
|
|
# Add category header
|
|
self.category_header = Gtk.Label(label="")
|
|
self.category_header.get_style_context().add_class("panel-header")
|
|
self.category_header.set_hexpand(True)
|
|
self.category_header.set_halign(Gtk.Align.START)
|
|
self.right_panel.pack_start(self.category_header, False, False, 0)
|
|
|
|
# Create subcategories bar (initially hidden)
|
|
self.subcategories_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
self.subcategories_bar.set_hexpand(True)
|
|
self.subcategories_bar.set_spacing(6)
|
|
self.subcategories_bar.set_border_width(6)
|
|
#self.subcategories_bar.get_style_context().add_class("dark-header")
|
|
self.subcategories_bar.set_visible(False)
|
|
self.subcategories_bar.set_halign(Gtk.Align.FILL) # Ensure full width
|
|
self.right_panel.pack_start(self.subcategories_bar, False, False, 0)
|
|
|
|
# Create scrollable area
|
|
scrolled_window = Gtk.ScrolledWindow()
|
|
scrolled_window.set_hexpand(True)
|
|
scrolled_window.set_vexpand(True)
|
|
|
|
# Create container for applications
|
|
self.right_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
self.right_container.set_spacing(6)
|
|
self.right_container.set_border_width(6)
|
|
self.right_container.set_hexpand(True) # Add this line
|
|
self.right_container.set_vexpand(True) # Add this line
|
|
self.right_container.get_style_context().add_class("app-window")
|
|
scrolled_window.add(self.right_container)
|
|
self.right_panel.pack_start(scrolled_window, True, True, 0)
|
|
return self.right_panel
|
|
|
|
def update_subcategories_bar(self, category):
|
|
"""Update the subcategories bar based on the current category."""
|
|
# Clear existing subcategories
|
|
for child in self.subcategories_bar.get_children():
|
|
child.destroy()
|
|
|
|
# Create pan start button
|
|
pan_start = Gtk.Button()
|
|
pan_start_icon = Gio.Icon.new_for_string('pan-start-symbolic')
|
|
pan_start.set_image(Gtk.Image.new_from_gicon(pan_start_icon, Gtk.IconSize.BUTTON))
|
|
pan_start.get_style_context().add_class("dark-category-button")
|
|
pan_start.connect("clicked", self.on_pan_start)
|
|
|
|
# Create scrolled window
|
|
self.scrolled_window = Gtk.ScrolledWindow()
|
|
self.scrolled_window.set_hexpand(True)
|
|
self.scrolled_window.set_vexpand(False)
|
|
self.scrolled_window.set_size_request(-1, 40)
|
|
self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER)
|
|
self.scrolled_window.set_min_content_width(0) # Allow shrinking below content size
|
|
self.scrolled_window.set_max_content_width(-1) # No artificial width limit
|
|
self.scrolled_window.set_overlay_scrolling(False)
|
|
self.scrolled_window.get_style_context().add_class("no-scroll-bars")
|
|
|
|
# Create container for subcategories
|
|
container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
container.set_spacing(6)
|
|
container.set_border_width(6)
|
|
container.set_hexpand(True)
|
|
container.set_halign(Gtk.Align.CENTER)
|
|
container.set_homogeneous(False)
|
|
|
|
# Check if the category has subcategories
|
|
if category in self.subcategory_groups:
|
|
# Add subcategories
|
|
for subcategory, title in self.subcategory_groups[category].items():
|
|
# Create clickable box for subcategory
|
|
subcategory_box = Gtk.EventBox()
|
|
subcategory_box.set_hexpand(False)
|
|
subcategory_box.set_halign(Gtk.Align.START)
|
|
subcategory_box.set_margin_top(2)
|
|
subcategory_box.set_margin_bottom(2)
|
|
|
|
# Create label for subcategory
|
|
subcategory_label = Gtk.Label(label=title)
|
|
subcategory_label.set_halign(Gtk.Align.START)
|
|
subcategory_label.set_hexpand(False)
|
|
subcategory_label.get_style_context().add_class("dark-category-button")
|
|
|
|
# Add label to box
|
|
subcategory_box.add(subcategory_label)
|
|
|
|
# Connect click event
|
|
subcategory_box.connect("button-release-event",
|
|
lambda widget, event, subcat=subcategory:
|
|
self.on_subcategory_clicked(subcat))
|
|
|
|
# Store widget in group
|
|
container.pack_start(subcategory_box, False, False, 0)
|
|
|
|
# Add container to scrolled window
|
|
self.scrolled_window.add(container)
|
|
|
|
# Create pan end button
|
|
pan_end = Gtk.Button()
|
|
pan_end_icon = Gio.Icon.new_for_string('pan-end-symbolic')
|
|
pan_end.set_image(Gtk.Image.new_from_gicon(pan_end_icon, Gtk.IconSize.BUTTON))
|
|
pan_end.get_style_context().add_class("dark-category-button")
|
|
pan_end.connect("clicked", self.on_pan_end)
|
|
|
|
# Show the bar and force a layout update
|
|
self.subcategories_bar.get_style_context().add_class("dark-header")
|
|
self.subcategories_bar.set_visible(True)
|
|
self.subcategories_bar.pack_start(pan_start, False, False, 0)
|
|
self.subcategories_bar.pack_start(self.scrolled_window, True, True, 0)
|
|
self.subcategories_bar.pack_start(pan_end, False, False, 0)
|
|
#self.subcategories_bar.pack_start(container, True, True, 0)
|
|
self.subcategories_bar.queue_resize()
|
|
self.subcategories_bar.show_all()
|
|
else:
|
|
# Check if current category is a subcategory
|
|
is_subcategory = False
|
|
parent_category = None
|
|
for parent, subcategories in self.subcategory_groups.items():
|
|
if category in subcategories:
|
|
is_subcategory = True
|
|
parent_category = parent
|
|
break
|
|
|
|
if is_subcategory:
|
|
# Add parent category and current subcategory
|
|
container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
container.set_spacing(6)
|
|
container.set_border_width(6)
|
|
container.set_hexpand(True)
|
|
container.set_halign(Gtk.Align.CENTER)
|
|
|
|
# Create parent category box
|
|
parent_box = Gtk.EventBox()
|
|
parent_box.set_hexpand(False)
|
|
parent_box.set_halign(Gtk.Align.START)
|
|
parent_box.set_margin_top(2)
|
|
parent_box.set_margin_bottom(2)
|
|
|
|
# Create parent label
|
|
parent_label = Gtk.Label(label=self.category_groups['categories'][parent_category])
|
|
parent_label.set_halign(Gtk.Align.START)
|
|
parent_label.set_hexpand(False)
|
|
parent_label.get_style_context().add_class("dark-category-button")
|
|
|
|
# Add label to box
|
|
parent_box.add(parent_label)
|
|
|
|
# Connect click event
|
|
parent_box.connect("button-release-event",
|
|
lambda widget, event, cat=parent_category, grp='categories':
|
|
self.on_category_clicked(cat, grp))
|
|
|
|
# Add parent box to container
|
|
container.pack_start(parent_box, False, False, 0)
|
|
|
|
# Create current subcategory box
|
|
subcategory_box = Gtk.EventBox()
|
|
subcategory_box.set_hexpand(False)
|
|
subcategory_box.set_halign(Gtk.Align.START)
|
|
subcategory_box.set_margin_top(2)
|
|
subcategory_box.set_margin_bottom(2)
|
|
|
|
# Create subcategory label
|
|
subcategory_label = Gtk.Label(label=self.subcategory_groups[parent_category][category])
|
|
subcategory_label.set_halign(Gtk.Align.START)
|
|
subcategory_label.set_hexpand(False)
|
|
subcategory_label.get_style_context().add_class("dark-category-button")
|
|
|
|
# Add label to box
|
|
subcategory_box.add(subcategory_label)
|
|
|
|
# Connect click event
|
|
subcategory_box.connect("button-release-event",
|
|
lambda widget, event, subcat=category:
|
|
self.on_subcategory_clicked(subcat))
|
|
|
|
# Add subcategory box to container
|
|
container.pack_start(subcategory_box, False, False, 0)
|
|
|
|
# Add container to scrolled window
|
|
self.scrolled_window.add(container)
|
|
self.subcategories_bar.get_style_context().add_class("dark-header")
|
|
# Show the bar and force a layout update
|
|
self.subcategories_bar.set_visible(True)
|
|
self.subcategories_bar.pack_start(self.scrolled_window, True, True, 0)
|
|
#self.subcategories_bar.pack_start(container, True, True, 0)
|
|
self.subcategories_bar.queue_resize()
|
|
self.subcategories_bar.show_all()
|
|
else:
|
|
self.subcategories_bar.get_style_context().remove_class("dark-header")
|
|
# Hide the bar and force a layout update
|
|
self.subcategories_bar.set_visible(False)
|
|
|
|
def on_pan_start(self, button):
|
|
# Get the scrolled window's adjustment
|
|
adjustment = self.scrolled_window.get_hadjustment()
|
|
# Scroll to the left by a page
|
|
adjustment.set_value(adjustment.get_value() - adjustment.get_page_size())
|
|
|
|
def on_pan_end(self, button):
|
|
# Get the scrolled window's adjustment
|
|
adjustment = self.scrolled_window.get_hadjustment()
|
|
# Scroll to the right by a page
|
|
adjustment.set_value(adjustment.get_value() + adjustment.get_page_size())
|
|
|
|
def on_subcategory_clicked(self, subcategory):
|
|
"""Handle subcategory button clicks."""
|
|
# Update the current page to the subcategory
|
|
self.current_page = subcategory
|
|
self.current_group = 'subcategories'
|
|
self.update_category_header(subcategory)
|
|
self.update_subcategories_bar(subcategory)
|
|
self.show_category_apps(subcategory)
|
|
|
|
# Create and connect buttons
|
|
def create_button(self, callback, app, label=None, condition=None):
|
|
"""Create a button with optional visibility condition"""
|
|
button = Gtk.Button()
|
|
if label:
|
|
button = Gtk.Button(label=label)
|
|
button.get_style_context().add_class("app-button")
|
|
if condition is not None:
|
|
if not condition(app):
|
|
return None
|
|
button.connect("clicked", callback, app)
|
|
return button
|
|
|
|
def clear_container(self, container):
|
|
"""Clear all widgets from a container"""
|
|
for child in container.get_children():
|
|
child.destroy()
|
|
|
|
def get_app_priority(self, kind):
|
|
"""Convert AppKind to numeric priority for sorting"""
|
|
priorities = {
|
|
"DESKTOP_APP": 0,
|
|
"ADDON": 1,
|
|
"RUNTIME": 2
|
|
}
|
|
return priorities.get(kind, 3)
|
|
|
|
def show_category_apps(self, category):
|
|
# Initialize apps list
|
|
apps = []
|
|
|
|
# Load system data
|
|
if 'installed' in category:
|
|
apps.extend([app for app in self.installed_results])
|
|
if 'updates' in category:
|
|
apps.extend([app for app in self.updates_results])
|
|
|
|
if ('installed' in category) or ('updates' in category):
|
|
# Sort apps by component type priority
|
|
if apps:
|
|
apps.sort(key=lambda app: self.get_app_priority(app.get_details()['kind']))
|
|
|
|
# Load collections data
|
|
try:
|
|
with open("collections_data.json", 'r', encoding='utf-8') as f:
|
|
collections_data = json.load(f)
|
|
|
|
# Find the specific category in collections data
|
|
category_entry = next((
|
|
entry for entry in collections_data
|
|
if entry['category'] == category
|
|
), None)
|
|
|
|
if category_entry:
|
|
# Get all app IDs in this category
|
|
app_ids_in_category = [
|
|
hit['app_id'] for hit in category_entry['data']['hits']
|
|
]
|
|
|
|
# Filter apps based on presence in category
|
|
apps.extend([
|
|
app for app in self.collection_results
|
|
if app.get_details()['id'] in app_ids_in_category
|
|
])
|
|
else:
|
|
# Fallback to previous behavior if category isn't in collections
|
|
apps.extend([
|
|
app for app in self.collection_results
|
|
if category in app.get_details()['categories']
|
|
])
|
|
|
|
except (IOError, json.JSONDecodeError) as e:
|
|
print(f"Error reading collections data: {str(e)}")
|
|
apps.extend([
|
|
app for app in self.collection_results
|
|
if category in app.get_details()['categories']
|
|
])
|
|
|
|
if 'repositories' in category:
|
|
# Clear existing content
|
|
for child in self.right_container.get_children():
|
|
child.destroy()
|
|
|
|
# Create header bar
|
|
header_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
header_bar.set_hexpand(True)
|
|
header_bar.set_spacing(6)
|
|
header_bar.set_border_width(6)
|
|
|
|
# Create left label
|
|
left_label = Gtk.Label(label="On/Off")
|
|
left_label.get_style_context().add_class("repo-list-header")
|
|
left_label.set_halign(Gtk.Align.START) # Align left
|
|
header_bar.pack_start(left_label, True, True, 0)
|
|
|
|
# Center container to fix "URL" label alignment
|
|
center_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
center_container.set_halign(Gtk.Align.START) # Align left
|
|
|
|
# Create center label
|
|
center_label = Gtk.Label(label="URL")
|
|
center_label.get_style_context().add_class("repo-list-header")
|
|
center_label.set_halign(Gtk.Align.START) # Align center
|
|
|
|
center_container.pack_start(center_label, True, True, 0)
|
|
header_bar.pack_start(center_container, True, True, 0)
|
|
|
|
# Create right label
|
|
right_label = Gtk.Label(label="+/-")
|
|
right_label.get_style_context().add_class("repo-list-header")
|
|
right_label.set_halign(Gtk.Align.END) # Align right
|
|
header_bar.pack_end(right_label, False, False, 0)
|
|
|
|
# Add header bar to container
|
|
self.right_container.pack_start(header_bar, False, False, 0)
|
|
|
|
# Get list of repositories
|
|
repos = fp_turbo.repolist(self.system_mode)
|
|
|
|
# Create a scrolled window for repositories
|
|
scrolled_window = Gtk.ScrolledWindow()
|
|
scrolled_window.set_hexpand(True)
|
|
scrolled_window.set_vexpand(True)
|
|
|
|
# Create container for repositories
|
|
repo_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
repo_container.set_spacing(6)
|
|
repo_container.set_border_width(6)
|
|
|
|
# Add repository button
|
|
add_repo_button = Gtk.Button()
|
|
add_icon = Gio.Icon.new_for_string('list-add')
|
|
add_repo_button.set_image(Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.BUTTON))
|
|
add_repo_button.get_style_context().add_class("dark-install-button")
|
|
add_repo_button.connect("clicked", self.on_add_repo_button_clicked)
|
|
|
|
add_flathub_repo_button = Gtk.Button(label="Add Flathub Repo")
|
|
add_flathub_repo_button.get_style_context().add_class("dark-install-button")
|
|
add_flathub_repo_button.connect("clicked", self.on_add_flathub_repo_button_clicked)
|
|
|
|
add_flathub_beta_repo_button = Gtk.Button(label="Add Flathub Beta Repo")
|
|
add_flathub_beta_repo_button.get_style_context().add_class("dark-install-button")
|
|
add_flathub_beta_repo_button.connect("clicked", self.on_add_flathub_beta_repo_button_clicked)
|
|
|
|
# Check for existing Flathub repositories and disable buttons accordingly
|
|
flathub_url = "https://dl.flathub.org/repo/"
|
|
flathub_beta_url = "https://dl.flathub.org/beta-repo/"
|
|
|
|
existing_urls = [repo.get_url().rstrip('/') for repo in repos]
|
|
add_flathub_repo_button.set_sensitive(flathub_url.rstrip('/') not in existing_urls)
|
|
add_flathub_beta_repo_button.set_sensitive(flathub_beta_url.rstrip('/') not in existing_urls)
|
|
|
|
# Add repositories to container
|
|
for repo in repos:
|
|
repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
repo_box.set_spacing(6)
|
|
repo_box.set_hexpand(True)
|
|
|
|
# Create checkbox
|
|
if self.system_mode:
|
|
checkbox = Gtk.CheckButton(label=f"{repo.get_name()} (System)")
|
|
else:
|
|
checkbox = Gtk.CheckButton(label=f"{repo.get_name()} (User)")
|
|
checkbox.set_active(not repo.get_disabled())
|
|
if not repo.get_disabled():
|
|
checkbox.get_style_context().remove_class("dim-label")
|
|
else:
|
|
checkbox.get_style_context().add_class("dim-label")
|
|
checkbox.connect("toggled", self.on_repo_toggled, repo)
|
|
checkbox_url_label = Gtk.Label(label=repo.get_url())
|
|
checkbox_url_label.set_halign(Gtk.Align.START)
|
|
checkbox_url_label.set_hexpand(True)
|
|
checkbox_url_label.get_style_context().add_class("dim-label")
|
|
|
|
# Create delete button
|
|
delete_button = Gtk.Button()
|
|
delete_icon = Gio.Icon.new_for_string('list-remove')
|
|
delete_button.set_image(Gtk.Image.new_from_gicon(delete_icon, Gtk.IconSize.BUTTON))
|
|
delete_button.get_style_context().add_class("destructive-action")
|
|
delete_button.connect("clicked", self.on_repo_delete, repo)
|
|
|
|
# Add widgets to box
|
|
repo_box.pack_start(checkbox, False, False, 0)
|
|
repo_box.pack_start(checkbox_url_label, False, False, 0)
|
|
repo_box.pack_end(delete_button, False, False, 0)
|
|
|
|
# Add box to container
|
|
repo_container.pack_start(repo_box, False, False, 0)
|
|
|
|
repo_container.pack_start(add_repo_button, False, False, 0)
|
|
repo_container.pack_start(add_flathub_repo_button, False, False, 0)
|
|
repo_container.pack_start(add_flathub_beta_repo_button, False, False, 0)
|
|
|
|
# Add container to scrolled window
|
|
scrolled_window.add(repo_container)
|
|
self.right_container.pack_start(scrolled_window, True, True, 0)
|
|
|
|
self.right_container.show_all()
|
|
return
|
|
|
|
# Apply component type filter if set
|
|
component_type_filter = self.current_component_type
|
|
if component_type_filter:
|
|
apps = [app for app in apps if app.get_details()['kind'] == component_type_filter]
|
|
|
|
self.display_apps(apps)
|
|
|
|
def create_scaled_icon(self, icon, is_themed=False):
|
|
if is_themed:
|
|
# For themed icons, create a pixbuf directly using the icon theme
|
|
icon_theme = Gtk.IconTheme.get_default()
|
|
pb = icon_theme.load_icon(icon.get_names()[0], 64, Gtk.IconLookupFlags.FORCE_SIZE)
|
|
else:
|
|
# For file-based icons
|
|
pb = GdkPixbuf.Pixbuf.new_from_file(icon)
|
|
|
|
# Scale to 64x64 using high-quality interpolation
|
|
scaled_pb = pb.scale_simple(
|
|
64, 64, # New dimensions
|
|
GdkPixbuf.InterpType.BILINEAR # High-quality scaling
|
|
)
|
|
|
|
# Create the image widget from the scaled pixbuf
|
|
return Gtk.Image.new_from_pixbuf(scaled_pb)
|
|
|
|
def display_apps(self, apps):
|
|
for child in self.right_container.get_children():
|
|
child.destroy()
|
|
# Create a dictionary to group apps by ID
|
|
apps_by_id = {}
|
|
for app in apps:
|
|
details = app.get_details()
|
|
app_id = details['id']
|
|
|
|
# If app_id isn't in dictionary, add it
|
|
if app_id not in apps_by_id:
|
|
apps_by_id[app_id] = {
|
|
'app': app,
|
|
'repos': set()
|
|
}
|
|
|
|
# Add repository to the set
|
|
repo_name = details.get('repo', 'unknown')
|
|
apps_by_id[app_id]['repos'].add(repo_name)
|
|
|
|
# Display each unique application
|
|
for app_id, app_data in apps_by_id.items():
|
|
app = app_data['app']
|
|
details = app.get_details()
|
|
is_installed = False
|
|
for package in self.installed_results:
|
|
if details['id'] == package.id:
|
|
is_installed = True
|
|
break
|
|
is_updatable = False
|
|
for package in self.updates_results:
|
|
if details['id'] == package.id:
|
|
is_updatable = True
|
|
break
|
|
|
|
# Create application container
|
|
app_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
app_container.set_spacing(12)
|
|
app_container.set_margin_top(6)
|
|
app_container.set_margin_bottom(6)
|
|
|
|
# Add icon placeholder
|
|
icon_box = Gtk.Box()
|
|
icon_box.set_size_request(88, -1)
|
|
|
|
# Create and add the icon
|
|
app_icon = Gio.Icon.new_for_string('package-x-generic-symbolic')
|
|
icon_widget = self.create_scaled_icon(app_icon, is_themed=True)
|
|
|
|
if details['icon_filename']:
|
|
if Path(details['icon_path_128'] + "/" + details['icon_filename']).exists():
|
|
icon_widget = self.create_scaled_icon(f"{details['icon_path_128']}/{details['icon_filename']}", is_themed=False)
|
|
|
|
icon_widget.set_size_request(64, 64)
|
|
icon_box.pack_start(icon_widget, True, True, 0)
|
|
|
|
# Create right side layout for text
|
|
right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
right_box.set_spacing(4)
|
|
right_box.set_hexpand(True)
|
|
|
|
# Add title
|
|
title_label = Gtk.Label(label=details['name'])
|
|
title_label.get_style_context().add_class("app-list-header")
|
|
title_label.set_halign(Gtk.Align.START)
|
|
title_label.set_yalign(0.5) # Use yalign instead of valign
|
|
title_label.set_hexpand(True)
|
|
|
|
# Add repository labels
|
|
kind_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
kind_box.set_spacing(4)
|
|
kind_box.set_halign(Gtk.Align.START)
|
|
kind_box.set_valign(Gtk.Align.START)
|
|
|
|
kind_label = Gtk.Label(label=details['kind'])
|
|
kind_label.get_style_context().add_class("item-repo-label")
|
|
kind_label.set_halign(Gtk.Align.START)
|
|
kind_box.pack_end(kind_label, False, False, 0)
|
|
|
|
# Add repository labels
|
|
repo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
repo_box.set_spacing(4)
|
|
repo_box.set_halign(Gtk.Align.END)
|
|
repo_box.set_valign(Gtk.Align.END)
|
|
|
|
# Add repository labels
|
|
for repo in sorted(app_data['repos']):
|
|
repo_label = Gtk.Label(label=repo)
|
|
repo_label.get_style_context().add_class("item-repo-label")
|
|
repo_label.set_halign(Gtk.Align.END)
|
|
repo_box.pack_end(repo_label, False, False, 0)
|
|
|
|
# Add summary
|
|
desc_label = Gtk.Label(label=details['summary'])
|
|
desc_label.set_halign(Gtk.Align.START)
|
|
desc_label.set_yalign(0.5)
|
|
desc_label.set_hexpand(True)
|
|
desc_label.set_line_wrap(True)
|
|
desc_label.set_line_wrap_mode(Gtk.WrapMode.WORD)
|
|
desc_label.get_style_context().add_class("dim-label")
|
|
desc_label.get_style_context().add_class("app-list-summary")
|
|
|
|
# Create buttons box
|
|
buttons_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
buttons_box.set_spacing(6)
|
|
buttons_box.set_margin_top(4)
|
|
buttons_box.set_halign(Gtk.Align.END)
|
|
|
|
# Install/Remove button
|
|
if is_installed:
|
|
button = self.create_button(
|
|
self.on_remove_clicked,
|
|
app,
|
|
None,
|
|
condition=lambda x: True
|
|
)
|
|
add_rm_icon = "list-remove"
|
|
add_rm_style = "dark-remove-button"
|
|
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)
|
|
|
|
# App options button
|
|
if is_installed:
|
|
button = self.create_button(
|
|
self.on_app_options_clicked,
|
|
app,
|
|
None,
|
|
condition=lambda x: True
|
|
)
|
|
add_options_icon = "system-run"
|
|
add_options_style = "dark-remove-button"
|
|
|
|
if button:
|
|
use_icon = Gio.Icon.new_for_string(add_options_icon)
|
|
button.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
|
|
button.get_style_context().add_class(add_options_style)
|
|
buttons_box.pack_end(button, False, False, 0)
|
|
|
|
# Add Update button if available
|
|
if is_updatable:
|
|
update_button = self.create_button(
|
|
self.on_update_clicked,
|
|
app,
|
|
None,
|
|
condition=lambda x: True
|
|
)
|
|
if update_button:
|
|
update_icon = Gio.Icon.new_for_string('system-software-update-symbolic')
|
|
update_button.set_image(Gtk.Image.new_from_gicon(update_icon, Gtk.IconSize.BUTTON))
|
|
update_button.get_style_context().add_class("dark-install-button")
|
|
buttons_box.pack_end(update_button, False, False, 0)
|
|
|
|
# Details button
|
|
details_btn = self.create_button(
|
|
self.on_details_clicked,
|
|
app,
|
|
None
|
|
)
|
|
if details_btn:
|
|
details_icon = Gio.Icon.new_for_string('question')
|
|
details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.BUTTON))
|
|
details_btn.get_style_context().add_class("dark-install-button")
|
|
buttons_box.pack_end(details_btn, False, False, 0)
|
|
|
|
# Donate button with condition
|
|
donate_btn = self.create_button(
|
|
self.on_donate_clicked,
|
|
app,
|
|
None,
|
|
condition=lambda x: x.get_details().get('urls', {}).get('donation', '')
|
|
)
|
|
if donate_btn:
|
|
donate_icon = Gio.Icon.new_for_string('donate')
|
|
donate_btn.set_image(Gtk.Image.new_from_gicon(donate_icon, Gtk.IconSize.BUTTON))
|
|
donate_btn.get_style_context().add_class("dark-install-button")
|
|
buttons_box.pack_end(donate_btn, False, False, 0)
|
|
|
|
# Add widgets to right box
|
|
right_box.pack_start(title_label, False, False, 0)
|
|
right_box.pack_start(kind_box, False, False, 0)
|
|
right_box.pack_start(repo_box, False, False, 0)
|
|
right_box.pack_start(desc_label, False, False, 0)
|
|
right_box.pack_start(buttons_box, False, True, 0)
|
|
|
|
# Add to container
|
|
app_container.pack_start(icon_box, False, False, 0)
|
|
app_container.pack_start(right_box, True, True, 0)
|
|
self.right_container.pack_start(app_container, False, False, 0)
|
|
self.right_container.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
|
|
|
|
self.right_container.show_all() # Show all widgets after adding them
|
|
|
|
def show_waiting_dialog(self, message="Please wait while task is running..."):
|
|
"""Show a modal dialog with a spinner"""
|
|
self.waiting_dialog = Gtk.Dialog(
|
|
title="Running Task...",
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
|
|
# Create spinner
|
|
self.spinner = Gtk.Spinner()
|
|
self.spinner.start()
|
|
|
|
# Add content
|
|
box = self.waiting_dialog.get_content_area()
|
|
box.set_spacing(12)
|
|
box.set_border_width(12)
|
|
|
|
# Add label and spinner
|
|
box.pack_start(Gtk.Label(label=message), False, False, 0)
|
|
box.pack_start(self.spinner, False, False, 0)
|
|
|
|
# Show dialog
|
|
self.waiting_dialog.show_all()
|
|
|
|
def on_install_clicked(self, button=None, app=None):
|
|
"""Handle the Install button click with installation options"""
|
|
id=""
|
|
if button and app:
|
|
details = app.get_details()
|
|
title=f"Install {details['name']}?"
|
|
label=f"Install: {details['id']}?"
|
|
id=details['id']
|
|
# this is a stupid workaround for our button creator
|
|
# so that we can use the same function in drag and drop
|
|
# which of course does not have a button object
|
|
elif app and not button:
|
|
title=f"Install {app}?"
|
|
label=f"Install: {app}?"
|
|
else:
|
|
message_type=Gtk.MessageType.ERROR
|
|
finished_dialog = Gtk.MessageDialog(
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
message_type=message_type,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text="Error: No app specified"
|
|
)
|
|
finished_dialog.run()
|
|
finished_dialog.destroy()
|
|
return
|
|
# Create dialog
|
|
dialog = Gtk.Dialog(
|
|
title=title,
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
# Add buttons using the new method
|
|
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
|
dialog.add_button("Install", Gtk.ResponseType.OK)
|
|
|
|
# Create content area
|
|
content_area = dialog.get_content_area()
|
|
content_area.set_spacing(12)
|
|
content_area.set_border_width(12)
|
|
|
|
# Create repository dropdown
|
|
repo_combo = Gtk.ComboBoxText()
|
|
|
|
content_area.pack_start(Gtk.Label(label=label), False, False, 0)
|
|
|
|
# Search for available repositories containing this app
|
|
searcher = fp_turbo.get_reposearcher(self.system_mode)
|
|
if self.system_mode is False:
|
|
content_area.pack_start(Gtk.Label(label="Installation Type: User"), False, False, 0)
|
|
else:
|
|
content_area.pack_start(Gtk.Label(label="Installation Type: System"), False, False, 0)
|
|
|
|
# Populate repository dropdown
|
|
if button and app:
|
|
available_repos = set()
|
|
repos = fp_turbo.repolist(self.system_mode)
|
|
for repo in repos:
|
|
if not repo.get_disabled():
|
|
search_results = searcher.search_flatpak(id, repo.get_name())
|
|
if search_results:
|
|
available_repos.add(repo)
|
|
|
|
# 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()
|
|
else:
|
|
# Show dialog
|
|
dialog.show_all()
|
|
|
|
# Run dialog
|
|
response = dialog.run()
|
|
if response == Gtk.ResponseType.OK:
|
|
selected_repo = None
|
|
if button and app:
|
|
selected_repo = repo_combo.get_active_text()
|
|
# Perform installation
|
|
def perform_installation():
|
|
# Show waiting dialog
|
|
GLib.idle_add(self.show_waiting_dialog)
|
|
if button and app:
|
|
success, message = fp_turbo.install_flatpak(app, selected_repo, self.system_mode)
|
|
else:
|
|
success, message = fp_turbo.install_flatpakref(app, self.system_mode)
|
|
GLib.idle_add(lambda: self.on_task_complete(dialog, success, message))
|
|
# Start spinner and begin installation
|
|
thread = threading.Thread(target=perform_installation)
|
|
thread.daemon = True # Allow program to exit even if thread is still running
|
|
thread.start()
|
|
dialog.destroy()
|
|
|
|
def on_task_complete(self, dialog, success, message):
|
|
"""Handle tasl completion"""
|
|
# Update UI
|
|
message_type=Gtk.MessageType.INFO
|
|
if not success:
|
|
message_type=Gtk.MessageType.ERROR
|
|
if message:
|
|
finished_dialog = Gtk.MessageDialog(
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
message_type=message_type,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text=message
|
|
)
|
|
finished_dialog.run()
|
|
finished_dialog.destroy()
|
|
self.refresh_local()
|
|
self.refresh_current_page()
|
|
self.waiting_dialog.destroy()
|
|
|
|
|
|
def on_remove_clicked(self, button, app):
|
|
"""Handle the Remove button click with removal options"""
|
|
details = app.get_details()
|
|
|
|
# Create dialog
|
|
dialog = Gtk.Dialog(
|
|
title=f"Remove {details['name']}?",
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
# Add buttons using the new method
|
|
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
|
dialog.add_button("Remove", Gtk.ResponseType.OK)
|
|
|
|
# Create content area
|
|
content_area = dialog.get_content_area()
|
|
content_area.set_spacing(12)
|
|
content_area.set_border_width(12)
|
|
|
|
content_area.pack_start(Gtk.Label(label=f"Remove: {details['id']}?"), False, False, 0)
|
|
|
|
# Show dialog
|
|
dialog.show_all()
|
|
|
|
# Run dialog
|
|
response = dialog.run()
|
|
if response == Gtk.ResponseType.OK:
|
|
# Perform Removal
|
|
def perform_removal():
|
|
# Show waiting dialog
|
|
GLib.idle_add(self.show_waiting_dialog, "Removing package...")
|
|
|
|
success, message = fp_turbo.remove_flatpak(app, None, self.system_mode)
|
|
|
|
# Update UI on main thread
|
|
GLib.idle_add(lambda: self.on_task_complete(dialog, success, message))
|
|
|
|
# Start spinner and begin installation
|
|
thread = threading.Thread(target=perform_removal)
|
|
thread.daemon = True # Allow program to exit even if thread is still running
|
|
thread.start()
|
|
|
|
dialog.destroy()
|
|
|
|
def _add_bus_section(self, app_id, app, listbox, section_title, perm_type):
|
|
"""Helper method to add System Bus or Session Bus section"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Get permissions
|
|
success, perms = fp_turbo.list_other_perm_values(app_id, perm_type, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
|
|
# Add Talks section
|
|
talks_row = Gtk.ListBoxRow(selectable=False)
|
|
talks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
talks_row.add(talks_box)
|
|
|
|
talks_header = Gtk.Label(label="Talks", xalign=0)
|
|
talks_box.pack_start(talks_header, False, False, 0)
|
|
|
|
# Add separator between header and paths
|
|
talks_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
|
|
False, False, 0)
|
|
|
|
# Add talk paths
|
|
for path in perms["paths"]:
|
|
if "talk" in path:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path.split("=")[0], xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
talks_box.add(row)
|
|
|
|
listbox.add(talks_row)
|
|
|
|
# Add Owns section
|
|
owns_row = Gtk.ListBoxRow(selectable=False)
|
|
owns_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
owns_row.add(owns_box)
|
|
|
|
owns_header = Gtk.Label(label="Owns", xalign=0)
|
|
owns_box.pack_start(owns_header, False, False, 0)
|
|
|
|
# Add separator between header and paths
|
|
owns_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
|
|
False, False, 0)
|
|
|
|
# Add own paths
|
|
for path in perms["paths"]:
|
|
if "own" in path:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path.split("=")[0], xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
owns_box.add(row)
|
|
|
|
owns_row.show_all()
|
|
listbox.add(owns_row)
|
|
|
|
# Add add button
|
|
add_path_row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
add_path_row.add(hbox)
|
|
|
|
btn = Gtk.Button(label="Add Path")
|
|
btn.connect("clicked", self._on_add_path, app_id, app, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(add_path_row)
|
|
|
|
def _add_path_section(self, app_id, app, listbox, section_title, perm_type):
|
|
"""Helper method to add sections with paths (Persistent, Environment)"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Get permissions
|
|
if perm_type == "persistent":
|
|
success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode)
|
|
else:
|
|
success, perms = fp_turbo.list_other_perm_values(app_id, perm_type, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
|
|
# Add paths
|
|
for path in perms["paths"]:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._on_remove_path, app_id, app, path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
# Add add button
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
btn = Gtk.Button(label="Add Path")
|
|
btn.connect("clicked", self._on_add_path, app_id, app, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
def _add_filesystem_section(self, app_id, app, listbox, section_title):
|
|
"""Helper method to add the Filesystems section"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Get filesystem permissions
|
|
success, perms = fp_turbo.list_file_perms(app_id, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": [], "special_paths": []}
|
|
|
|
# Add special paths as toggles
|
|
special_paths = [
|
|
("All user files", "home", "Access to all user files"),
|
|
("All system files", "host", "Access to all system files"),
|
|
("All system libraries, executables and static data", "host-os", "Access to system libraries and executables"),
|
|
("All system configurations", "host-etc", "Access to system configurations")
|
|
]
|
|
|
|
for display_text, option, description in special_paths:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=display_text, xalign=0)
|
|
desc = Gtk.Label(label=description, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
vbox.pack_start(desc, True, True, 0)
|
|
|
|
switch = Gtk.Switch()
|
|
switch.props.valign = Gtk.Align.CENTER
|
|
switch.set_active(option in perms["special_paths"])
|
|
switch.set_sensitive(True)
|
|
switch.connect("state-set", self._on_switch_toggled, app_id, "filesystems", option)
|
|
hbox.pack_end(switch, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
# Add normal paths with remove buttons
|
|
for path in perms["paths"]:
|
|
if path != "":
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._on_remove_path, app_id, app, path)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
# Add add button
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
btn = Gtk.Button(label="Add Path")
|
|
btn.connect("clicked", self._on_add_path, app_id, app)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
|
|
def on_app_options_clicked(self, button, app):
|
|
"""Handle the app options click"""
|
|
details = app.get_details()
|
|
app_id = details['id']
|
|
|
|
# Create window (as before)
|
|
self.options_window = Gtk.Window(title=f"{details['name']} Settings")
|
|
self.options_window.set_default_size(600, 800)
|
|
|
|
# Set subtitle
|
|
header_bar = Gtk.HeaderBar(title=f"{details['name']} Settings",
|
|
subtitle="List of resources selectively granted to the application")
|
|
header_bar.set_show_close_button(True)
|
|
self.options_window.set_titlebar(header_bar)
|
|
|
|
# Create main container with padding
|
|
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
box_outer.set_border_width(20)
|
|
self.options_window.add(box_outer)
|
|
|
|
# Create scrolled window for content
|
|
scrolled = Gtk.ScrolledWindow()
|
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
# Create list box for options
|
|
listbox = Gtk.ListBox()
|
|
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
listbox.get_style_context().add_class("permissions-window")
|
|
|
|
# Add Portals section first
|
|
self._add_section(app_id, listbox, "Portals", section_options=[
|
|
("Background", "background", "Can run in the background"),
|
|
("Notifications", "notifications", "Can send notifications"),
|
|
("Microphone", "microphone", "Can listen to your microphone"),
|
|
("Speakers", "speakers", "Can play sounds to your speakers"),
|
|
("Camera", "camera", "Can record videos with your camera"),
|
|
("Location", "location", "Can access your location")
|
|
])
|
|
|
|
# Add other sections with correct permission types
|
|
self._add_section(app_id, listbox, "Shared", "shared", [
|
|
("Network", "network", "Can communicate over network"),
|
|
("Inter-process communications", "ipc", "Can communicate with other applications")
|
|
])
|
|
|
|
self._add_section(app_id, listbox, "Sockets", "sockets", [
|
|
("X11 windowing system", "x11", "Can access X11 display server"),
|
|
("Wayland windowing system", "wayland", "Can access Wayland display server"),
|
|
("Fallback to X11 windowing system", "fallback-x11", "Can fallback to X11 if Wayland unavailable"),
|
|
("PulseAudio sound server", "pulseaudio", "Can access PulseAudio sound system"),
|
|
("D-Bus session bus", "session-bus", "Can communicate with session D-Bus"),
|
|
("D-Bus system bus", "system-bus", "Can communicate with system D-Bus"),
|
|
("Secure Shell agent", "ssh-auth", "Can access SSH authentication agent"),
|
|
("Smart cards", "pcsc", "Can access smart card readers"),
|
|
("Printing system", "cups", "Can access printing subsystem"),
|
|
("GPG-Agent directories", "gpg-agent", "Can access GPG keyring"),
|
|
("Inherit Wayland socket", "inherit-wayland-socket", "Can inherit existing Wayland socket")
|
|
])
|
|
|
|
self._add_section(app_id, listbox, "Devices", "devices", [
|
|
("GPU Acceleration", "dri", "Can use hardware graphics acceleration"),
|
|
("Input devices", "input", "Can access input devices"),
|
|
("Virtualization", "kvm", "Can access virtualization services"),
|
|
("Shared memory", "shm", "Can use shared memory"),
|
|
("All devices (e.g. webcam)", "all", "Can access all device files")
|
|
])
|
|
|
|
self._add_section(app_id, listbox, "Features", "features", [
|
|
("Development syscalls", "devel", "Can perform development operations"),
|
|
("Programs from other architectures", "multiarch", "Can execute programs from other architectures"),
|
|
("Bluetooth", "bluetooth", "Can access Bluetooth hardware"),
|
|
("Controller Area Network bus", "canbus", "Can access CAN bus"),
|
|
("Application Shared Memory", "per-app-dev-shm", "Can use shared memory for IPC")
|
|
])
|
|
|
|
# Add Filesystems section
|
|
self._add_filesystem_section(app_id, app, listbox, "Filesystems")
|
|
self._add_path_section(app_id, app, listbox, "Persistent", "persistent")
|
|
self._add_path_section(app_id, app, listbox, "Environment", "environment")
|
|
self._add_bus_section(app_id, app, listbox, "System Bus", "system_bus")
|
|
self._add_bus_section(app_id, app, listbox, "Session Bus", "session_bus")
|
|
|
|
# Add widgets to container
|
|
box_outer.pack_start(scrolled, True, True, 0)
|
|
scrolled.add(listbox)
|
|
|
|
# Connect destroy signal
|
|
self.options_window.connect("destroy", lambda w: w.destroy())
|
|
|
|
# Show window
|
|
self.options_window.show_all()
|
|
|
|
def _add_section(self, app_id, listbox, section_title, perm_type=None, section_options=None):
|
|
"""Helper method to add a section with multiple options"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Handle portal permissions specially
|
|
if section_title == "Portals":
|
|
success, perms = fp_turbo.portal_get_app_permissions(app_id)
|
|
if not success:
|
|
perms = {}
|
|
elif section_title in ["Persistent", "Environment", "System Bus", "Session Bus"]:
|
|
success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
else:
|
|
success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
|
|
if section_options:
|
|
# Add options
|
|
for display_text, option, description in section_options:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=display_text, xalign=0)
|
|
desc = Gtk.Label(label=description, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
vbox.pack_start(desc, True, True, 0)
|
|
|
|
switch = Gtk.Switch()
|
|
switch.props.valign = Gtk.Align.CENTER
|
|
|
|
# Handle portal permissions differently
|
|
if section_title == "Portals":
|
|
if option in perms:
|
|
switch.set_active(perms[option] == 'yes')
|
|
switch.set_sensitive(True)
|
|
else:
|
|
switch.set_sensitive(False)
|
|
else:
|
|
switch.set_active(option in [p.lower() for p in perms["paths"]])
|
|
switch.set_sensitive(True)
|
|
|
|
switch.connect("state-set", self._on_switch_toggled, app_id, perm_type, option)
|
|
hbox.pack_end(switch, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
def _on_switch_toggled(self, switch, state, app_id, perm_type, option):
|
|
"""Handle switch toggle events"""
|
|
if perm_type is None: # Portal section
|
|
success, message = fp_turbo.portal_set_app_permissions(
|
|
option.lower(),
|
|
app_id,
|
|
"yes" if state else "no"
|
|
)
|
|
else:
|
|
success, message = fp_turbo.toggle_other_perms(
|
|
app_id,
|
|
perm_type,
|
|
option.lower(),
|
|
state,
|
|
self.system_mode
|
|
)
|
|
|
|
if not success:
|
|
switch.set_active(not state)
|
|
print(f"Error: {message}")
|
|
|
|
def _on_remove_path(self, button, app_id, app, path, perm_type=None):
|
|
"""Handle remove path button click"""
|
|
if perm_type:
|
|
if perm_type == "persistent":
|
|
success, message = fp_turbo.remove_file_permissions(
|
|
app_id,
|
|
path,
|
|
"persistent",
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.remove_permission_value(
|
|
app_id,
|
|
perm_type,
|
|
path,
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.remove_file_permissions(
|
|
app_id,
|
|
path,
|
|
"filesystems",
|
|
self.system_mode
|
|
)
|
|
if success:
|
|
# Refresh the current window
|
|
self.options_window.destroy()
|
|
self.on_app_options_clicked(None, app)
|
|
|
|
def _on_add_path(self, button, app_id, app, perm_type=None):
|
|
"""Handle add path button click"""
|
|
dialog = Gtk.Dialog(
|
|
title="Add Filesystem Path",
|
|
parent=self.options_window,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
|
|
# Add buttons separately
|
|
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
|
dialog.add_button("Add", Gtk.ResponseType.OK)
|
|
|
|
entry = Gtk.Entry()
|
|
entry.set_placeholder_text("Enter filesystem path")
|
|
dialog.vbox.pack_start(entry, True, True, 0)
|
|
dialog.show_all()
|
|
|
|
response = dialog.run()
|
|
if response == Gtk.ResponseType.OK:
|
|
path = entry.get_text()
|
|
if perm_type:
|
|
if perm_type == "persistent":
|
|
success, message = fp_turbo.add_file_permissions(
|
|
app_id,
|
|
path,
|
|
"persistent",
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.add_permission_value(
|
|
app_id,
|
|
perm_type,
|
|
path,
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.add_file_permissions(
|
|
app_id,
|
|
path,
|
|
"filesystems",
|
|
self.system_mode
|
|
)
|
|
if success:
|
|
# Refresh the current window
|
|
self.options_window.destroy()
|
|
self.on_app_options_clicked(None, app)
|
|
message_type = Gtk.MessageType.INFO
|
|
else:
|
|
message_type = Gtk.MessageType.ERROR
|
|
if message:
|
|
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=message
|
|
)
|
|
error_dialog.run()
|
|
error_dialog.destroy()
|
|
dialog.destroy()
|
|
|
|
def _add_option(self, parent_box, label_text, description):
|
|
"""Helper method to add an individual option"""
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=label_text, xalign=0)
|
|
desc = Gtk.Label(label=description, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
vbox.pack_start(desc, True, True, 0)
|
|
|
|
switch = Gtk.Switch()
|
|
switch.props.valign = Gtk.Align.CENTER
|
|
hbox.pack_end(switch, False, True, 0)
|
|
|
|
parent_box.add(row)
|
|
return row, switch
|
|
|
|
def _global_add_bus_section(self, listbox, section_title, perm_type):
|
|
"""Helper method to add System Bus or Session Bus section"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Get permissions
|
|
success, perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
|
|
# Add Talks section
|
|
talks_row = Gtk.ListBoxRow(selectable=False)
|
|
talks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
talks_row.add(talks_box)
|
|
|
|
talks_header = Gtk.Label(label="Talks", xalign=0)
|
|
talks_box.pack_start(talks_header, False, False, 0)
|
|
|
|
# Add separator between header and paths
|
|
talks_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
|
|
False, False, 0)
|
|
|
|
# Add talk paths
|
|
for path in perms["paths"]:
|
|
if "talk" in path:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path.split("=")[0], xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._global_on_remove_path, path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
talks_box.add(row)
|
|
|
|
listbox.add(talks_row)
|
|
|
|
# Add Owns section
|
|
owns_row = Gtk.ListBoxRow(selectable=False)
|
|
owns_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
owns_row.add(owns_box)
|
|
|
|
owns_header = Gtk.Label(label="Owns", xalign=0)
|
|
owns_box.pack_start(owns_header, False, False, 0)
|
|
|
|
# Add separator between header and paths
|
|
owns_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
|
|
False, False, 0)
|
|
|
|
# Add own paths
|
|
for path in perms["paths"]:
|
|
if "own" in path:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path.split("=")[0], xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._on_global_remove_path, path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
owns_box.add(row)
|
|
|
|
owns_row.show_all()
|
|
listbox.add(owns_row)
|
|
|
|
# Add add button
|
|
add_path_row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
add_path_row.add(hbox)
|
|
|
|
btn = Gtk.Button(label="Add Path")
|
|
btn.connect("clicked", self._global_on_add_path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(add_path_row)
|
|
|
|
def _global_add_path_section(self, listbox, section_title, perm_type):
|
|
"""Helper method to add sections with paths (Persistent, Environment)"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Get permissions
|
|
if perm_type == "persistent":
|
|
success, perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode)
|
|
else:
|
|
success, perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
|
|
# Add paths
|
|
for path in perms["paths"]:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._global_on_remove_path, path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
# Add add button
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
btn = Gtk.Button(label="Add Path")
|
|
btn.connect("clicked", self._global_on_add_path, perm_type)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
def _global_add_filesystem_section(self, listbox, section_title):
|
|
"""Helper method to add the Filesystems section"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
# Get filesystem permissions
|
|
success, perms = fp_turbo.global_list_file_perms(True, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": [], "special_paths": []}
|
|
|
|
# Add special paths as toggles
|
|
special_paths = [
|
|
("All user files", "home", "Access to all user files"),
|
|
("All system files", "host", "Access to all system files"),
|
|
("All system libraries, executables and static data", "host-os", "Access to system libraries and executables"),
|
|
("All system configurations", "host-etc", "Access to system configurations")
|
|
]
|
|
|
|
for display_text, option, description in special_paths:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=display_text, xalign=0)
|
|
desc = Gtk.Label(label=description, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
vbox.pack_start(desc, True, True, 0)
|
|
|
|
switch = Gtk.Switch()
|
|
switch.props.valign = Gtk.Align.CENTER
|
|
switch.set_active(option in perms["special_paths"])
|
|
switch.set_sensitive(True)
|
|
switch.connect("state-set", self._global_on_switch_toggled, "filesystems", option)
|
|
hbox.pack_end(switch, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
# Add normal paths with remove buttons
|
|
for path in perms["paths"]:
|
|
if path != "":
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=path, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
|
|
btn = Gtk.Button(label="Remove")
|
|
btn.connect("clicked", self._global_on_remove_path, path)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
# Add add button
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
btn = Gtk.Button(label="Add Path")
|
|
btn.connect("clicked", self._global_on_add_path)
|
|
hbox.pack_end(btn, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
|
|
def global_on_options_clicked(self, button):
|
|
"""Handle the app options click"""
|
|
|
|
# Create window (as before)
|
|
self.global_options_window = Gtk.Window(title="Global Setting Overrides")
|
|
self.global_options_window.set_default_size(600, 800)
|
|
|
|
# Set subtitle
|
|
header_bar = Gtk.HeaderBar(title="Global Setting Overrides",
|
|
subtitle="Override list of resources selectively granted to applications")
|
|
header_bar.set_show_close_button(True)
|
|
self.global_options_window.set_titlebar(header_bar)
|
|
|
|
# Create main container with padding
|
|
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
box_outer.set_border_width(20)
|
|
self.global_options_window.add(box_outer)
|
|
|
|
# Create scrolled window for content
|
|
scrolled = Gtk.ScrolledWindow()
|
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
# Create list box for options
|
|
listbox = Gtk.ListBox()
|
|
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
listbox.get_style_context().add_class("permissions-window")
|
|
|
|
# No portals section. Portals are only handled on per-user basis.
|
|
|
|
# Add other sections with correct permission types
|
|
self._global_add_section(listbox, "Shared", "shared", [
|
|
("Network", "network", "Can communicate over network"),
|
|
("Inter-process communications", "ipc", "Can communicate with other applications")
|
|
])
|
|
|
|
self._global_add_section(listbox, "Sockets", "sockets", [
|
|
("X11 windowing system", "x11", "Can access X11 display server"),
|
|
("Wayland windowing system", "wayland", "Can access Wayland display server"),
|
|
("Fallback to X11 windowing system", "fallback-x11", "Can fallback to X11 if Wayland unavailable"),
|
|
("PulseAudio sound server", "pulseaudio", "Can access PulseAudio sound system"),
|
|
("D-Bus session bus", "session-bus", "Can communicate with session D-Bus"),
|
|
("D-Bus system bus", "system-bus", "Can communicate with system D-Bus"),
|
|
("Secure Shell agent", "ssh-auth", "Can access SSH authentication agent"),
|
|
("Smart cards", "pcsc", "Can access smart card readers"),
|
|
("Printing system", "cups", "Can access printing subsystem"),
|
|
("GPG-Agent directories", "gpg-agent", "Can access GPG keyring"),
|
|
("Inherit Wayland socket", "inherit-wayland-socket", "Can inherit existing Wayland socket")
|
|
])
|
|
|
|
self._global_add_section(listbox, "Devices", "devices", [
|
|
("GPU Acceleration", "dri", "Can use hardware graphics acceleration"),
|
|
("Input devices", "input", "Can access input devices"),
|
|
("Virtualization", "kvm", "Can access virtualization services"),
|
|
("Shared memory", "shm", "Can use shared memory"),
|
|
("All devices (e.g. webcam)", "all", "Can access all device files")
|
|
])
|
|
|
|
self._global_add_section(listbox, "Features", "features", [
|
|
("Development syscalls", "devel", "Can perform development operations"),
|
|
("Programs from other architectures", "multiarch", "Can execute programs from other architectures"),
|
|
("Bluetooth", "bluetooth", "Can access Bluetooth hardware"),
|
|
("Controller Area Network bus", "canbus", "Can access CAN bus"),
|
|
("Application Shared Memory", "per-app-dev-shm", "Can use shared memory for IPC")
|
|
])
|
|
|
|
# Add Filesystems section
|
|
self._global_add_filesystem_section(listbox, "Filesystems")
|
|
self._global_add_path_section(listbox, "Persistent", "persistent")
|
|
self._global_add_path_section(listbox, "Environment", "environment")
|
|
self._global_add_bus_section(listbox, "System Bus", "system_bus")
|
|
self._global_add_bus_section(listbox, "Session Bus", "session_bus")
|
|
|
|
# Add widgets to container
|
|
box_outer.pack_start(scrolled, True, True, 0)
|
|
scrolled.add(listbox)
|
|
|
|
# Connect destroy signal
|
|
self.global_options_window.connect("destroy", lambda w: w.destroy())
|
|
|
|
# Show window
|
|
self.global_options_window.show_all()
|
|
|
|
def _global_add_section(self, listbox, section_title, perm_type=None, section_options=None):
|
|
"""Helper method to add a section with multiple options"""
|
|
# Add separator
|
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
listbox.add(sep)
|
|
|
|
# Add section header
|
|
row_header = Gtk.ListBoxRow(selectable=False)
|
|
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
label_header = Gtk.Label(label=f"<b>{section_title}</b>",
|
|
use_markup=True, xalign=0)
|
|
box_header.pack_start(label_header, True, True, 0)
|
|
row_header.add(box_header)
|
|
listbox.add(row_header)
|
|
|
|
if section_title in ["Persistent", "Environment", "System Bus", "Session Bus"]:
|
|
success, perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
else:
|
|
success, perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode)
|
|
if not success:
|
|
perms = {"paths": []}
|
|
|
|
if section_options:
|
|
# Add options
|
|
for display_text, option, description in section_options:
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=display_text, xalign=0)
|
|
desc = Gtk.Label(label=description, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
vbox.pack_start(desc, True, True, 0)
|
|
|
|
switch = Gtk.Switch()
|
|
switch.props.valign = Gtk.Align.CENTER
|
|
|
|
# Handle portal permissions differently
|
|
if section_title == "Portals":
|
|
if option in perms:
|
|
switch.set_active(perms[option] == 'yes')
|
|
switch.set_sensitive(True)
|
|
else:
|
|
switch.set_sensitive(False)
|
|
else:
|
|
switch.set_active(option in [p.lower() for p in perms["paths"]])
|
|
switch.set_sensitive(True)
|
|
|
|
switch.connect("state-set", self._global_on_switch_toggled, perm_type, option)
|
|
hbox.pack_end(switch, False, True, 0)
|
|
|
|
listbox.add(row)
|
|
|
|
def _global_on_switch_toggled(self, switch, state, perm_type, option):
|
|
"""Handle switch toggle events"""
|
|
success, message = fp_turbo.global_toggle_other_perms(
|
|
perm_type,
|
|
option.lower(),
|
|
state,
|
|
True,
|
|
self.system_mode
|
|
)
|
|
|
|
if not success:
|
|
switch.set_active(not state)
|
|
print(f"Error: {message}")
|
|
|
|
def _global_on_remove_path(self, button, path, perm_type=None):
|
|
"""Handle remove path button click"""
|
|
if perm_type:
|
|
if perm_type == "persistent":
|
|
success, message = fp_turbo.global_remove_file_permissions(
|
|
path,
|
|
"persistent",
|
|
True,
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.global_remove_permission_value(
|
|
perm_type,
|
|
path,
|
|
True,
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.global_remove_file_permissions(
|
|
path,
|
|
"filesystems",
|
|
True,
|
|
self.system_mode
|
|
)
|
|
if success:
|
|
# Refresh the current window
|
|
self.global_options_window.destroy()
|
|
self.global_on_options_clicked(None)
|
|
|
|
def _global_on_add_path(self, button, perm_type=None):
|
|
"""Handle add path button click"""
|
|
dialog = Gtk.Dialog(
|
|
title="Add Filesystem Path",
|
|
parent=self.global_options_window,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
|
|
# Add buttons separately
|
|
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
|
dialog.add_button("Add", Gtk.ResponseType.OK)
|
|
|
|
entry = Gtk.Entry()
|
|
entry.set_placeholder_text("Enter filesystem path")
|
|
dialog.vbox.pack_start(entry, True, True, 0)
|
|
dialog.show_all()
|
|
|
|
response = dialog.run()
|
|
if response == Gtk.ResponseType.OK:
|
|
path = entry.get_text()
|
|
if perm_type:
|
|
if perm_type == "persistent":
|
|
success, message = fp_turbo.global_add_file_permissions(
|
|
path,
|
|
"persistent",
|
|
True,
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.global_add_permission_value(
|
|
perm_type,
|
|
path,
|
|
True,
|
|
self.system_mode
|
|
)
|
|
else:
|
|
success, message = fp_turbo.global_add_file_permissions(
|
|
path,
|
|
"filesystems",
|
|
True,
|
|
self.system_mode
|
|
)
|
|
if success:
|
|
# Refresh the current window
|
|
self.global_options_window.destroy()
|
|
self.global_on_options_clicked(None)
|
|
message_type = Gtk.MessageType.INFO
|
|
else:
|
|
message_type = Gtk.MessageType.ERROR
|
|
if message:
|
|
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=message
|
|
)
|
|
error_dialog.run()
|
|
error_dialog.destroy()
|
|
dialog.destroy()
|
|
|
|
def _global_add_option(self, parent_box, label_text, description):
|
|
"""Helper method to add an individual option"""
|
|
row = Gtk.ListBoxRow(selectable=False)
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
row.add(hbox)
|
|
|
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
hbox.pack_start(vbox, True, True, 0)
|
|
|
|
label = Gtk.Label(label=label_text, xalign=0)
|
|
desc = Gtk.Label(label=description, xalign=0)
|
|
vbox.pack_start(label, True, True, 0)
|
|
vbox.pack_start(desc, True, True, 0)
|
|
|
|
switch = Gtk.Switch()
|
|
switch.props.valign = Gtk.Align.CENTER
|
|
hbox.pack_end(switch, False, True, 0)
|
|
|
|
parent_box.add(row)
|
|
return row, switch
|
|
|
|
|
|
def on_update_clicked(self, button, app):
|
|
"""Handle the Remove button click with removal options"""
|
|
details = app.get_details()
|
|
|
|
# Create dialog
|
|
dialog = Gtk.Dialog(
|
|
title=f"Update {details['name']}?",
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
# Add buttons using the new method
|
|
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
|
dialog.add_button("Update", Gtk.ResponseType.OK)
|
|
|
|
# Create content area
|
|
content_area = dialog.get_content_area()
|
|
content_area.set_spacing(12)
|
|
content_area.set_border_width(12)
|
|
|
|
content_area.pack_start(Gtk.Label(label=f"Update: {details['id']}?"), False, False, 0)
|
|
|
|
# Show dialog
|
|
dialog.show_all()
|
|
|
|
# Run dialog
|
|
response = dialog.run()
|
|
if response == Gtk.ResponseType.OK:
|
|
# Perform Removal
|
|
def perform_update():
|
|
# Show waiting dialog
|
|
GLib.idle_add(self.show_waiting_dialog, "Updating package...")
|
|
|
|
success, message = fp_turbo.update_flatpak(app, None, self.system_mode)
|
|
|
|
# Update UI on main thread
|
|
GLib.idle_add(lambda: self.on_task_complete(dialog, success, message))
|
|
|
|
# Start spinner and begin installation
|
|
thread = threading.Thread(target=perform_update)
|
|
thread.daemon = True # Allow program to exit even if thread is still running
|
|
thread.start()
|
|
|
|
dialog.destroy()
|
|
|
|
def download_screenshot(self, url, local_path):
|
|
"""Download a screenshot and save it locally"""
|
|
try:
|
|
# Download the image
|
|
response = requests.get(url)
|
|
response.raise_for_status()
|
|
|
|
# Create the directory if it doesn't exist
|
|
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
|
|
# Save the image
|
|
with open(local_path, 'wb') as f:
|
|
f.write(response.content)
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error downloading screenshot {url}: {e}")
|
|
return False
|
|
|
|
def create_screenshot_slideshow(self, screenshots, app_id):
|
|
# Create main container for slideshow
|
|
slideshow_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
|
slideshow_box.set_border_width(0)
|
|
|
|
# Create main frame for the current screenshot (removed border)
|
|
main_frame = Gtk.Frame()
|
|
main_frame.set_size_request(400, 300) # Adjust size as needed
|
|
main_frame.set_shadow_type(Gtk.ShadowType.NONE)
|
|
slideshow_box.pack_start(main_frame, True, True, 0)
|
|
|
|
# Create image for current screenshot
|
|
current_image = Gtk.Image()
|
|
current_image.set_size_request(400, 300) # Adjust size as needed
|
|
main_frame.add(current_image)
|
|
|
|
# Create box for navigation dots
|
|
nav_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
nav_box.set_halign(Gtk.Align.CENTER)
|
|
nav_box.set_border_width(0) # Remove border
|
|
slideshow_box.pack_start(nav_box, False, True, 0)
|
|
|
|
# Create navigation dots
|
|
dots = []
|
|
for i in range(len(screenshots)):
|
|
# Create new EventBox for each dot
|
|
event_box = Gtk.EventBox()
|
|
event_box.set_border_width(0)
|
|
|
|
# Create bullet using Label
|
|
bullet = Gtk.Label(label="•")
|
|
bullet.get_style_context().add_class("screenshot-bullet")
|
|
bullet.set_opacity(0.3 if i > 0 else 1.0) # First dot is active
|
|
|
|
# Add bullet to event box
|
|
event_box.add(bullet)
|
|
|
|
# Connect navigation
|
|
event_box.connect('button-release-event',
|
|
lambda w, e, idx=i: self._switch_screenshot(
|
|
current_image, screenshots, dots, idx, app_id))
|
|
|
|
# Add event box to nav box
|
|
nav_box.pack_start(event_box, False, True, 0)
|
|
|
|
# Store the event box
|
|
dots.append(event_box)
|
|
|
|
# Load first screenshot
|
|
self._load_screenshot(current_image, screenshots[0], app_id)
|
|
|
|
return slideshow_box
|
|
|
|
def _load_screenshot(self, image, screenshot, app_id):
|
|
"""Helper method to load a single screenshot"""
|
|
home_dir = os.path.expanduser("~")
|
|
|
|
# Get URL using fp_turbo.screenshot_details() like in your original code
|
|
image_data = fp_turbo.screenshot_details(screenshot)
|
|
url = image_data.get_url()
|
|
|
|
local_path = f"{home_dir}/.local/share/flatshop/app-screenshots/{app_id}/{os.path.basename(url)}"
|
|
|
|
if os.path.exists(local_path):
|
|
image.set_from_file(local_path)
|
|
else:
|
|
if fp_turbo.check_internet():
|
|
try:
|
|
if not self.download_screenshot(url, local_path):
|
|
print("Failed to download screenshot")
|
|
return
|
|
image.set_from_file(local_path)
|
|
except Exception:
|
|
image.set_from_icon_name('image-x-generic', Gtk.IconSize.MENU)
|
|
else:
|
|
image.set_from_icon_name('image-x-generic', Gtk.IconSize.MENU)
|
|
|
|
def _switch_screenshot(self, image, screenshots, dots, index, app_id):
|
|
# Update dots opacity
|
|
for i, dot in enumerate(dots):
|
|
# Get the bullet label from the event box
|
|
bullet = dot.get_children()[0]
|
|
bullet.set_opacity(1.0 if i == index else 0.3)
|
|
|
|
# Load the new screenshot
|
|
self._load_screenshot(image, screenshots[index], app_id)
|
|
|
|
def on_details_clicked(self, button, app):
|
|
details = app.get_details()
|
|
|
|
# Create window
|
|
self.details_window = Gtk.Window(title=f"{details['name']}")
|
|
self.details_window.set_default_size(900, 600)
|
|
|
|
# Set header bar
|
|
header_bar = Gtk.HeaderBar(
|
|
title=f"{details['name']}",
|
|
subtitle="List of resources selectively granted to the application"
|
|
)
|
|
header_bar.set_show_close_button(True)
|
|
self.details_window.set_titlebar(header_bar)
|
|
|
|
# Main container with padding
|
|
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
|
box_outer.set_border_width(20)
|
|
box_outer.set_border_width(0)
|
|
self.details_window.add(box_outer)
|
|
|
|
# Scrolled window for content
|
|
scrolled = Gtk.ScrolledWindow()
|
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
scrolled.set_border_width(0)
|
|
box_outer.pack_start(scrolled, True, True, 0)
|
|
|
|
# Content box
|
|
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
|
content_box.set_border_width(0)
|
|
content_box.get_style_context().add_class("details-window")
|
|
scrolled.add(content_box)
|
|
|
|
# Icon section - New implementation
|
|
icon_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
|
icon_row.set_border_width(0)
|
|
|
|
icon_box = Gtk.Box()
|
|
icon_box.set_size_request(88, -1)
|
|
|
|
app_icon = Gio.Icon.new_for_string('package-x-generic-symbolic')
|
|
icon_widget = self.create_scaled_icon(app_icon, is_themed=True)
|
|
|
|
if details['icon_filename']:
|
|
if Path(details['icon_path_128'] + "/" + details['icon_filename']).exists():
|
|
icon_widget = self.create_scaled_icon(
|
|
f"{details['icon_path_128']}/{details['icon_filename']}",
|
|
is_themed=False
|
|
)
|
|
|
|
icon_widget.set_size_request(64, 64)
|
|
icon_box.pack_start(icon_widget, True, True, 0)
|
|
|
|
# Middle column - Name, Version, Developer
|
|
middle_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
name_label = Gtk.Label(label=f"{details['name']}")
|
|
name_label.get_style_context().add_class("large-title")
|
|
name_label.set_xalign(0)
|
|
version_label = Gtk.Label(label=f"Version {details['version']}")
|
|
version_label.set_xalign(0)
|
|
developer_label = Gtk.Label(label=f"Developer: {details['developer']}")
|
|
developer_label.set_xalign(0)
|
|
|
|
middle_column.pack_start(name_label, False, True, 0)
|
|
middle_column.pack_start(version_label, False, True, 0)
|
|
middle_column.pack_start(developer_label, False, True, 0)
|
|
|
|
# Right column - ID and Kind
|
|
right_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
id_label = Gtk.Label(label=f"ID: {details['id']}")
|
|
id_label.set_xalign(0)
|
|
kind_label = Gtk.Label(label=f"Kind: {details['kind']}")
|
|
kind_label.set_xalign(0)
|
|
right_column.pack_start(id_label, False, True, 0)
|
|
right_column.pack_start(kind_label, False, True, 0)
|
|
|
|
# Assemble the row
|
|
icon_row.pack_start(icon_box, False, False, 0)
|
|
icon_row.pack_start(middle_column, True, True, 0)
|
|
icon_row.pack_start(right_column, False, True, 0)
|
|
|
|
content_box.pack_start(icon_row, False, True, 0)
|
|
|
|
content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
|
|
|
|
# Add the slideshow after the icon
|
|
screenshot_slideshow = self.create_screenshot_slideshow(details['screenshots'], details['id'])
|
|
screenshot_slideshow.set_border_width(0)
|
|
content_box.pack_start(screenshot_slideshow, False, True, 0)
|
|
|
|
def create_text_section(title, text):
|
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
|
|
title_label = Gtk.Label(label=f"{title}:")
|
|
title_label.set_xalign(0)
|
|
|
|
# Create a TextView for HTML content
|
|
text_view = Gtk.TextView()
|
|
text_view.set_editable(False)
|
|
text_view.set_cursor_visible(False)
|
|
text_view.set_wrap_mode(Gtk.WrapMode.WORD)
|
|
|
|
# Remove background
|
|
text_view.get_style_context().add_class("details-textview")
|
|
|
|
# Parse HTML and insert into TextView
|
|
buffer = text_view.get_buffer()
|
|
if title == "Description":
|
|
try:
|
|
|
|
class TextExtractor(HTMLParser):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.text = []
|
|
|
|
def handle_data(self, data):
|
|
self.text.append(data)
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
if tag == 'p':
|
|
self.text.append('\n')
|
|
elif tag == 'ul':
|
|
self.text.append('\n')
|
|
elif tag == 'li':
|
|
self.text.append('• ')
|
|
|
|
def handle_endtag(self, tag):
|
|
if tag == 'li':
|
|
self.text.append('\n')
|
|
elif tag == 'ul':
|
|
self.text.append('\n')
|
|
|
|
# Parse the HTML
|
|
parser = TextExtractor()
|
|
parser.feed(text)
|
|
parsed_text = ''.join(parser.text)
|
|
|
|
# Add basic HTML styling
|
|
buffer.set_text(parsed_text)
|
|
text_view.set_left_margin(10)
|
|
text_view.set_right_margin(10)
|
|
text_view.set_pixels_above_lines(4)
|
|
text_view.set_pixels_below_lines(4)
|
|
|
|
except Exception:
|
|
# Fallback to plain text if HTML parsing fails
|
|
buffer.set_text(text)
|
|
else:
|
|
buffer.set_text(text)
|
|
|
|
box.pack_start(title_label, False, True, 0)
|
|
box.pack_start(text_view, True, True, 0)
|
|
return box
|
|
|
|
def create_url_section(url_type, url):
|
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
|
|
|
label_widget = Gtk.Label(label=f"{url_type.capitalize()}:")
|
|
label_widget.set_xalign(0)
|
|
|
|
# Create a clickable URL label
|
|
url_label = Gtk.Label(label=url)
|
|
url_label.set_use_underline(True)
|
|
url_label.set_use_markup(True)
|
|
url_label.set_markup(f'<span color="#18A3FF">{url}</span>')
|
|
url_label.set_halign(Gtk.Align.START)
|
|
|
|
# Connect click event
|
|
event_box = Gtk.EventBox()
|
|
event_box.add(url_label)
|
|
event_box.connect("button-release-event",
|
|
lambda w, e: Gio.AppInfo.launch_default_for_uri(url))
|
|
|
|
box.pack_start(label_widget, False, True, 0)
|
|
box.pack_start(event_box, True, True, 0)
|
|
return box
|
|
|
|
summary_section = create_text_section("Summary", details['summary'])
|
|
content_box.pack_start(summary_section, False, True, 0)
|
|
content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
|
|
|
|
# URLs section
|
|
urls_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
|
|
for url_type, url in details['urls'].items():
|
|
row = create_url_section(url_type, url)
|
|
urls_section.pack_start(row, False, True, 0)
|
|
urls_section.pack_start(create_url_section("Flathub Page", f"https://flathub.org/apps/details/{details['id']}"), False, True, 0)
|
|
|
|
content_box.pack_start(urls_section, False, True, 0)
|
|
|
|
content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
|
|
|
|
description_section = create_text_section("Description", details['description'])
|
|
content_box.pack_start(description_section, False, True, 0)
|
|
|
|
self.details_window.connect("destroy", lambda w: w.destroy())
|
|
self.details_window.show_all()
|
|
scrolled.get_vadjustment().set_value(0)
|
|
|
|
|
|
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 = fp_turbo.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 = fp_turbo.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:
|
|
fp_turbo.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 = fp_turbo.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 = fp_turbo.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=None, file_path=None):
|
|
"""Handle the Add Repository button click"""
|
|
response = Gtk.ResponseType.CANCEL
|
|
dialog = Gtk.Dialog(
|
|
title="Install?",
|
|
transient_for=self,
|
|
modal=True,
|
|
destroy_with_parent=True,
|
|
)
|
|
repo_file_path = ""
|
|
# Create file chooser dialog
|
|
if button and not file_path:
|
|
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)
|
|
response = dialog.run()
|
|
repo_file_path = dialog.get_filename()
|
|
elif file_path and not button:
|
|
# Create dialog
|
|
dialog = Gtk.Dialog(
|
|
title=f"Install {file_path}?",
|
|
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)
|
|
|
|
content_area.pack_start(Gtk.Label(label=f"Install {file_path}?"), False, False, 0)
|
|
|
|
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)
|
|
dialog.show_all()
|
|
response = dialog.run()
|
|
repo_file_path = file_path
|
|
dialog.destroy()
|
|
|
|
if response == Gtk.ResponseType.OK and repo_file_path:
|
|
# Add the repository
|
|
success, error_message = fp_turbo.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():
|
|
# Check for command line argument
|
|
if len(sys.argv) > 1:
|
|
arg = sys.argv[1]
|
|
if arg.endswith('.flatpakref'):
|
|
# Create a temporary window just to handle the installation
|
|
app = MainWindow()
|
|
app.handle_flatpakref_file(arg)
|
|
# Keep the window open for 5 seconds to show the result
|
|
GLib.timeout_add_seconds(5, Gtk.main_quit)
|
|
Gtk.main()
|
|
return
|
|
if arg.endswith('.flatpakrepo'):
|
|
# Create a temporary window just to handle the installation
|
|
app = MainWindow()
|
|
app.handle_flatpakrepo_file(arg)
|
|
# Keep the window open for 5 seconds to show the result
|
|
GLib.timeout_add_seconds(5, Gtk.main_quit)
|
|
Gtk.main()
|
|
return
|
|
app = MainWindow()
|
|
app.connect("destroy", Gtk.main_quit)
|
|
app.show_all()
|
|
Gtk.main()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|