flatpost/main.py

4117 lines
168 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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.subcategory_buttons = {}
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;
}
.top-bar {
margin: 0px;
padding: 10px;
border: 0px;
}
# revealer and tool_box are hidden components inside GtkSearchBar
# This gets rid of the stupid grey line the tool_box causes.
#search_hidden_revealer,
#search_hidden_tool_box {
background: transparent;
border: none;
box-shadow: none;
background-image: none;
border-image: none;
padding: 0px;
margin: 0px;
}
.category-group-header {
padding: 6px;
margin: 0;
font-weight: bold;
}
.category-button {
border: 0px;
padding: 6px;
margin: 0;
background: none;
}
.pan-button {
border: 0px;
padding: 6px;
margin: 0;
background: none;
box-shadow: none;
}
.no-scroll-bars scrollbar {
min-width: 0px;
opacity: 0;
margin-top: -20px;
}
.subcategory-group-header {
padding: 6px;
margin: 0;
}
.subcategory-group-header active {
padding: 6px;
margin: 0;
font-weight: bold;
}
.subcategory-button {
border: 0px;
padding: 6px;
margin: 0;
background: none;
}
.subcategory-button.active {
font-weight: bold;
}
.subcategories-scroll {
border: none;
background-color: transparent;
min-height: 40px;
}
.repo-item {
padding: 6px;
margin: 2px;
border-bottom: 1px solid #eee;
}
.repo-delete-button {
border: none;
padding: 6px;
margin-left: 6px;
}
.repo-list-header {
font-size: 18px;
padding: 5px;;
}
.app-window {
border: 0px;
margin: 0px;
padding-right: 20px;
background: none;
}
.app-list-header {
font-size: 18px;
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;
}
.app-repo-label {
font-size: 0.8em;
}
.app-type-label {
font-size: 0.8em;
}
.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;
}
.permissions-header-label {
font-weight: bold;
font-size: 24px;
}
.permissions-row {
padding: 4px;
background: none;
}
.permissions-item-label {
font-weight: bold;
font-size: 14px;
}
.permissions-item-summary {
font-size: 12px;
}
.permissions-global-indicator {
background: none;
}
.permissions-spacing-box {
background: none;
padding: 5px;
}
.permissions-path-vbox {
padding: 6px;
}
.permissions-path {
padding: 6px;
}
.permissions-path-text text {
color: @search_fg_color;
}
.permissions-path-text textview {
border-radius: 4px;
padding: 8px;
background-color: @search_bg_color;
border: 1px solid @search_border_color;
margin: 8px;
}
.permissions-path-text border {
background-color: @search_border_color;
border-radius: 4px;
}
.permissions-path-scroll {
padding: 6px;
}
.permissions-bus-box {
padding-left: 8px;
background: none;
}
combobox,
combobox box,
combobox button {
font-size: 12px;
padding-top: 0px;
padding-bottom: 0px;
margin: 0px;
min-height: 0px;
}
button {
padding-top: 0px;
padding-bottom: 0px;
margin: 0px;
min-height: 0px;
}
""")
# Add CSS provider to the default screen
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 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.get_style_context().add_class("top-bar")
self.top_bar.set_hexpand(True)
self.top_bar.set_vexpand(False)
self.top_bar.set_spacing(6)
# Add search bar
self.searchbar = Gtk.SearchBar() # Use self.searchbar instead of searchbar
self.searchbar.set_show_close_button(False)
self.searchbar.set_hexpand(False)
self.searchbar.set_vexpand(False)
self.searchbar.set_margin_top(0)
self.searchbar.set_margin_bottom(0)
self.searchbar.set_margin_start(0)
self.searchbar.set_margin_end(0)
revealer = self.searchbar.get_children()[0]
revealer.set_name("search_hidden_revealer")
revealer.set_margin_top(0)
revealer.set_margin_bottom(0)
revealer.set_margin_start(0)
revealer.set_margin_end(0)
tool_box = revealer.get_children()[0]
tool_box.set_name("search_hidden_tool_box")
tool_box.set_margin_top(0)
tool_box.set_margin_bottom(0)
tool_box.set_margin_start(0)
tool_box.set_margin_end(0)
# 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('system-search-symbolic'))
# 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_wrap_width(1)
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('applications-system-symbolic')
global_overrides_button.set_image(Gtk.Image.new_from_gicon(global_overrides_button_icon, Gtk.IconSize.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.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("category-group-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)
container.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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("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 and reset labels for all widgets
for group_name in self.category_widgets:
for widget in self.category_widgets[group_name]:
label = widget.get_children()[0]
label.set_use_markup(False)
# Loop through known original titles to find a match
for grp in self.category_groups:
for key, val in self.category_groups[grp].items():
# Escape val for comparison with possible markup in label
safe_val = GLib.markup_escape_text(val)
if safe_val in label.get_text() or val in label.get_text():
label.set_label(val)
break
# Add active state and markup icon
display_title = self.category_groups[group][category]
for widget in self.category_widgets[group]:
label = widget.get_children()[0]
if label.get_text() == display_title:
safe_title = GLib.markup_escape_text(display_title)
markup = f"{safe_title} <span foreground='#18A3FF'><b></b></span>"
label.set_markup(markup)
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:
parent_title = self.category_groups['categories'].get(parent_category, parent_category)
subcat_title = subcategories[category]
display_title = f"{parent_title} » {subcat_title}"
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.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)
self.right_panel.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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 create_subcategory_container(self):
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)
return container
def create_scroll_buttons(self):
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("pan-button")
pan_start.connect("clicked", self.on_pan_start)
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("pan-button")
pan_end.connect("clicked", self.on_pan_end)
return pan_start, pan_end
def build_subcategory_bar(self, category):
container = self.create_subcategory_container()
for subcategory, title in self.subcategory_groups[category].items():
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)
label = Gtk.Label(label=title)
label.set_halign(Gtk.Align.START)
label.set_hexpand(False)
label.get_style_context().add_class("subcategory-button")
if subcategory == category:
label.get_style_context().add_class("selected")
subcategory_box.add(label)
subcategory_box.connect(
"button-release-event",
lambda widget, event, subcat=subcategory: self.on_subcategory_clicked(subcat)
)
self.subcategory_buttons[subcategory] = label
container.pack_start(subcategory_box, False, False, 0)
return container
def build_subcategory_context_view(self, category, parent_category):
container = self.create_subcategory_container()
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)
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("subcategory-button")
parent_box.add(parent_label)
parent_box.connect(
"button-release-event",
lambda widget, event, cat=parent_category, grp='categories':
self.on_category_clicked(cat, grp)
)
container.pack_start(parent_box, False, False, 0)
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)
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("subcategory-button")
subcategory_box.add(subcategory_label)
subcategory_box.connect(
"button-release-event",
lambda widget, event, subcat=category:
self.on_subcategory_clicked(subcat)
)
container.pack_start(subcategory_box, False, False, 0)
return container
def scroll_to_widget(self, widget):
"""Scrolls the scrolled window to ensure the widget is fully visible."""
adjustment = self.scrolled_window.get_hadjustment()
# Container is the Gtk.Box inside the scrolled window
container = self.scrolled_window.get_child()
if not container:
return False
# Translate widget's position relative to the container
widget_coords = widget.translate_coordinates(container, 0, 0)
if not widget_coords:
return False
widget_x, _ = widget_coords
widget_width = widget.get_allocated_width()
view_start = adjustment.get_value()
view_end = view_start + adjustment.get_page_size()
# Scroll only if the widget is outside the visible area
if widget_x < view_start:
adjustment.set_value(widget_x)
elif (widget_x + widget_width) > view_end:
adjustment.set_value(widget_x + widget_width - adjustment.get_page_size())
return False
def update_subcategories_bar(self, category):
for child in self.subcategories_bar.get_children():
child.destroy()
self.subcategory_buttons.clear()
if not hasattr(self, 'scrolled_window'):
self.scrolled_window = Gtk.ScrolledWindow()
for child in self.scrolled_window.get_children():
child.destroy()
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)
self.scrolled_window.set_max_content_width(-1)
self.scrolled_window.set_overlay_scrolling(False)
self.scrolled_window.get_style_context().add_class("no-scroll-bars")
pan_start, pan_end = self.create_scroll_buttons()
self.subcategories_bar.get_style_context().add_class("subcategory-group-header")
self.subcategories_bar.set_visible(True)
if category in self.subcategory_groups:
container = self.build_subcategory_bar(category)
else:
parent_category = self.get_parent_category(category)
if parent_category:
container = self.build_subcategory_context_view(category, parent_category)
else:
self.subcategories_bar.set_visible(False)
return
self.scrolled_window.add(container)
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.queue_resize()
self.subcategories_bar.show_all()
def get_parent_category(self, subcategory):
for parent, subcats in self.subcategory_groups.items():
if subcategory in subcats:
return parent
return None
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 highlight_selected_subcategory(self, selected_subcat):
for subcat, widget in self.subcategory_buttons.items():
if subcat == selected_subcat:
widget.get_style_context().add_class("active")
else:
widget.get_style_context().remove_class("active")
# Scroll to make sure the selected subcategory is visible
selected_widget = self.subcategory_buttons.get(selected_subcat)
if selected_widget:
adj = self.scrolled_window.get_hadjustment()
alloc = selected_widget.get_allocation()
new_value = alloc.x + alloc.width / 2 - adj.get_page_size() / 2
adj.set_value(max(0, new_value))
def on_subcategory_clicked(self, subcategory):
"""Handle subcategory button clicks."""
# Remove 'selected' from all subcategory buttons
for label in self.subcategory_buttons.values():
label.get_style_context().remove_class("selected")
# Add 'selected' to the clicked one
if subcategory in self.subcategory_buttons:
self.subcategory_buttons[subcategory].get_style_context().add_class("selected")
# Update current state
self.current_page = subcategory
self.current_group = 'subcategories'
self.update_category_header(subcategory)
self.highlight_selected_subcategory(subcategory)
self.show_category_apps(subcategory)
if subcategory in self.subcategory_buttons:
selected_widget = self.subcategory_buttons[subcategory]
GLib.idle_add(self.scroll_to_widget, selected_widget)
# 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-symbolic')
add_repo_button.set_image(Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.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.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.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-symbolic')
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=f"Type: {details['kind']}")
kind_label.get_style_context().add_class("app-type-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=f"{repo}")
#repo_label.get_style_context().add_class("app-repo-label")
repo_label.set_halign(Gtk.Align.END)
repo_box.pack_end(repo_label, False, False, 0)
repo_list_label = Gtk.Label(label="Sources: ")
repo_box.pack_end(repo_list_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-symbolic"
else:
button = self.create_button(
self.on_install_clicked,
app,
None,
condition=lambda x: True
)
add_rm_icon = "list-add-symbolic"
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))
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 = "applications-system-symbolic"
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))
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))
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('help-about-symbolic')
details_btn.set_image(Gtk.Image.new_from_gicon(details_icon, Gtk.IconSize.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-symbolic')
donate_btn.set_image(Gtk.Image.new_from_gicon(donate_icon, Gtk.IconSize.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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0)
row_header.add(box_header)
listbox.add(row_header)
# Get permissions
global_success, global_perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode)
if not global_success:
global_perms = {"paths": []}
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_row.get_style_context().add_class("permissions-row")
talks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
talks_box.get_style_context().add_class("permissions-bus-box")
talks_row.add(talks_box)
talks_header = Gtk.Label(label="Talks", xalign=0)
talks_header.get_style_context().add_class("permissions-item-label")
talks_box.pack_start(talks_header, False, False, 0)
# Add talk paths
for path in global_perms["paths"]:
if path != "" and "talk" in path:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
# Configure button based on permission type
btn.set_sensitive(False)
btn.get_style_context().add_class("destructive-action")
btn_box.pack_end(btn, False, False, 0)
indicator_label = Gtk.Label(label="*", xalign=0)
btn_box.pack_end(indicator_label, False, True, 0)
hbox.pack_end(btn_box, False, False, 0)
talks_box.add(row)
for path in perms["paths"]:
if path != "" and "talk" in path and path not in global_perms["paths"]:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
talks_box.add(row)
listbox.add(talks_row)
# Add Owns section
owns_row = Gtk.ListBoxRow(selectable=False)
owns_row.get_style_context().add_class("permissions-row")
owns_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
owns_box.get_style_context().add_class("permissions-bus-box")
owns_row.add(owns_box)
owns_header = Gtk.Label(label="Owns", xalign=0)
owns_header.get_style_context().add_class("permissions-item-label")
owns_box.pack_start(owns_header, False, False, 0)
# Add own paths
for path in global_perms["paths"]:
if path != "" and "own" in path:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
# Configure button based on permission type
btn.set_sensitive(False)
btn.get_style_context().add_class("destructive-action")
btn_box.pack_end(btn, False, False, 0)
indicator_label = Gtk.Label(label="*", xalign=0)
btn_box.pack_end(indicator_label, False, True, 0)
hbox.pack_end(btn_box, False, False, 0)
owns_box.add(row)
for path in perms["paths"]:
if path != "" and "own" in path and path not in global_perms["paths"]:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
owns_box.add(row)
owns_row.show_all()
listbox.add(owns_row)
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# Add add button
add_path_row = Gtk.ListBoxRow(selectable=False)
add_path_row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
add_path_row.add(hbox)
btn = Gtk.Button()
add_rm_icon = "list-add-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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": []}
if perm_type == "persistent":
global_success, global_perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode)
else:
global_success, global_perms = fp_turbo.global_list_other_perm_values(perm_type, True, self.system_mode)
if not global_success:
global_perms = {"paths": []}
# First, create rows for global paths
for path in global_perms["paths"]:
if path != "":
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
# Configure button based on permission type
btn.set_sensitive(False)
btn.get_style_context().add_class("destructive-action")
btn_box.pack_end(btn, False, False, 0)
indicator_label = Gtk.Label(label="*", xalign=0)
btn_box.pack_end(indicator_label, False, True, 0)
hbox.pack_end(btn_box, False, False, 0)
listbox.add(row)
# Then create rows for application-specific paths
for path in perms["paths"]:
if path != "" and path not in global_perms["paths"]:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
listbox.add(row)
# Add add button
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
btn = Gtk.Button()
add_rm_icon = "list-add-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_add_path, app_id, app)
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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0)
row_header.add(box_header)
listbox.add(row_header)
# Get filesystem permissions
global_success, global_perms = fp_turbo.global_list_file_perms(True, self.system_mode)
if not global_success:
global_perms = {"paths": [], "special_paths": []}
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)
row.get_style_context().add_class("permissions-row")
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)
label.get_style_context().add_class("permissions-item-label")
desc = Gtk.Label(label=description, xalign=0)
desc.get_style_context().add_class("permissions-item-summary")
vbox.pack_start(label, True, True, 0)
vbox.pack_start(desc, True, True, 0)
switch = Gtk.Switch()
switch.props.valign = Gtk.Align.CENTER
# Add indicator label before switch
switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
in_perms = option in perms["special_paths"]
in_global_perms = option in global_perms["special_paths"]
switch.set_active(in_global_perms or in_perms)
# Set sensitivity based on your requirements
if in_global_perms:
switch.set_sensitive(False) # Global permissions take precedence
indicator = Gtk.Label(label="*", xalign=1.0)
indicator.get_style_context().add_class("global-indicator")
switch_box.pack_start(indicator, False, True, 0)
elif in_perms:
switch.set_sensitive(True) # Local permissions enabled and sensitive
switch_box.pack_start(switch, False, True, 0)
switch.connect("state-set", self._on_switch_toggled, app_id, "filesystems", option)
hbox.pack_end(switch_box, False, True, 0)
listbox.add(row)
# First, create rows for global paths
for path in global_perms["paths"]:
if path != "":
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
# Configure button based on permission type
btn.set_sensitive(False)
btn.get_style_context().add_class("destructive-action")
btn_box.pack_end(btn, False, False, 0)
indicator_label = Gtk.Label(label="*", xalign=0)
btn_box.pack_end(indicator_label, False, True, 0)
hbox.pack_end(btn_box, False, False, 0)
listbox.add(row)
# Then create rows for application-specific paths
for path in perms["paths"]:
if path != "" and path not in global_perms["paths"]:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_remove_path, app_id, app, path)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
listbox.add(row)
# Add add button
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
btn = Gtk.Button()
add_rm_icon = "list-add-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
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")
indicator = Gtk.Label(label="* = global override", xalign=1.0)
indicator.get_style_context().add_class("permissions-global-indicator")
# 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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# Add Filesystems section
self._add_filesystem_section(app_id, app, listbox, "Filesystems")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._add_path_section(app_id, app, listbox, "Persistent", "persistent")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._add_path_section(app_id, app, listbox, "Environment", "environment")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._add_bus_section(app_id, app, listbox, "System Bus", "system_bus")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._add_bus_section(app_id, app, listbox, "Session Bus", "session_bus")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# Add Portals section
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# Add widgets to container
box_outer.pack_start(indicator, False, False, 0)
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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0)
row_header.add(box_header)
listbox.add(row_header)
# Handle portal permissions specially
perms = {}
global_perms = {}
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"]:
global_success, global_perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode)
if not global_success:
global_perms = {"paths": []}
success, perms = fp_turbo.list_other_perm_toggles(app_id, perm_type, self.system_mode)
if not success:
perms = {"paths": []}
else:
global_success, global_perms = fp_turbo.global_list_other_perm_toggles(perm_type, True, self.system_mode)
if not global_success:
global_perms = {"paths": []}
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)
row.get_style_context().add_class("permissions-row")
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)
label.get_style_context().add_class("permissions-item-label")
desc = Gtk.Label(label=description, xalign=0)
desc.get_style_context().add_class("permissions-item-summary")
vbox.pack_start(label, True, True, 0)
vbox.pack_start(desc, True, True, 0)
switch = Gtk.Switch()
switch.props.valign = Gtk.Align.CENTER
# Add indicator label before switch
switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# 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:
# First check if option exists in either perms or global_perms
in_perms = option.lower() in [p.lower() for p in perms["paths"]]
in_global_perms = option.lower() in [p.lower() for p in global_perms["paths"]]
# Set active state based on precedence rules
switch.set_active(in_global_perms or in_perms)
# Set sensitivity based on your requirements
if in_global_perms:
switch.set_sensitive(False) # Global permissions take precedence
indicator = Gtk.Label(label="*", xalign=0)
indicator.get_style_context().add_class("global-indicator")
switch_box.pack_start(indicator, False, True, 0)
elif in_perms:
switch.set_sensitive(True) # Local permissions enabled and sensitive
switch_box.pack_start(switch, False, True, 0)
switch.connect("state-set", self._on_switch_toggled, app_id, perm_type, option)
hbox.pack_end(switch_box, 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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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_row.get_style_context().add_class("permissions-row")
talks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
talks_box.get_style_context().add_class("permissions-bus-box")
talks_row.add(talks_box)
talks_header = Gtk.Label(label="Talks", xalign=0)
talks_header.get_style_context().add_class("permissions-item-label")
talks_box.pack_start(talks_header, False, False, 0)
# Add talk paths
for path in perms["paths"]:
if "talk" in path:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._global_on_remove_path, path, perm_type)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
talks_box.add(row)
listbox.add(talks_row)
# Add Owns section
owns_row = Gtk.ListBoxRow(selectable=False)
owns_row.get_style_context().add_class("permissions-row")
owns_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
owns_box.get_style_context().add_class("permissions-bus-box")
owns_row.add(owns_box)
owns_header = Gtk.Label(label="Owns", xalign=0)
owns_header.get_style_context().add_class("permissions-item-label")
owns_box.pack_start(owns_header, False, False, 0)
# Add own paths
for path in perms["paths"]:
if "own" in path:
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._on_global_remove_path, path, perm_type)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
owns_box.add(row)
owns_row.show_all()
listbox.add(owns_row)
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# Add add button
add_path_row = Gtk.ListBoxRow(selectable=False)
add_path_row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
add_path_row.add(hbox)
btn = Gtk.Button()
add_rm_icon = "list-add-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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 normal paths with remove buttons
for path in perms["paths"]:
if path != "":
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._global_on_remove_path, path)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
listbox.add(row)
# Add add button
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
btn = Gtk.Button()
add_rm_icon = "list-add-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._global_on_add_path)
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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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)
row.get_style_context().add_class("permissions-row")
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)
label.get_style_context().add_class("permissions-item-label")
desc = Gtk.Label(label=description, xalign=0)
desc.get_style_context().add_class("permissions-item-summary")
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)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#vbox.get_style_context().add_class("permissions-path-vbox")
vbox.set_size_request(400, 30)
hbox.pack_start(vbox, False, True, 0)
text_view = Gtk.TextView()
text_view.set_size_request(400, 20)
text_view.get_style_context().add_class("permissions-path-text")
text_view.set_editable(False)
text_view.set_cursor_visible(False)
#text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
# Enable horizontal scrolling
scrolled_window = Gtk.ScrolledWindow()
#scrolled_window.get_style_context().add_class("permissions-path-scroll")
scrolled_window.set_hexpand(False)
scrolled_window.set_vexpand(False)
scrolled_window.set_size_request(400, 30)
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, # Enable horizontal scrollbar
Gtk.PolicyType.NEVER # Disable vertical scrollbar
)
# Add TextView to ScrolledWindow
scrolled_window.add(text_view)
# Add the text
buffer = text_view.get_buffer()
buffer.set_text(path)
vbox.pack_start(scrolled_window, False, True, 0)
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create remove button
btn = Gtk.Button()
add_rm_icon = "list-remove-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
btn.connect("clicked", self._global_on_remove_path, path)
btn_box.pack_end(btn, False, False, 0)
hbox.pack_end(btn_box, False, False, 0)
listbox.add(row)
# Add add button
row = Gtk.ListBoxRow(selectable=False)
row.get_style_context().add_class("permissions-row")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
row.add(hbox)
btn = Gtk.Button()
add_rm_icon = "list-add-symbolic"
use_icon = Gio.Icon.new_for_string(add_rm_icon)
btn.set_image(Gtk.Image.new_from_gicon(use_icon, Gtk.IconSize.BUTTON))
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")
indicator = Gtk.Label(label="* = global override", xalign=1.0)
indicator.get_style_context().add_class("permissions-global-indicator")
# 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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
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")
])
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# Add Filesystems section
self._global_add_filesystem_section(listbox, "Filesystems")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._global_add_path_section(listbox, "Persistent", "persistent")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._global_add_path_section(listbox, "Environment", "environment")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._global_add_bus_section(listbox, "System Bus", "system_bus")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
self._global_add_bus_section(listbox, "Session Bus", "session_bus")
spacing_box = Gtk.ListBoxRow(selectable=False)
spacing_box.get_style_context().add_class("permissions-spacing-box")
listbox.add(spacing_box)
# 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 section header
row_header = Gtk.ListBoxRow(selectable=False)
row_header.get_style_context().add_class("permissions-row")
box_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
label_header = Gtk.Label(label=f"{section_title}",
use_markup=True, xalign=0)
label_header.get_style_context().add_class("permissions-header-label")
box_header.pack_start(label_header, True, True, 0)
box_header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 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)
row.get_style_context().add_class("permissions-row")
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)
label.get_style_context().add_class("permissions-item-label")
desc = Gtk.Label(label=description, xalign=0)
desc.get_style_context().add_class("permissions-item-summary")
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()