This commit is contained in:
Alex 2024-04-15 13:21:43 +00:00
commit a08c62a976
12 changed files with 1115 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__
thumbnails
scan.json
config.ini

33
__main__.py Normal file
View File

@ -0,0 +1,33 @@
import pip, sys, os
# check if PyQt6 is installed
print('### Checking install of PyQt6 ###')
try:
from PyQt6 import QtCore, QtGui, QtWidgets
print("### Check passed ###")
except ImportError:
print("PyQt6 is not installed. Installing PyQt6...")
pip.main(['install', 'PyQt6'])
print("### PyQt6 installed ###")
from PyQt6.QtWidgets import QApplication
import traceback, configparser
from main_window import MainWindow
def folderCheck(name):
if not os.path.exists(name):
os.makedirs(name)
if __name__ == '__main__':
folderCheck("thumbnails")
# configparser
try:
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
except Exception as e:
print("### Init Error ###")
traceback.print_exc()

69
action_runner.py Normal file
View File

@ -0,0 +1,69 @@
from PyQt6.QtWidgets import (
QDialog, QLabel, QFrame,
QVBoxLayout
)
from PyQt6.QtCore import Qt
import subprocess, traceback, threading
class RunningDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.hide()
self.setObjectName("runningdialog")
self.setWindowTitle("Running")
# always on top
self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
self.setFixedSize(600, 1)
# disable resizing
self.setFixedSize(self.size())
# disable close button
self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False)
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
class ActionRunner:
def __init__(self, command: str, args: list[str] = []):
self.command = command
self.args = args
def run(self):
running_dialog = RunningDialog(None)
running_dialog.show()
running_dialog.setWindowTitle("Running: " + self.command + " " + " ".join(self.args) )
try:
process=subprocess.Popen(
" ".join([self.command] + self.args),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
output = process.stdout.readlines()
err = process.stderr.readlines()
if output == []:
print("Output:", output)
print("err:")
for e in err:
print(e.decode("utf-8"))
raise Exception("Error while invoking command: " + self.command + " " + " ".join(self.args))
lines = []
for line in output:
l = line.decode("utf-8")
lines.append(l)
result = "".join(lines)
# remove last newline
if result.endswith("\n"):
result = result[:-1]
running_dialog.close()
return result
except Exception as e:
running_dialog.close()
print("### ActionRunner Error ###")
print(e)
traceback.print_exc()
return None

31
config.py Normal file
View File

@ -0,0 +1,31 @@
import json, os
class configManager():
def __init__(self):
print("Loading config...")
self.config = {
"config_version": 1,
"music_folder": "C:/Users/username/Music",
}
def setConfigTemplate(self, template):
self.config = template
def load_config(self):
# load config
if os.path.exists("config.json"):
with open("config.json", "r") as config_file:
self.config = json.load(config_file)
else:
self.save_config()
def save_config(self):
# save config
with open("config.json", "w") as config_file:
json.dump(self.config, config_file)
def get(self, key):
return self.config.get(key, None)
def set(self, key, value):
self.config[key] = value

BIN
default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

112
dialogs.py Normal file
View File

@ -0,0 +1,112 @@
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QLabel, QPushButton,
QWidget, QFrame, QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
QDialog, QFileDialog
)
from PyQt6.QtCore import Qt
class SimpleHeader(QFrame):
def __init__(self, parent) -> None:
super().__init__(parent)
self.setObjectName("SimpleHeader")
self.setStyleSheet("#SimpleHeader { border: 1px solid #00000000; border-bottom-color: #444; }")
self.parent = parent
self.setFixedHeight(24)
self.label = QLabel("Simple Header", self)
self.label.move(12, 0)
self.label.resize( self.width() - 24, 24)
def resizeEvent(self, event):
self.label.resize( self.width() - 24, 24)
self.resize(self.parent.width(), 24 )
def setLabel(self, text):
self.label.setText(text)
class ErrorPopup(QDialog):
def __init__(self, parent, message, trace, CanIgnore=True, buttonLayout=0) -> None:
super().__init__(parent)
self.parent = parent
self.message = message
self.trace = trace
self.canTerminate = CanIgnore
self.setWindowTitle("Error")
self.setGeometry(000, 150, 600, 300)
# reset styles
# self.setStyleSheet("")
# self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
self.setWindowModality(Qt.WindowModality.WindowModal)
self.header = SimpleHeader(self)
self.header.setLabel(self.message)
self.header.move(0, 0)
# self.label = QLabel(self.message, self)
# self.label.move(12, 0)
# self.label.resize( self.width() - 24, 24)
self.trace = QLabel(self.trace, self)
self.trace.move(12, 24)
self.trace.resize( self.width() - 24, 24)
match buttonLayout:
case 0:
pass
self.ignore = QPushButton(CanIgnore and "Ignore" or "Critical Error", self)
self.ignore.move(12, self.height() - 36)
self.ignore.resize( self.width() - 24, 24)
self.ignore.clicked.connect(self.close)
self.ignore.setDisabled(not CanIgnore)
self.btn_terminate = QPushButton("Terminate", self)
self.btn_terminate.move(12, self.height() - 60)
self.btn_terminate.resize( self.width() - 24, 24)
self.btn_terminate.clicked.connect(self.terminate)
def resizeEvent(self, event):
# self.move(int(self.parent.width()/2) - int(self.width()/2), int(self.parent.height()/2) - int(self.height()/2))
self.header.resize( self.width(), 24)
self.trace.resize( self.width() - 24, self.height() - 60)
self.ignore.move(12, self.height() - 36)
self.ignore.resize( int(self.width()/2) - 24, 24)
self.btn_terminate.move(int(self.width()/2) + 12, self.height() - 36)
self.btn_terminate.resize( int(self.width()/2) - 24, 24)
def close(self):
super().close()
def terminate(self):
self.parent.close()
super().close()
exit(1)
# on window close
def closeEvent(self, event):
if self.canTerminate:
self.close()
else:
self.terminate()
class ListDialog(QDialog):
def __init__(self, parent = None, dialogHeader = None, header = None, data = None):
super(ListDialog, self).__init__(parent)
self.setWindowTitle(dialogHeader)
layout = QVBoxLayout()
layout.addWidget(QLabel(header))
# scroll area
scroll = QScrollArea()
widget = QWidget()
widget.setLayout(QVBoxLayout())
layout.addWidget(scroll)
self.setLayout(layout)
for item in data:
widget.layout().addWidget(QLabel(item))
scroll.setWidget(widget)
self.exec()

