i forgot to commit for maaany hours. its pretty much done i think :D
This commit is contained in:
parent
486f2dad71
commit
e6e8f4c1ef
8 changed files with 405 additions and 168 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
|
# 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
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 |
30
menus.py
30
menus.py
|
@ -12,7 +12,7 @@ except ImportError:
|
||||||
|
|
||||||
import ostools
|
import ostools
|
||||||
import parsetools
|
import parsetools
|
||||||
from thememanager import ThemeManagerWidget
|
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
|
||||||
|
@ -1454,16 +1454,34 @@ 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)
|
||||||
|
def reset_themeBox():
|
||||||
|
avail_themes = self.config.availableThemes()
|
||||||
|
PchumLog.debug("Resetting themeself.themeBox")
|
||||||
|
self.themeBox.clear()
|
||||||
notheme = theme.name not in avail_themes
|
notheme = theme.name not in avail_themes
|
||||||
for i, t in enumerate(avail_themes):
|
for i, t in enumerate(avail_themes):
|
||||||
self.themeBox.addItem(t)
|
self.themeBox.addItem(t)
|
||||||
if (not notheme and t == theme.name) or (notheme and t == "pesterchum"):
|
if (not notheme and t == theme.name) or (notheme and t == "pesterchum"):
|
||||||
self.themeBox.setCurrentIndex(i)
|
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 = 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())
|
||||||
|
|
||||||
|
@ -1676,8 +1694,12 @@ 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.themeManager)
|
||||||
layout_theme.addWidget(self.ghostchum)
|
layout_theme.addWidget(self.ghostchum)
|
||||||
self.pages.addWidget(widget)
|
self.pages.addWidget(widget)
|
||||||
|
|
11
ostools.py
11
ostools.py
|
@ -51,7 +51,13 @@ 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")
|
||||||
|
themes = os.path.join(datadir, "themes")
|
||||||
|
# ~lisanne Datadir/themes for repository themes
|
||||||
|
# Apparently everything checks this folder for themes already
|
||||||
|
# So hopefully im not plugging into an existng system on accident
|
||||||
|
|
||||||
js_pchum = os.path.join(datadir, "pesterchum.js")
|
js_pchum = os.path.join(datadir, "pesterchum.js")
|
||||||
|
js_manifest = os.path.join(datadir, "manifest.js")
|
||||||
|
|
||||||
dirs = [datadir, profile, quirks, logs, errorlogs, backup]
|
dirs = [datadir, profile, quirks, logs, errorlogs, backup]
|
||||||
for d in dirs:
|
for d in dirs:
|
||||||
|
@ -59,8 +65,9 @@ def validateDataDir():
|
||||||
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):
|
||||||
|
with open(filepath, "w") as f:
|
||||||
f.write("{}")
|
f.write("{}")
|
||||||
|
|
||||||
|
|
||||||
|
|
363
theme_repo_manager.py
Normal file
363
theme_repo_manager.py
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
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
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
all_themes = self.config.availableThemes()
|
||||||
|
for theme_name in self.manifest:
|
||||||
|
if not theme_name in all_themes:
|
||||||
|
PchumLog.warning("Theme %s from the manifest seems to have been deleted" % theme_name)
|
||||||
|
|
||||||
|
def download_theme(self, theme_name):
|
||||||
|
PchumLog.info("Downloading %s" % theme_name)
|
||||||
|
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 has_update(self, theme_name):
|
||||||
|
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):
|
||||||
|
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 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())
|
||||||
|
print([reply.error()])
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
original_url = reply.request().url().url()
|
||||||
|
# Check if zipfile or database fetch
|
||||||
|
if original_url in self.downloads:
|
||||||
|
theme = self.downloads[original_url]
|
||||||
|
self._handle_downloaded_zip(bytes(reply.readAll()), theme)
|
||||||
|
self.downloads.pop(original_url)
|
||||||
|
self.manifest[theme['name']] = theme
|
||||||
|
self.save_manifest()
|
||||||
|
self.manifest_updated.emit(self.manifest)
|
||||||
|
else:
|
||||||
|
as_json = bytes(reply.readAll()).decode("utf-8")
|
||||||
|
self.database = json.loads(as_json)
|
||||||
|
self.database_entries = {}
|
||||||
|
if not self.is_database_valid():
|
||||||
|
PchumLog.error('Incorrect database format, missing "entries"')
|
||||||
|
self.errored.emit('Incorrect database format, missing "entries"')
|
||||||
|
return
|
||||||
|
self.database_entries = {}
|
||||||
|
for dbitem in self.database['entries']:
|
||||||
|
self.database_entries[dbitem['name']] = dbitem
|
||||||
|
self.database_refreshed.emit(self.database)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
PchumLog.error("Could not decode JSON: %s" % e)
|
||||||
|
self.errored.emit("Could not decode JSON: %s" % e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_downloaded_zip(self, zip_buffer, theme):
|
||||||
|
# ~liasnne TODO: i dont know if this is inside a thread or not so
|
||||||
|
# probably check to make sure it is
|
||||||
|
# when its not 5 am
|
||||||
|
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 == 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.itemClicked.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(False)
|
||||||
|
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)
|
||||||
|
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.download_theme(theme['name'])
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(QtWidgets.QListWidgetItem)
|
||||||
|
def _on_theme_selected(self, item):
|
||||||
|
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 )
|
||||||
|
self.lbl_requires.setText( ("Requires %s" % theme['inherits']) if theme['inherits'] else "")
|
||||||
|
self.lbl_last_update.setText( "Released on: " + datetime.fromtimestamp(theme['updated']).strftime("%d/%m/%Y, %H:%M") )
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(dict)
|
||||||
|
def _on_database_refreshed(self,_):
|
||||||
|
self.rebuild()
|
||||||
|
|
||||||
|
def rebuild(self):
|
||||||
|
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)
|
||||||
|
|
||||||
|
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")
|
156
thememanager.py
156
thememanager.py
|
@ -1,156 +0,0 @@
|
||||||
|
|
||||||
import logging
|
|
||||||
from os import remove
|
|
||||||
|
|
||||||
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 (menus.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
|
|
||||||
|
|
||||||
# ~Lisanne
|
|
||||||
# This file has all the stuff needed to use a theme repository
|
|
||||||
# - ThemeManagerWidget, a GUI widget that
|
|
||||||
# - class for fetching & parsing database
|
|
||||||
# - class for handling installs & uninstalls. it also updates the manifest
|
|
||||||
# - manifest variable / json which keeps track of installed themes & their version
|
|
||||||
|
|
||||||
def unzip_file(path_zip, path_destination):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ThemeManagerWidget(QtWidgets.QWidget):
|
|
||||||
manifest = {}
|
|
||||||
database = {}
|
|
||||||
config = None
|
|
||||||
promise = None
|
|
||||||
def __init__(self, config, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.config = config
|
|
||||||
self.setupUI()
|
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(QtWidgets.QListWidgetItem)
|
|
||||||
def themeSelected(self, item):
|
|
||||||
print("Done!")
|
|
||||||
print(item)
|
|
||||||
pass
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(QtNetwork.QNetworkReply)
|
|
||||||
def receiveReply(self, reply):
|
|
||||||
print(reply)
|
|
||||||
breakpoint
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot()
|
|
||||||
def refresh(self):
|
|
||||||
print("Starting refresh @ ",self.config.theme_repo_url())
|
|
||||||
request = QtNetwork.QNetworkRequest()
|
|
||||||
request.setUrl(QtCore.QUrl(self.config.theme_repo_url()))
|
|
||||||
manager = QtNetwork.QNetworkAccessManager()
|
|
||||||
promise = manager.get(request)
|
|
||||||
manager.finished[QtNetwork.QNetworkReply].connect(self.receiveReply)
|
|
||||||
|
|
||||||
|
|
||||||
def setupUI(self):
|
|
||||||
self.layout_main = QtWidgets.QVBoxLayout(self)
|
|
||||||
self.setLayout(self.layout_main)
|
|
||||||
|
|
||||||
# Search bar
|
|
||||||
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.MinimumExpanding,
|
|
||||||
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.list_results.itemActivated.connect(self.themeSelected)
|
|
||||||
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("Theme name here")
|
|
||||||
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("Author: ?")
|
|
||||||
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("Description")
|
|
||||||
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("Requires: pibby")
|
|
||||||
self.lbl_requires.setTextInteractionFlags(_flag_selectable)
|
|
||||||
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("Version: 2 (installed: 0)")
|
|
||||||
self.lbl_version.setTextInteractionFlags(_flag_selectable)
|
|
||||||
layout_vbox_scroll_insides.addWidget(self.lbl_version)
|
|
||||||
|
|
||||||
# Last update time
|
|
||||||
self.lbl_last_update = QtWidgets.QLabel("DD/MM/YYYY HH:MM")
|
|
||||||
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)
|
|
||||||
layout_vbox_details.addWidget(self.btn_uninstall)
|
|
||||||
# "Install" button. can also say "Update" if an update is availible
|
|
||||||
self.btn_install = QtWidgets.QPushButton("Install", self)
|
|
||||||
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(self.refresh)
|
|
||||||
self.layout_main.addWidget(self.btn_refresh)
|
|
||||||
|
|
Loading…
Reference in a new issue