Merge pull request #146 from mocchapi/theme-repository-integration
Theme repository integration
This commit is contained in:
commit
22cda74bb9
9 changed files with 635 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -179,3 +179,4 @@ cython_debug/
|
|||
# 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.
|
||||
#.idea/
|
||||
|
||||
|
|
BIN
img/download_done.png
Normal file
BIN
img/download_done.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 B |
BIN
img/download_pending.png
Normal file
BIN
img/download_pending.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 B |
BIN
img/download_update.png
Normal file
BIN
img/download_update.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 B |
51
menus.py
51
menus.py
|
@ -12,6 +12,7 @@ except ImportError:
|
|||
|
||||
import ostools
|
||||
import parsetools
|
||||
from theme_repo_manager import ThemeManagerWidget
|
||||
from generic import RightClickList, RightClickTree, MultiTextDialog
|
||||
from dataobjs import pesterQuirk, PesterProfile, PesterHistory
|
||||
from memos import TimeSlider, TimeInput
|
||||
|
@ -1425,6 +1426,13 @@ class PesterOptions(QtWidgets.QDialog):
|
|||
layout_5.addWidget(QtWidgets.QLabel("Minutes before Idle:"))
|
||||
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.addItem("Once a Day")
|
||||
# self.updateBox.addItem("Once a Week")
|
||||
|
@ -1446,15 +1454,36 @@ class PesterOptions(QtWidgets.QDialog):
|
|||
if not parent.randhandler.running:
|
||||
self.randomscheck.setEnabled(False)
|
||||
|
||||
avail_themes = self.config.availableThemes()
|
||||
self.themeBox = QtWidgets.QComboBox(self)
|
||||
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)
|
||||
|
||||
def reset_themeBox():
|
||||
avail_themes = self.config.availableThemes()
|
||||
PchumLog.debug("Resetting themeself.themeBox")
|
||||
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.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.setChecked(self.config.ghostchum())
|
||||
|
||||
|
@ -1656,6 +1685,7 @@ class PesterOptions(QtWidgets.QDialog):
|
|||
layout_idle = QtWidgets.QVBoxLayout(widget)
|
||||
layout_idle.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
||||
layout_idle.addLayout(layout_5)
|
||||
layout_idle.addLayout(layout_repo_url)
|
||||
layout_idle.addLayout(layout_6)
|
||||
# if not ostools.isOSXLeopard():
|
||||
# layout_idle.addWidget(self.mspaCheck)
|
||||
|
@ -1666,8 +1696,13 @@ class PesterOptions(QtWidgets.QDialog):
|
|||
layout_theme = QtWidgets.QVBoxLayout(widget)
|
||||
layout_theme.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
||||
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)
|
||||
self.pages.addWidget(widget)
|
||||
|
||||
|
|
17
ostools.py
17
ostools.py
|
@ -51,17 +51,24 @@ def validateDataDir():
|
|||
logs = os.path.join(datadir, "logs")
|
||||
errorlogs = os.path.join(datadir, "errorlogs")
|
||||
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:
|
||||
if not os.path.isdir(d) or not os.path.exists(d):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
# pesterchum.js
|
||||
if not os.path.exists(js_pchum):
|
||||
with open(js_pchum, "w") as f:
|
||||
f.write("{}")
|
||||
for filepath in [js_pchum, js_manifest]:
|
||||
if not os.path.exists(filepath):
|
||||
with open(filepath, "w") as f:
|
||||
f.write("{}")
|
||||
|
||||
|
||||
def getDataDir():
|
||||
|
|
|
@ -3101,6 +3101,11 @@ class PesterWindow(MovingWindow):
|
|||
if idlesetting != curidle:
|
||||
self.config.set("idleTime", 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
|
||||
ghostchumsetting = self.optionmenu.ghostchum.isChecked()
|
||||
curghostchum = self.config.ghostchum()
|
||||
|
@ -4194,6 +4199,7 @@ class MainProgram(QtCore.QObject):
|
|||
self.irc.finished.disconnect(self.restartIRC)
|
||||
|
||||
def showUpdate(self, q):
|
||||
# ~Lisanne: Doesn't seem to be used anywhere, old update notif mechanism?
|
||||
new_url = q.get()
|
||||
if new_url[0]:
|
||||
self.widget.pcUpdate.emit(new_url[0], new_url[1])
|
||||
|
|
567
theme_repo_manager.py
Normal file
567
theme_repo_manager.py
Normal 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")
|
|
@ -361,6 +361,12 @@ with a backup from: <a href='%s'>%s</a></h3></html>"
|
|||
def irc_compatibility_mode(self):
|
||||
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):
|
||||
return self.config.get("force_prefix", True)
|
||||
|
||||
|
|
Loading…
Reference in a new issue