26
events.py Normal file
View File

@ -0,0 +1,26 @@
class Event():
def __init__(self):
self.__listeners = []
@property
def on(self, event, callback):
self.__listeners.append({event: callback})
def emit(self, event, data):
for listener in self.__listeners:
if event in listener:
# is this a awaitable?
if hasattr(listener[event], "__await__"):
listener[event](data)
else:
listener[event](data)
def addListener(self, event, callback):
if callback in self.__listeners:
return
self.__listeners.append({event: callback})
def removeListener(self, event, callback):
if callback not in self.__listeners:
return
self.__listeners.remove({event: callback})

176
extractor.py Normal file
View File

@ -0,0 +1,176 @@
import os, json
import traceback
def escape_file_name(fname):
"""
Helper function to safely convert the file name (a.k.a. escaping) with
spaces which can cause issues when passing over to the command line.
"""
result = fname.replace('\"', '\\"') # escapes double quotes first
result = ['"',result,'"']
return "".join(result)
def BytesToReadable(bytes):
sizes = ["B", "KB", "MB", "GB", "TB"]
i = 0
while bytes > 1024:
bytes = bytes / 1024
i += 1
return f"{bytes:.2f} {sizes[i]}"
def get_file_size(file_path):
"""
Returns the size of the file in bytes.
"""
return os.path.getsize(file_path)
import subprocess
def CheckFFPROBE():
try:
process=subprocess.Popen(
" ".join(["ffprobe", "-version"]),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
output = process.stdout.readlines()
err = process.stderr.readlines()
if output == []:
err_msg = "".join(err)
raise Exception("Error while invoking ffprobe:\n" + err_msg)
return True
except Exception as e:
return False
except:
pass
return
def CheckFFMPEG():
try:
process=subprocess.Popen(
" ".join(["ffmpeg", "-version"]),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
output = process.stdout.readlines()
err = process.stderr.readlines()
if output == []:
err_msg = "".join(err)
raise Exception("Error while invoking ffmpeg:\n" + err_msg)
return True
except Exception as e:
return False
except:
pass
return
class ExtractorException(Exception):
def __init__(self, errors):
self.errors = errors
def __str__(self):
return repr(self.errors)
def ExtractMetadata(file):
print("Extracting Metadata: ", file)
AdvancedData = {
"file:" : file,
"data": "Unknown",
}
test = CheckFFPROBE()
if not test:
print("### FFPROBE: Check failed ###")
e = ExtractorException("FFPROBE not found or not installed.")
raise e
try:
escaped_fpath = escape_file_name(file)
process=subprocess.Popen(
" ".join(["ffprobe",
"-show_entries format_tags",
"-print_format json",
escaped_fpath],),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
output = process.stdout.readlines()
err = process.stderr.readlines()
if output == []:
err_msg = "".join(err)
raise Exception("Error while invoking ffprobe: " + err_msg)
ffprobe_json = []
skip_line = True
# cleanse any additional information that is not valid JSON
for line in output:
# string out b' and \r\n
line = line.decode("utf-8")
if line.strip() == '{':
skip_line = False
if not skip_line:
ffprobe_json.append(line)
result = json.loads("".join(ffprobe_json))
return result
except Exception as e:
print("### Extractor Error ###")
print(e)
except:
pass
return AdvancedData
def ExtractMediaData(file, output_file):
"""
Extracts media data from a file using ffmpeg.
"""
try:
escaped_fpath = escape_file_name(file)
# get the absolute path to the output file
current_dir = os.getcwd()
absout = os.path.join(current_dir, output_file)
if os.path.exists(absout):
return absout
print("Extracting Media Data: ", file)
subprocess.Popen(
" ".join(["ffmpeg",
"-i",
escaped_fpath,
"-an",
"-vcodec",
"copy",
f'"{absout}"'],), # output file should be in quotes to avoid spaces in the path 'E:\Downloads\4kvideodownloader\video.mp4
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
return absout
except Exception as e:
print("### Extractor Error ###")
traceback.print_exc()
except:
pass

33
main_window.py Normal file
View File

@ -0,0 +1,33 @@
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QLabel, QVBoxLayout,
QWidget, QFrame, QSplitter, QStackedWidget, QTabWidget
)
from tab_musiclibrary import MusicLibraryTab
from tab_settings import SettingsTab
class MusicLibrary(QFrame):
def __init__(self):
super().__init__()
library = QTabWidget()
library.addTab(MusicLibraryTab(), "Songs")
library.addTab(SettingsTab(), "Settings")
layout = QVBoxLayout()
layout.addWidget(library)
self.setLayout(layout)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
self.setFixedSize(800, 600)
layout = QVBoxLayout()
layout.addWidget( MusicLibrary() )
container = QWidget()
container.setLayout(layout)
container.layout().setContentsMargins(0, 0, 0, 0)
self.setCentralWidget(container)

99
settings_widgets.py Normal file
View File

@ -0,0 +1,99 @@
import traceback
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QLabel, QPushButton,
QWidget, QFrame, QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
QDialog, QFileDialog
)
from PyQt6.QtGui import QPixmap
from PyQt6.QtCore import Qt, QAbstractListModel
from dialogs import ErrorPopup, ListDialog
import os, sys, json
# settings widgets
class SettingBox(QFrame):
def __init__(self, parent, name, description = None):
super().__init__(parent)
self.setObjectName("settingbox")
self.name = name
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
self.label = QLabel(name, self)
self.label.setObjectName("settinglabel")
self.layout.addWidget(self.label)
if description:
self.description = QLabel(description, self)
self.layout.addWidget(self.description)
def AddSetting(self, setting):
self.layout.addWidget(setting)
class SettingButton(QFrame):
def __init__(self, parent, text, description = None):
super().__init__(parent)
self.setObjectName("settingbutton")
self.description = None
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
self.button = QPushButton("Button", self)
self.layout.addWidget(self.button)
self.button.setFixedWidth(100)
self.setting_info_base = QWidget()
self.setting_info = QVBoxLayout(self.setting_info_base)
self.setting_info.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.setting_info_base)
self.label = QLabel(text, self)
self.label.setObjectName("settinglabel")
self.setting_info.addWidget(self.label)
if description:
self.description = QLabel(description, self)
self.setting_info.addWidget(self.description)
def SetText(self, text):
self.label.setText(text)
def SetDescription(self, text):
# check the type of the description
if type(text) == bool:
text = str(text)
if type(text) == list:
text = "\n".join(text)
if type(text) == dict:
text = json.dumps(text, indent=4)
if type(text) == traceback.FrameSummary:
text = traceback.format_tb(text)
if not self.description:
self.description = QLabel(text, self)
self.setting_info.addWidget(self.description)
if self.description:
self.description.setText(text)
def GetButton(self):
return self.button
def SetButtonText(self, text):
self.button.setText(text)
def SetButtonPressed(self, func):
self.button.clicked.connect(func)
def SetButtonWidth(self, width):
self.button.setFixedWidth(width)

301
tab_musiclibrary.py Normal file
View File

@ -0,0 +1,301 @@
import traceback
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QLabel, QPushButton, QLineEdit,
QWidget, QFrame, QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
QDialog, QFileDialog
)
from PyQt6.QtGui import QPixmap
from PyQt6.QtCore import Qt, QAbstractListModel
from dialogs import ErrorPopup, ListDialog
import os, sys, json, time
from extractor import ExtractMetadata, ExtractMediaData
from action_runner import RunningDialog
def calculateAspectRatio(pixmap, width, height, aspectRatioMode):
return pixmap.scaled(width, height, aspectRatioMode)
# E:\Downloads\4kvideodownloader
class FolderScanner:
def __init__(self, path):
self.path = path
def scan(self):
data = []
if not os.path.exists(self.path):
print("Path does not exist: ", self.path)
return data
for root, dirs, files in os.walk(self.path):
for file in files:
if file.endswith(".mp3"):
p = os.path.join(root, file)
metadata = ExtractMetadata(p)
# debug.json
# with open(f"{file}.debug.json", "w") as f:
# json.dump(metadata, f, indent=4)
data.append(metadata)
metadata.update({
"file:": p
})
if "data" not in data:
metadata.update({
"thumbnail": self.ExtractImageData(p, f"thumbnails\\{file}.png"),
})
return data
def ExtractImageData(self, file, output):
try:
return ExtractMediaData(file, output)
except Exception as e:
print("### ExtractImageData Error ###")
traceback.print_exc()
pass
class MusicModel(QAbstractListModel):
def __init__(self, data):
super().__init__()
self._data = data
def rowCount(self, parent):
return len(self._data)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
return self._data[index.row()]
class MusicModelWidget(QFrame):
def __init__(self, parent = None, name = None, path = None):
super(MusicModelWidget, self).__init__(parent)
self.setObjectName(name)
layout = QHBoxLayout()
self.image = image( "default.png" )
self.image.setFixedSize(64, 64)
iteminfo_base = QWidget()
iteminfo = QVBoxLayout(iteminfo_base)
iteminfo.setSpacing(0)
self.itemname = QLabel(name)
self.itemname.setObjectName("itemname")
self.artist = QLabel("Artist")
iteminfo.addWidget(self.itemname)
iteminfo.addWidget(self.artist)
layout.addWidget(self.image)
layout.addWidget(iteminfo_base)
self.setStyleSheet("#itemname { font-size: 16px; }")
self.setLayout(layout)
def openFile(self):
os.startfile(self.data["format"]["filename"])
class image(QFrame):
def __init__(self, path = None):
super().__init__()
if path is None:
path = "default.png"
self.setObjectName("Logo")
self.setFrameShadow(QFrame.Shadow.Raised)
self.setFrameShape(QFrame.Shape.StyledPanel)
self.sceene = QGraphicsScene(self)
self.view = QGraphicsView(self.sceene, self)
self.view.setFixedSize(self.width(), self.height())
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.view.move(0, 0)
# remove border
self.view.setLineWidth(0)
self.view.setMidLineWidth(0)
self.view.setFrameShadow(QFrame.Shadow.Plain)
self.view.setFrameShape(QFrame.Shape.NoFrame)
self.view.setStyleSheet("background-color: transparent;")
self.pixmap = None
self.pixmap = QGraphicsPixmapItem()
self.sceene.addItem(self.pixmap)
try:
self.pixmap.setPixmap(QPixmap(path).scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio))
except Exception as e:
print("### Init Error ###")
traceback.print_exc()
def loadimage(self, path):
try:
print("Setting image: ", path)
self.pixmap.setPixmap(QPixmap(path).scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio))
except Exception as e:
print("### Load Image Error ###")
traceback.print_exc()
def resizeEvent(self, event):
self.view.setFixedSize(self.width(), self.height())
calculateAspectRatio(self.pixmap.pixmap(), self.width(), self.height(), Qt.AspectRatioMode.KeepAspectRatio)
class MusicItem:
def __init__(self):
self.data = None
self.image = None
self.title = None
self.artist = None
self.imagepath = None
def setupData(self, ffprobeJsonData):
self.data = ffprobeJsonData
data_format = self.data["format"]
# get title from the tags
if "tags" in data_format:
if "title" in data_format["tags"]:
self.title = data_format["tags"]["title"]
if "artist" in data_format["tags"]:
self.artist = data_format["tags"]["artist"]
if "thumbnail" in self.data:
self.image = self.data["thumbnail"]
def getImage(self):
return self.image
def openFile(self):
try:
os.startfile(self.data['file:'])
except Exception as e:
print("Unable to open file: ", e)
traceback.print_exc()
class MusicLibraryTab(QFrame):
def __init__(self):
super().__init__()
self.list = QListWidget()
self.scanpathInput = QLineEdit()
self.scanpathInput.setPlaceholderText("Path to scan")
self.scanbutton = QPushButton("Scan")
self.scanbutton.clicked.connect(self.scan)
self.list.setObjectName("list")
self.list.setLineWidth(2)
self.list.setMidLineWidth(2)
self.list.setFrameShadow(QFrame.Shadow.Raised)
self.list.setFrameShape(QFrame.Shape.StyledPanel)
self.list.doubleClicked.connect(lambda: self.list.currentItem().data(0).openFile())
scan_base = QWidget()
scan_layout = QHBoxLayout(scan_base)
scan_layout.setSpacing(0)
scan_layout.setContentsMargins(0, 0, 0, 0)
scan_layout.addWidget(self.scanbutton)
scan_layout.addWidget(self.scanpathInput)
layout = QVBoxLayout()
layout.setSpacing(0)
layout.addWidget(scan_base)
layout.addWidget(self.list)
self.setLayout(layout)
# check if scan.json exists
if os.path.exists("scan.json"):
self.scan("scan.json")
def scan(self, file = None):
path = self.scanpathInput.text()
musicscanner = FolderScanner(path)
runningDialog = RunningDialog(None)
try:
# clear list
self.list.clear()
runningDialog.show()
runningDialog.setWindowTitle("Scanning: " + path)
scandate = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
if not file:
# formated date time
scandate = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
scan = musicscanner.scan()
else:
loadeddata = json.load(open(file))["scan"]
scan = loadeddata
unknownData = False
unknownFiles = []
# debug.scan.json
with open("scan.json", "w") as f:
settings={
"path": self.scanpathInput.text(),
"scanned": scandate
}
finaldata ={
"settings": settings,
"scan": scan
}
json.dump(finaldata, f, indent=4)
# check if any of the data is unknown
for data in scan:
# does data key exist
if "data" in data:
# is data key unknown
if data["data"] == "Unknown":
print("Unknown data: ", data["file:"])
unknownData = True
unknownFiles.append(data["file:"])
#
music = MusicItem()
try:
music.setupData(data)
except Exception as e:
print("Skipping setup data: ", e)
item = QListWidgetItem(self.list)
item.setData(0, music)
item_frame = MusicModelWidget(self.list, music.title)
if music.image:
runningDialog.setWindowTitle("Loading Image: " + music.image)
item_frame.image.loadimage(music.image)
item.setSizeHint(item_frame.sizeHint())
if music.artist:
item_frame.artist.setText(music.artist)
else:
item_frame.artist.setText("Unknown Artist")
# double click
# item_frame.itemname.mouseDoubleClickEvent = os.startfile(data["format"]["filename"])
self.list.addItem(item)
self.list.setItemWidget(item, item_frame)
if unknownData:
ListDialog(None, "Unknown Files", "Some files metadata could not be extracted", unknownFiles)
runningDialog.close()
except Exception as e:
runningDialog.close()
traceback.print_exc()
try:
ErrorPopup(self, str(e), str(traceback.format_exc(
limit=1, chain=True
))).exec()
except Exception as e:
print("Unable to show error popup: ", e)
pass
def resizeEvent(self, event):
self.list.resize(self.width(), self.height())

