Merge pull request #146 from mocchapi/theme-repository-integration

Theme repository integration
This commit is contained in:
Dpeta 2023-09-12 19:23:08 +02:00 committed by GitHub
commit 22cda74bb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 635 additions and 13 deletions

1
.gitignore vendored
View file

@ -179,3 +179,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/

BIN
img/download_done.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

BIN
img/download_pending.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

BIN
img/download_update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

View file

@ -12,6 +12,7 @@ except ImportError:
import ostools import ostools
import parsetools import parsetools
from theme_repo_manager import ThemeManagerWidget
from generic import RightClickList, RightClickTree, MultiTextDialog from generic import RightClickList, RightClickTree, MultiTextDialog
from dataobjs import pesterQuirk, PesterProfile, PesterHistory from dataobjs import pesterQuirk, PesterProfile, PesterHistory
from memos import TimeSlider, TimeInput from memos import TimeSlider, TimeInput
@ -1425,6 +1426,13 @@ class PesterOptions(QtWidgets.QDialog):
layout_5.addWidget(QtWidgets.QLabel("Minutes before Idle:")) layout_5.addWidget(QtWidgets.QLabel("Minutes before Idle:"))
layout_5.addWidget(self.idleBox) layout_5.addWidget(self.idleBox)
layout_repo_url = QtWidgets.QHBoxLayout()
self.repoUrlBox = QtWidgets.QLineEdit(self)
self.repoUrlBox.setText(self.config.theme_repo_url())
layout_repo_url.addWidget(QtWidgets.QLabel("Theme repository db URL:"))
layout_repo_url.addWidget(self.repoUrlBox)
# self.updateBox = QtWidgets.QComboBox(self) # self.updateBox = QtWidgets.QComboBox(self)
# self.updateBox.addItem("Once a Day") # self.updateBox.addItem("Once a Day")
# self.updateBox.addItem("Once a Week") # self.updateBox.addItem("Once a Week")
@ -1446,15 +1454,36 @@ class PesterOptions(QtWidgets.QDialog):
if not parent.randhandler.running: if not parent.randhandler.running:
self.randomscheck.setEnabled(False) self.randomscheck.setEnabled(False)
avail_themes = self.config.availableThemes()
self.themeBox = QtWidgets.QComboBox(self) self.themeBox = QtWidgets.QComboBox(self)
notheme = theme.name not in avail_themes
for i, t in enumerate(avail_themes): def reset_themeBox():
self.themeBox.addItem(t) avail_themes = self.config.availableThemes()
if (not notheme and t == theme.name) or (notheme and t == "pesterchum"): PchumLog.debug("Resetting themeself.themeBox")
self.themeBox.setCurrentIndex(i) self.themeBox.clear()
notheme = theme.name not in avail_themes
for i, t in enumerate(avail_themes):
self.themeBox.addItem(t)
if (not notheme and t == theme.name) or (notheme and t == "pesterchum"):
self.themeBox.setCurrentIndex(i)
self.themeBox.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
QtWidgets.QSizePolicy.Policy.Minimum,
)
)
reset_themeBox()
self.refreshtheme = QtWidgets.QPushButton("Refresh current theme", self) self.refreshtheme = QtWidgets.QPushButton("Refresh current theme", self)
self.refreshtheme.clicked.connect(parent.themeSelectOverride) self.refreshtheme.clicked.connect(parent.themeSelectOverride)
self.refreshtheme.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Minimum,
QtWidgets.QSizePolicy.Policy.Minimum,
)
)
self.themeManager = ThemeManagerWidget(self.config)
self.themeManager.rebuilt.connect(reset_themeBox)
# This makes it so that the themeBox gets updated when a theme is installed or removed through the repository
self.ghostchum = QtWidgets.QCheckBox("Pesterdunk Ghostchum!!", self) self.ghostchum = QtWidgets.QCheckBox("Pesterdunk Ghostchum!!", self)
self.ghostchum.setChecked(self.config.ghostchum()) self.ghostchum.setChecked(self.config.ghostchum())
@ -1656,6 +1685,7 @@ class PesterOptions(QtWidgets.QDialog):
layout_idle = QtWidgets.QVBoxLayout(widget) layout_idle = QtWidgets.QVBoxLayout(widget)
layout_idle.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) layout_idle.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
layout_idle.addLayout(layout_5) layout_idle.addLayout(layout_5)
layout_idle.addLayout(layout_repo_url)
layout_idle.addLayout(layout_6) layout_idle.addLayout(layout_6)
# if not ostools.isOSXLeopard(): # if not ostools.isOSXLeopard():
# layout_idle.addWidget(self.mspaCheck) # layout_idle.addWidget(self.mspaCheck)
@ -1666,8 +1696,13 @@ class PesterOptions(QtWidgets.QDialog):
layout_theme = QtWidgets.QVBoxLayout(widget) layout_theme = QtWidgets.QVBoxLayout(widget)
layout_theme.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) layout_theme.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
layout_theme.addWidget(QtWidgets.QLabel("Pick a Theme:")) layout_theme.addWidget(QtWidgets.QLabel("Pick a Theme:"))
layout_theme.addWidget(self.themeBox)
layout_theme.addWidget(self.refreshtheme) layout_theme_hbox = QtWidgets.QHBoxLayout()
layout_theme_hbox.addWidget(self.themeBox)
layout_theme_hbox.addWidget(self.refreshtheme)
layout_theme.addLayout(layout_theme_hbox)
layout_theme.addWidget(QtWidgets.QLabel("Get new themes:"))
layout_theme.addWidget(self.themeManager)
layout_theme.addWidget(self.ghostchum) layout_theme.addWidget(self.ghostchum)
self.pages.addWidget(widget) self.pages.addWidget(widget)

