proot-apps/apps/gui/root/usr/bin/proot-apps-gui

299 lines
10 KiB
Python
Executable File

#!/usr/bin/env python
# Imports
import sys
import os
import gi
import platform
import time
import requests
import yaml
import threading
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
gi.require_version('Vte', '3.91')
from gi.repository import Gtk, Adw, Vte, GLib, GdkPixbuf, Gdk
# CSS
style = '''
.info {
font-size: 16px;
}
.installed-button {
background-color: #3bb78f;
background-image: linear-gradient(315deg, #3bb78f 0%, #0bab64 74%);
border-radius: 5px;
}
.term {
border-radius: 5px;
}
'''
# App Class
class MainWindow(Gtk.ApplicationWindow):
# On launch set globals and style
def __init__(self, *args, **kwargs):
global css
super().__init__(*args, **kwargs)
self.repo = 'linuxserver/proot-apps'
self.metaUrl = 'https://raw.githubusercontent.com/' + self.repo + '/master/metadata/'
self.searchEntry = ''
self.appPath = os.environ['HOME'] + '/proot-apps/ghcr.io_' + self.repo.replace('/','_') + '_'
self.appData = None
self.logos = {}
self.css = Gtk.CssProvider()
self.css.load_from_data(style.encode('utf-8'))
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), self.css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
self.set_default_size(1024, 768)
self.set_title('PRoot Apps Installer')
self.header = Adw.HeaderBar()
self.set_titlebar(self.header)
self.set_icon_name('gui-pa')
loading = Gtk.Box()
self.set_child(loading)
loadMain = threading.Thread(target=self.getAppData, name="LoadMain")
loadMain.start()
# Ingest external metadata
def getAppData(self):
if "PA_REPO_FOLDER" in os.environ:
try:
f = open(os.environ['PA_REPO_FOLDER'] + '/metadata/metadata.yml'); self.appData = yaml.safe_load(f); f.close()
self.renderHome(None)
except Exception as e:
main = Gtk.Box()
text = Gtk.Label()
text.set_text(str(e))
main.append(text)
self.set_child(main)
else:
try:
res = requests.get(self.metaUrl + 'metadata.yml', allow_redirects=True)
txt = res.content.decode("utf-8")
self.appData = yaml.safe_load(txt)
self.renderHome(None)
except Exception as e:
main = Gtk.Box()
text = Gtk.Label()
text.set_text(str(e))
main.append(text)
self.set_child(main)
# Render the landing page
def renderHome(self, button):
self.set_title('PRoot Apps Installer')
entry = Gtk.Entry()
entry.set_text('')
# Remove back button if exists
if button:
self.header.remove(button)
self.onSearch(entry)
self.set_child(self.home)
return
# Setup scrollable app grid
self.home = Gtk.ScrolledWindow()
self.appGrid = Gtk.FlowBox()
self.appGrid.set_valign(Gtk.Align.START)
self.appGrid.set_selection_mode(Gtk.SelectionMode.NONE)
self.home.set_child(self.appGrid)
self.set_child(self.home)
self.search = Gtk.Entry()
self.search.set_placeholder_text(text='Search')
self.search.set_icon_from_icon_name(
icon_pos=Gtk.EntryIconPosition.PRIMARY,
icon_name='system-search-symbolic',
)
self.search.connect('changed', self.onSearch)
self.header.pack_end(self.search)
self.appButton = {}
self.logos = {}
self.logoBoxes = {}
for app in self.appData['include']:
# Do not show disabled apps
if 'disabled' in app:
continue
# Do not show single arch on aarch64
if platform.machine() not in ("i386", "AMD64", "x86_64") and app['arch'] == 'linux/amd64':
continue
# Only download the logos once
if app['name'] in self.logos:
content = self.logos[app['name']]
else:
if "PA_REPO_FOLDER" in os.environ:
content = open(os.environ['PA_REPO_FOLDER'] + '/metadata/img/' + app['icon'], "rb").read()
self.logos[app['name']] = content
else:
getLogo = threading.Thread(target=self.getLogo, name="GetLogo", args=(app['icon'],app['name']))
getLogo.start()
content = False
time.sleep(.04)
self.gridButton(app, content)
# If still loading and user searched call it to show
if self.searchEntry != '' and self.searchEntry in app['name']:
entry = Gtk.Entry()
entry.set_text(self.searchEntry)
self.onSearch(entry)
elif self.searchEntry == '':
# Setup buttons and append to grid
self.appGrid.append(self.appButton[app['name']])
# Run a blank search to render everything properly
self.onSearch(entry)
# Download remote logo set bytes
def getLogo(self, icon, name):
res = requests.get(self.metaUrl + 'img/' + icon, allow_redirects=True)
content = res.content
self.logos[name] = content
logo = self.makeImage(content)
self.logoBoxes[name].set_center_widget(logo)
# Make pixbuf from bytes
def makeImage(self, content):
loader = GdkPixbuf.PixbufLoader()
loader.write_bytes(GLib.Bytes.new(content))
loader.set_size(192,192)
loader.close()
pixBuf = loader.get_pixbuf()
logo = Gtk.Image.new_from_pixbuf(pixBuf)
logo.set_size_request(192, 192)
return logo
# Generate an app grid button
def gridButton(self, app, content):
self.logoBoxes[app['name']] = Gtk.CenterBox()
self.logoBoxes[app['name']].set_size_request(192, 192)
if content:
logo = self.makeImage(content)
self.logoBoxes[app['name']].set_center_widget(logo)
label = Gtk.Label()
label.set_text(app['full_name'])
bContent = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,spacing=10)
# Indicate an app is installed
if os.path.exists(self.appPath + app['name']):
bContent.add_css_class('installed-button')
bContent.set_size_request(220, 220)
bContent.append(self.logoBoxes[app['name']])
bContent.append(label)
self.appButton[app['name']] = Gtk.Button()
self.appButton[app['name']].connect('clicked', self.renderApp, app)
self.appButton[app['name']].set_child(bContent)
# Render the app screen
def renderApp(self, button, app):
self.set_title(app['full_name'])
back = Gtk.Button(label='Back')
self.header.pack_start(back)
# Setup appinfo containers
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,spacing=5)
self.set_child(container)
appInfo = Gtk.Box(spacing=5)
container.append(appInfo)
logoContainer = Gtk.CenterBox(orientation=Gtk.Orientation.VERTICAL)
logoContainer.set_size_request(500, 250)
infoContainer = Gtk.Label()
infoContainer.set_wrap(True)
infoContainer.set_justify(Gtk.Justification.CENTER)
infoContainer.add_css_class('info')
infoContainer.set_size_request(500, 250)
appInfo.append(logoContainer)
appInfo.append(infoContainer)
# Add logo and info
if app['name'] in self.logos:
content = self.logos[app['name']]
loader = GdkPixbuf.PixbufLoader()
loader.write_bytes(GLib.Bytes.new(content))
loader.set_size(192,192)
loader.close()
pixBuf = loader.get_pixbuf()
logo = Gtk.Image.new_from_pixbuf(pixBuf)
logo.set_size_request(192, 192)
logoContainer.set_center_widget(logo)
infoContainer.set_text(app['description'])
# Add action container for clicks and term
actionContainer = Gtk.CenterBox()
actionContainer.set_size_request(1000, 65)
container.append(actionContainer)
self.actionButtons(app, actionContainer)
back.connect('clicked', self.renderHome)
# Run a proot-apps command
def prootRun(self, button, action, app, container):
term = Vte.Terminal()
term.set_size_request(200, 50)
term.add_css_class('term')
term.connect("child-exited", self.removeTerm, app, container)
container.set_center_widget(term)
term.spawn_async(
Vte.PtyFlags.DEFAULT, os.environ['HOME'],
[os.environ['HOME'] + '/.local/bin/proot-apps', action, app['name']],
None, GLib.SpawnFlags.DEFAULT, None, None, -1, None, None, None
)
# Post proot-apps run hook
def removeTerm(self, terminal, error, app, container):
# Leave the terminal up if something went wrong
if error:
pass
# Let the user see the result and render buttons
else:
time.sleep(2)
self.actionButtons(app, container)
# Setup the action buttons depending on if app is installed
def actionButtons(self, app, container):
installButton = Gtk.Button(label='Install ' + app['full_name'])
updateButton = Gtk.Button(label='Update ' + app['full_name'])
removeButton = Gtk.Button(label='Remove ' + app['full_name'])
installedButtons = Gtk.Box(spacing=5)
uninstalledButtons = Gtk.Box(spacing=5)
installedButtons.append(updateButton)
installedButtons.append(removeButton)
uninstalledButtons.append(installButton)
if os.path.exists(self.appPath + app['name']):
container.set_center_widget(installedButtons)
else:
container.set_center_widget(uninstalledButtons)
installButton.connect('clicked', self.prootRun, 'install', app, container)
updateButton.connect('clicked', self.prootRun, 'update', app, container)
removeButton.connect('clicked', self.prootRun, 'remove', app, container)
# Process search
def onSearch(self, entry):
self.searchEntry = entry.get_text()
self.appGrid.remove_all()
for appName in list(self.appButton):
if entry.get_text() != '':
if entry.get_text() in appName:
if appName in self.logos:
content = self.logos[appName]
else:
content = False
app = next(app for app in self.appData['include'] if app['name'] == appName)
self.gridButton(app, content)
self.appGrid.append(self.appButton[app['name']])
else:
if self.appButton[appName].get_parent():
self.appGrid.remove(self.appButton[appName])
else:
if appName in self.logos:
content = self.logos[appName]
else:
content = False
app = next(app for app in self.appData['include'] if app['name'] == appName)
self.gridButton(app, content)
self.appGrid.append(self.appButton[app['name']])
# Run app
class App(Adw.Application):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect('activate', self.on_activate)
def on_activate(self, app):
self.win = MainWindow(application=app)
self.win.present()
app = App(application_id="io.linuxserver.ProotApps")
app.run(sys.argv)