231
tab_settings.py Normal file
View File

@ -0,0 +1,231 @@
import traceback, webbrowser
from PyQt6.QtWidgets import (
QLabel, QPushButton, QWidget, QFrame,
QSplitter, QStackedWidget, QListWidget, QListWidgetItem,
QGraphicsScene, QGraphicsView, QGraphicsPixmapItem,
QLayout, QScrollArea, QVBoxLayout, QHBoxLayout,
)
from PyQt6.QtGui import QPixmap
from PyQt6.QtCore import Qt, QAbstractListModel
from dialogs import ErrorPopup, ListDialog
from settings_widgets import SettingBox, SettingButton
from extractor import CheckFFPROBE
import os, sys, json
from action_runner import ActionRunner, RunningDialog
class SidebarPage(QFrame):
def __init__(self, name):
super().__init__()
self.name = name
self.setObjectName("SidebarPage")
self.setStyleSheet("#SidebarPage { border: 1px solid #00000000; border-left-color: #444; }")
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
def AddWidget(self, widget) -> None:
self.layout.addWidget(widget)
def AddLayout(self, layout) -> None:
self.layout.addLayout(layout)
def AddSpacer(self) -> None:
self.layout.addStretch(1)
class SidebarButton(QPushButton):
def __init__(self, text, parent):
super().__init__(text, parent)
self.defaultstyle = "border: 1px solid #00000000; padding: 4px 8px; color: #000; text-align: left;"
self.setObjectName("SidebarButton")
self.setStyleSheet("#SidebarButton { " + self.defaultstyle + " background-color: #00000000; }")
def SetSelected(self, selected):
if selected:
self.setStyleSheet("#SidebarButton { " + self.defaultstyle + " background-color: #444; color: #fff; }")
else:
self.setStyleSheet("#SidebarButton { " + self.defaultstyle + " background-color: #00000000; }")
class Sidebar(QFrame):
def __init__(self):
super().__init__()
self.pages = []
self.setObjectName("Sidebar")
self.mainlayout = QHBoxLayout()
self.mainlayout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.mainlayout)
self.left_base = QWidget()
self.left_base.setFixedWidth(200)
self.left = QVBoxLayout(self.left_base)
# gap between buttons : 0px
self.left.setSpacing(0)
self.left.setAlignment(Qt.AlignmentFlag.AlignTop)
self.left.setContentsMargins(0, 0, 0, 0)
self.right_base = QWidget()
self.right = QVBoxLayout(self.right_base)
self.right.setAlignment(Qt.AlignmentFlag.AlignTop)
self.right.setContentsMargins(0, 0, 0, 0)
self.mainlayout.addWidget(self.left_base)
self.mainlayout.addWidget(self.right_base)
def swisthPage(self, index) -> QFrame:
self.showPage(self.pages[index])
return self.pages[index]
def AddButton(self, text) -> QPushButton:
button = SidebarButton(text, self)
self.left.addWidget(button)
return button
def showPage(self, page) -> None:
for p in self.pages:
p.hide()
# get buttons
for b in self.left_base.children():
if isinstance(b, QPushButton):
b.SetSelected(False)
if b.text() == page.name:
b.SetSelected(True)
page.show()
def AddPage(self, name, page) -> None:
# hide all pages
button = self.AddButton(name)
button.clicked.connect(lambda: self.showPage(page))
for p in self.pages:
p.hide()
self.right.addWidget(page)
self.pages.append(page)
return page
class subpage_general(QFrame):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.setStyleSheet(
"#subpage_general_title { font-size: 24px; font-weight: bold; }"
)
header = QLabel("General")
header.setObjectName("subpage_general_title")
self.layout.addWidget(header)
self.layout.addWidget(QLabel("General settings for the application."))
# settings
self.layout.addWidget(SettingBox(self, "No general settings available."))
class subpage_requirements(QFrame):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.setStyleSheet(
"#subpage_requirements_title { font-size: 24px; font-weight: bold; }"
)
header = QLabel("Requirements")
header.setObjectName("subpage_requirements_title")
self.layout.addWidget(header)
self.layout.addWidget(QLabel("Important applications and libraries required for the application to function."))
setting_echo_test = SettingButton(self, "Echo Test", "Test the echo command.")
setting_update_winget = SettingButton(self, "Manual download and install", "Download and install winget manually.")
setting_install_ffprobe = SettingButton(self, "Install FFMPEG", "Install or update FFMPEG ( includes FFPROBE ) for media processing.")
self.layout.addWidget(setting_echo_test)
self.layout.addWidget(setting_update_winget)
self.layout.addWidget(setting_install_ffprobe)
if not CheckFFPROBE():
setting_install_ffprobe.SetText("Required: Install FFMPEG")
setting_echo_test.SetButtonText("Run action")
echo_test = ActionRunner("echo", ["Hello World!"])
ffprobe_installer = ActionRunner("winget", ["install", "-e", "--id", "Gyan.FFmpeg"])
def echo_test_button():
run = echo_test.run()
setting_echo_test.SetText("Echo Test Complete")
setting_echo_test.SetDescription(run)
setting_echo_test.SetButtonPressed(echo_test_button)
setting_update_winget.SetButtonText("Start download")
def update_winget():
# https://aka.ms/getwinget
webbrowser.open("https://aka.ms/getwinget")
setting_update_winget.SetButtonPressed(update_winget)
setting_install_ffprobe.SetButtonText("Run action")
def install_ffprobe():
installed = CheckFFPROBE()
print("Status:", installed)
print(installed and "FFPROBE Upadting." or "FFPROBE Installing.")
ffprobe_installer.run()
print(installed and "FFPROBE Upadted." or "FFPROBE Installed.")
setting_install_ffprobe.SetText( installed and "FFPROBE Upadted." or "FFPROBE Installed." )
setting_install_ffprobe.SetButtonPressed(install_ffprobe)
class SettingsTab(QFrame):
def __init__(self):
super().__init__()
self.setStyleSheet(
"#settinglabel { font-size: 16px; }"
"#settingbutton, #settingbox { border: 1px solid #8c8c8c; padding: 4px 8px; border-radius: 4px;}"
)
try:
self.sidebar = Sidebar()
# general = self.sidebar.AddButton("General")
# general.clicked.connect(lambda: print("General"))
# general = self.sidebar.AddButton("Requirements")
# general.clicked.connect(lambda: print("Requirements"))
page_general = SidebarPage( "General" )
page_general.AddWidget(subpage_general())
page_general.AddSpacer()
page_requirements = SidebarPage( "Requirements" )
page_requirements.AddWidget(subpage_requirements())
page_requirements.AddSpacer()
self.sidebar.AddPage("General", page_general)
self.sidebar.AddPage("Requirements", page_requirements)
self.sidebar.swisthPage(0)
self.layout = QHBoxLayout()
self.layout.addWidget(self.sidebar)
self.setLayout(self.layout)
self.setObjectName("Settings")
except Exception as e:
print("### SettingsTab Error ###")
traceback.print_exc()
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("SettingsTab Error"))
self.setLayout(self.layout)