View file

@ -51,17 +51,24 @@ def validateDataDir():
logs = os.path.join(datadir, "logs") logs = os.path.join(datadir, "logs")
errorlogs = os.path.join(datadir, "errorlogs") errorlogs = os.path.join(datadir, "errorlogs")
backup = os.path.join(datadir, "backup") backup = os.path.join(datadir, "backup")
js_pchum = os.path.join(datadir, "pesterchum.js") themes = os.path.join(datadir, "themes")
# ~lisanne `datadir/themes` is for repository installed themes
# Apparently everything checks this folder for themes already
# So hopefully im not plugging into an existng system on accident
dirs = [datadir, profile, quirks, logs, errorlogs, backup] js_pchum = os.path.join(datadir, "pesterchum.js")
js_manifest = os.path.join(datadir, "manifest.js")
dirs = [datadir, profile, quirks, logs, themes, errorlogs, backup]
for d in dirs: for d in dirs:
if not os.path.isdir(d) or not os.path.exists(d): if not os.path.isdir(d) or not os.path.exists(d):
os.makedirs(d, exist_ok=True) os.makedirs(d, exist_ok=True)
# pesterchum.js # pesterchum.js
if not os.path.exists(js_pchum): for filepath in [js_pchum, js_manifest]:
with open(js_pchum, "w") as f: if not os.path.exists(filepath):
f.write("{}") with open(filepath, "w") as f:
f.write("{}")
def getDataDir(): def getDataDir():

View file

@ -3101,6 +3101,11 @@ class PesterWindow(MovingWindow):
if idlesetting != curidle: if idlesetting != curidle:
self.config.set("idleTime", idlesetting) self.config.set("idleTime", idlesetting)
self.idler["threshold"] = 60 * idlesetting self.idler["threshold"] = 60 * idlesetting
# theme repo url
repourlsetting = self.optionmenu.repoUrlBox.text()
if repourlsetting != self.config.theme_repo_url():
self.config.set("theme_repo_url", repourlsetting)
# theme # theme
ghostchumsetting = self.optionmenu.ghostchum.isChecked() ghostchumsetting = self.optionmenu.ghostchum.isChecked()
curghostchum = self.config.ghostchum() curghostchum = self.config.ghostchum()
@ -4194,6 +4199,7 @@ class MainProgram(QtCore.QObject):
self.irc.finished.disconnect(self.restartIRC) self.irc.finished.disconnect(self.restartIRC)
def showUpdate(self, q): def showUpdate(self, q):
# ~Lisanne: Doesn't seem to be used anywhere, old update notif mechanism?
new_url = q.get() new_url = q.get()
if new_url[0]: if new_url[0]:
self.widget.pcUpdate.emit(new_url[0], new_url[1]) self.widget.pcUpdate.emit(new_url[0], new_url[1])

