mirror of
https://github.com/linuxserver/proot-apps.git
synced 2026-03-23 00:05:38 +08:00
299 lines
10 KiB
Python
Executable File
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)
|