567
theme_repo_manager.py Normal file
View file

@ -0,0 +1,567 @@
import os
import io
import json
import zipfile
import logging
from shutil import rmtree
from datetime import datetime
from ostools import getDataDir
try:
from PyQt6 import QtCore, QtGui, QtWidgets, QtNetwork
from PyQt6.QtGui import QAction
_flag_selectable = QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
_flag_topalign = (
QtCore.Qt.AlignmentFlag.AlignLeading
| QtCore.Qt.AlignmentFlag.AlignLeft
| QtCore.Qt.AlignmentFlag.AlignTop
)
except ImportError:
print("PyQt5 fallback (thememanager.py)")
from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork
from PyQt5.QtWidgets import QAction
_flag_selectable = QtCore.Qt.TextSelectableByMouse
_flag_topalign = QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
PchumLog = logging.getLogger("pchumLogger")
themeManager = None
# ~Lisanne
# This file has all the stuff needed to use a theme repository
# - ThemeManagerWidget, a GUI widget that lets the user install, update, & delete repository themes
# - ThemeManager, the class that the widget hooks up to. Handles fetching, downloading, installing, & keeps the manifest updated
# Manifest is a local file that tracks the metadata of themes downloaded from the repository
# Its a themename: {database entry} dict
class ThemeManager(QtCore.QObject):
# signals
theme_installed = QtCore.pyqtSignal(str) # theme name
zip_downloaded = QtCore.pyqtSignal(str, str) # theme name, zip location
database_refreshed = QtCore.pyqtSignal(dict) # self.manifest
manifest_updated = QtCore.pyqtSignal(dict) # self.manifest
errored = QtCore.pyqtSignal(str) # error_text
# variables
manifest = {}
database = {}
database_entries = {}
config = None
manifest_path = os.path.join(getDataDir(), "manifest.js")
NAManager = None
downloads = {}
def __init__(self, config):
super().__init__()
with open(self.manifest_path, "r") as f:
self.manifest = json.load(f)
PchumLog.debug("Manifest.js loaded with: %s", self.manifest)
self.config = config
# TODO: maybe make seperate QNetworkAccessManagers for theme downloads, database fetches, and integrity checkfile
# OR figure out how to connect the signal to tasks instead of the whole object
# That way we dont have to figure out what got downloaded afterwards, and can just have a _on_reply_theme & _on_reply_database or something
self.NAManager = QtNetwork.QNetworkAccessManager()
self.NAManager.finished[QtNetwork.QNetworkReply].connect(self._on_reply)
self.validate_manifest()
self.refresh_database()
@QtCore.pyqtSlot()
def refresh_database(self):
# Fetches a new copy of the theme database from the given URL
# The initialisation & processing of it is handled in self._on_reply
PchumLog.debug(
"Refreshing theme repo database @ %s", self.config.theme_repo_url()
)
promise = self.NAManager.get(
QtNetwork.QNetworkRequest(QtCore.QUrl(self.config.theme_repo_url()))
)
def delete_theme(self, theme_name):
# TODO: check if other installed themes inherit from this to avoid broken themes
# would require some kinda confirmation popup which i havent figure out yet
PchumLog.info("Deleting installed repo theme %s", theme_name)
theme = self.manifest[theme_name]
directory = os.path.join(getDataDir(), "themes", theme["name"])
if os.path.isdir(directory):
rmtree(directory)
self.manifest.pop(theme_name)
self.save_manifest()
self.manifest_updated.emit(self.manifest)
def save_manifest(self):
with open(self.manifest_path, "w") as f:
json.dump(self.manifest, f)
PchumLog.debug("Saved manifes.js to %s", self.manifest_path)
def validate_manifest(self):
# Checks if the themes the manifest claims are installed actually exists
# Removes them from the manifest if they dont
to_pop = set()
all_themes = self.config.availableThemes()
for theme_name in self.manifest:
if not theme_name in all_themes:
PchumLog.warning(
"Supposedly installed theme %s from the manifest seems to have been deleted, removing from manifest now",
theme_name,
)
# Cannot be popped while iterating!
to_pop.add(theme_name)
for theme_name in to_pop:
self.manifest.pop(theme_name)
def download_theme(self, theme_name):
# Downloads the theme .zip
# The actual installing is handled by _on_reply when the theme is downloaded
# Performs no version checks or dependency handling
# Use install_theme() instead unless you know what you're doing
PchumLog.info("Downloading %s", theme_name)
if not theme_name in self.database_entries:
PchumLog.error("Theme name %s does not exist in the database!", theme_name)
return
PchumLog.debug("(From %s)", self.database_entries[theme_name]["download"])
promise = self.NAManager.get(
QtNetwork.QNetworkRequest(
QtCore.QUrl(self.database_entries[theme_name]["download"])
)
)
self.downloads[
self.database_entries[theme_name]["download"]
] = self.database_entries[theme_name]
def install_theme(self, theme_name, force_install=False):
# A higher way to install a theme than download_theme
# Checks if the theme is already installed & if its up to date
# Also recursively handled dependencies, which download_theme does not
# !! note that this does not check if theres a circular dependency !!
# Setting force_install to True will install a given theme, even if it is deemed unnecessary to do so or its inherit dependency cannot be installed
# This gives it the same no-nonsense operation as download_theme, but with the checks in place
PchumLog.info("Installing theme %s", theme_name)
if force_install:
PchumLog.debug("(force_install is enabled)")
if not theme_name in self.database_entries:
PchumLog.error("Theme %s does not exist in the database!", theme_name)
self.errored.emit("Theme %s does not exist in the database!" % theme_name)
return
all_themes = self.config.availableThemes()
theme = self.database_entries[theme_name]
if (
not self.is_installed(theme_name) and theme_name in all_themes
): # Theme exists, but not installed by manager
PchumLog.warning(
"Theme %s is already installed manually. The manual version will get shadowed by the repository version & will not be usable",
theme_name,
)
# Check depedencies
if theme["inherits"] != "":
if self.is_installed(theme["inherits"]):
# Inherited theme is installed. A-OK
PchumLog.debug(
"Theme %s requires theme %s, which is already installed through the repository",
theme_name,
theme["inherits"],
)
if theme["inherits"] in all_themes:
# Inherited theme is manually installed. A-OK
PchumLog.debug(
"Theme %s requires theme %s, which is already installed manually by the user",
theme_name,
theme["inherits"],
)
elif theme["inherits"] in self.database_entries:
# The Inherited theme is not installed, but can be. A-OK
PchumLog.info(
"Theme %s requires theme %s, which will now be installed",
theme_name,
theme["inherits"],
)
self.install_theme(theme["inherits"])
else:
# Inherited theme is not installed, and can't be installed automatically. Exits unless force_install is True
if force_install:
PchumLog.error(
"Theme %s requires theme %s, which is not installed and not in the database. Installing %s anyways, because force_install is True",
theme_name,
theme,
theme_name["inherits"],
)
else:
PchumLog.error(
"Theme %s requires theme %s, which is not installed and not in the database. Cancelling install",
theme_name,
theme["inherits"],
)
self.errored.emit(
"Theme %s requires theme %s, which is not installed and not in the database. Cancelling install"
% (theme_name, theme["inherits"])
)
return
# Check if there's no need to re-install theme
# This is done after the dependency check in case an inherited theme is missing two levels down
if self.is_installed(theme_name) and not self.has_update(
theme_name
): # Theme is installed by manager, and is up-to-date
if force_install:
PchumLog.warning(
"Theme %s is already installed, and no update is available. Installing anyways, because force_install is True",
theme_name,
)
else:
PchumLog.warning(
"Theme %s is already installed, and no update is available. Cancelling install",
theme_name,
)
self.errored.emit(
"Theme %s is already installed, and no update is available. Cancelling install"
% theme_name
)
return
# All is ok. or we're just ignoring the errors through force_install
# No matter. downloading time
self.download_theme(theme_name)
def has_update(self, theme_name):
# Has the given theme an update available
# Returns False if the theme is installed manually or when the theme is up to date
if self.is_installed(theme_name) and theme_name in self.database_entries:
return (
self.manifest[theme_name]["version"]
< self.database_entries[theme_name]["version"]
)
return False
def is_installed(self, theme_name):
# checks if a theme is installed through the manager
# Note that this will return False if the given name is a theme that the user installed manually!
return theme_name in self.manifest
def is_database_valid(self):
return "entries" in self.database and isinstance(
self.database.get("entries"), list
)
# using a slot decorator here raises `ERROR - <class 'TypeError'>, connect() failed between finished(QNetworkReply*) and _on_reply()``
# maybe because of the underscore?
# @QtCore.pyqtSlot(QtNetwork.QNetworkReply)
def _on_reply(self, reply):
if reply.error() != QtNetwork.QNetworkReply.NetworkError.NoError:
PchumLog.error(
"An error occured contacting the repository: %s", reply.error()
)
self.errored.emit(
"An error occured contacting the repository: %s" % reply.error()
)
return
try:
original_url = reply.request().url().url()
# Check if zipfile or database fetch
if original_url in self.downloads:
# This is a theme zip!
theme = self.downloads[original_url]
self._handle_downloaded_zip(bytes(reply.readAll()), theme["name"])
self.downloads.pop(original_url)
self.manifest[theme["name"]] = theme
self.save_manifest()
self.manifest_updated.emit(self.manifest)
PchumLog.info("Theme %s is now installed", theme["name"])
else:
# This is a database refresh!
as_json = bytes(reply.readAll()).decode("utf-8")
self.database = json.loads(as_json)
self.database_entries = {}
if not self.is_database_valid():
self.database = {}
PchumLog.error('Incorrect database format, missing "entries"')
self.errored.emit('Incorrect database format, missing "entries"')
return
# Filter out non-QTchum client themes, like for godot
for dbindex in range(
len(self.database["entries"]) - 1, -1, -1
): # Iterate over the database in reverse
dbitem = self.database["entries"][dbindex]
if dbitem["client"] != "pesterchum":
PchumLog.debug(
"Removed database theme %s because it is not compatible with this client",
dbitem["name"],
)
self.database["entries"].pop(dbindex)
# Make an easy lookup table instead of the array we get from the DB
for dbitem in self.database["entries"]:
self.database_entries[dbitem["name"]] = dbitem
PchumLog.info("Database refreshed")
self.database_refreshed.emit(self.database)
except json.decoder.JSONDecodeError as e:
PchumLog.error("Could not decode theme database JSON: %s", e)
self.errored.emit("Could not decode theme database JSON: %s" % e)
return
except KeyError as e:
self.database = {}
self.database_entries = {}
PchumLog.error("Vital key missing from theme database: %s", e)
self.errored.emit("Vital key missing from theme database: %s" % e)
return
def _handle_downloaded_zip(self, zip_buffer, theme_name):
# Unzips the downloaded theme package in-memory to datadir/themes/theme_name
# I dont think this runs in a thread so it may block, but its so fast i dont think it really matters
# But i guess if its a zip bomb itll crash
directory = os.path.join(getDataDir(), "themes", theme_name)
with zipfile.ZipFile(io.BytesIO(zip_buffer)) as z:
if os.path.isdir(directory):
rmtree(directory)
# Deletes old files that have been removed in an update
z.extractall(directory)
class ThemeManagerWidget(QtWidgets.QWidget):
icons = None
config = None
rebuilt = QtCore.pyqtSignal()
def __init__(self, config, parent=None):
super().__init__(parent)
self.icons = [
QtGui.QIcon("img/download_pending.png"),
QtGui.QIcon("img/download_done.png"),
QtGui.QIcon("img/download_update.png"),
]
self.config = config
global themeManager
if themeManager is None or not themeManager.is_database_valid():
themeManager = ThemeManager(config)
self.setupUI()
else:
self.setupUI()
self.rebuild()
themeManager.database_refreshed.connect(self._on_database_refreshed)
themeManager.manifest_updated.connect(self._on_database_refreshed)
def setupUI(self):
self.layout_main = QtWidgets.QVBoxLayout(self)
self.setLayout(self.layout_main)
# Search bar
# TODO: implement searching
# self.line_search = QtWidgets.QLineEdit()
# self.line_search.setPlaceholderText("Search for themes")
# self.layout_main.addWidget(self.line_search)
# Main layout
# [list of themes/results] | [selected theme details]
layout_hbox_list_and_details = QtWidgets.QHBoxLayout()
# This is the list of database themes
self.list_results = QtWidgets.QListWidget()
self.list_results.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding,
)
)
self.list_results.itemSelectionChanged.connect(self._on_theme_selected)
layout_hbox_list_and_details.addWidget(self.list_results)
# This is the right side, has the install buttons & all the theme details of the selected item
layout_vbox_details = QtWidgets.QVBoxLayout()
# The theme details are inside a scroll container in case of small window
self.frame_scroll = QtWidgets.QScrollArea()
self.frame_scroll.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Expanding,
)
)
# The vbox that the detail labels will rest in
layout_vbox_scroll_insides = QtWidgets.QVBoxLayout()
# here starts the actual detail labels
# Selected theme's name
self.lbl_theme_name = QtWidgets.QLabel("Click a theme to get started")
self.lbl_theme_name.setTextInteractionFlags(_flag_selectable)
self.lbl_theme_name.setStyleSheet(
"QLabel { font-size: 16px; font-weight:bold;}"
)
self.lbl_theme_name.setWordWrap(True)
layout_vbox_scroll_insides.addWidget(self.lbl_theme_name)
# Author name
self.lbl_author_name = QtWidgets.QLabel("")
self.lbl_author_name.setTextInteractionFlags(_flag_selectable)
layout_vbox_scroll_insides.addWidget(self.lbl_author_name)
# description. this needs to be the biggest
self.lbl_description = QtWidgets.QLabel("")
self.lbl_description.setTextInteractionFlags(_flag_selectable)
self.lbl_description.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
)
)
self.lbl_description.setAlignment(_flag_topalign)
self.lbl_description.setWordWrap(True)
layout_vbox_scroll_insides.addWidget(self.lbl_description)
# requires. shows up if a theme has "inherits" set & we dont have it installed
self.lbl_requires = QtWidgets.QLabel("")
self.lbl_requires.setTextInteractionFlags(_flag_selectable)
self.lbl_requires.setWordWrap(True)
layout_vbox_scroll_insides.addWidget(self.lbl_requires)
# Version number. this will also show the current installed one if there is an update
self.lbl_version = QtWidgets.QLabel("")
self.lbl_version.setTextInteractionFlags(_flag_selectable)
layout_vbox_scroll_insides.addWidget(self.lbl_version)
# Last update time
self.lbl_last_update = QtWidgets.QLabel("")
self.lbl_last_update.setWordWrap(True)
self.lbl_last_update.setTextInteractionFlags(_flag_selectable)
layout_vbox_scroll_insides.addWidget(self.lbl_last_update)
# Theme details done, so we wont need the scroll after this
self.frame_scroll.setLayout(layout_vbox_scroll_insides)
layout_vbox_details.addWidget(self.frame_scroll)
# Insta//uninstall buttons
# "Uninstall" button. Only visisble when the selected thene is installed
self.btn_uninstall = QtWidgets.QPushButton("Uninstall", self)
self.btn_uninstall.setHidden(True)
self.btn_uninstall.clicked.connect(self._on_uninstall_clicked)
layout_vbox_details.addWidget(self.btn_uninstall)
# "Install" button. can also say "Update" if an update is availible
# Only visible when not installed or if theres an update
self.btn_install = QtWidgets.QPushButton("Install", self)
self.btn_install.clicked.connect(self._on_install_clicked)
self.btn_install.setDisabled(True)
layout_vbox_details.addWidget(self.btn_install)
# Done with details
layout_hbox_list_and_details.addLayout(layout_vbox_details)
self.layout_main.addLayout(layout_hbox_list_and_details)
self.btn_refresh = QtWidgets.QPushButton("Refresh", self)
self.btn_refresh.clicked.connect(
themeManager.refresh_database
) # Conneced to themeManager!
self.layout_main.addWidget(self.btn_refresh)
self.lbl_error = QtWidgets.QLabel("")
self.lbl_error.setVisible(False)
self.lbl_error.setWordWrap(True)
themeManager.errored.connect(self._on_fetch_error)
self.lbl_error.setTextInteractionFlags(_flag_selectable)
self.lbl_error.setStyleSheet(
" QLabel { background-color:black; color:red; font-size: 16px;}"
)
self.layout_main.addWidget(self.lbl_error)
def _on_fetch_error(self, text):
self.lbl_error.setText(text)
self.lbl_error.setVisible(True)
def _on_uninstall_clicked(self):
theme = themeManager.database["entries"][self.list_results.currentRow()]
themeManager.delete_theme(theme["name"])
def _on_install_clicked(self):
theme = themeManager.database["entries"][self.list_results.currentRow()]
themeManager.install_theme(theme["name"])
@QtCore.pyqtSlot()
def _on_theme_selected(self):
index = self.list_results.currentRow()
theme = themeManager.database["entries"][index]
theme_name = theme["name"]
is_installed = themeManager.is_installed(theme_name)
has_update = themeManager.has_update(theme_name)
self.btn_install.setDisabled(False)
self.btn_install.setText("Update" if has_update else "Install")
self.btn_install.setVisible((is_installed and has_update) or not is_installed)
self.btn_uninstall.setVisible(themeManager.is_installed(theme_name))
self.lbl_theme_name.setText(theme_name)
self.lbl_author_name.setText("By %s" % theme["author"])
self.lbl_description.setText(theme["description"])
version_text = "Version %s" % theme["version"]
if has_update:
version_text += (
" (installed: %s)" % themeManager.manifest[theme_name]["version"]
)
self.lbl_version.setText(version_text)
requires_text = ""
if theme["inherits"]:
requires_text += "Requires %s" % theme["inherits"]
if theme["inherits"] in self.config.availableThemes():
requires_text += " (installed)"
self.lbl_requires.setText((requires_text) if theme["inherits"] else "")
last_update_text = "Released on: "
last_update_text += datetime.fromtimestamp(theme["updated"]).strftime(
"%d/%m/%Y, %H:%M"
)
self.lbl_last_update.setText(last_update_text)
@QtCore.pyqtSlot(dict)
def _on_database_refreshed(self, _):
self.rebuild()
def rebuild(self):
prev_selected_index = self.list_results.currentRow()
database = themeManager.database
self.list_results.clear()
self.lbl_error.setText("")
self.lbl_error.setVisible(False)
if not themeManager.is_database_valid():
self.lbl_error.setText("")
self.lbl_error.setVisible(True)
# Repopulate the list
for dbitem in database["entries"]:
# Determine the suffix
icon = self.icons[0]
status = ""
if themeManager.is_installed(dbitem["name"]):
if themeManager.has_update(dbitem["name"]):
status = "~ (update available)"
icon = self.icons[2]
else:
status = "~ (installed)"
icon = self.icons[1]
text = "%s by %s %s" % (dbitem["name"], dbitem["author"], status)
item = QtWidgets.QListWidgetItem(icon, text)
self.list_results.addItem(item)
if prev_selected_index > -1:
# Re-select last item, if it was selected
self.list_results.setCurrentRow(prev_selected_index)
self._on_theme_selected()
else:
# Return sidebar info panel to defaults if nothing was selected
self.btn_install.setDisabled(True)
for lbl in [
self.lbl_author_name,
self.lbl_description,
self.lbl_version,
self.lbl_requires,
self.lbl_last_update,
]:
lbl.setText("")
self.lbl_theme_name.setText("Click a theme to get started")
self.btn_uninstall.setVisible(False)
self.btn_install.setVisible(True)
self.btn_install.setDisabled(True)
self.rebuilt.emit()
PchumLog.debug("Rebuilt emitted")

View file

@ -361,6 +361,12 @@ with a backup from: <a href='%s'>%s</a></h3></html>"
def irc_compatibility_mode(self): def irc_compatibility_mode(self):
return self.config.get("irc_compatibility_mode", False) return self.config.get("irc_compatibility_mode", False)
def theme_repo_url(self):
return self.config.get(
"theme_repo_url",
"https://raw.githubusercontent.com/mocchapi/pesterchum-themes/main/db.json",
)
def force_prefix(self): def force_prefix(self):
return self.config.get("force_prefix", True) return self.config.get("force_prefix", True)