1
0
mirror of https://github.com/darkzoul5/YoutubePlaylistSync.git synced 2026-07-04 21:04:01 +03:00

10 Commits

Author SHA1 Message Date
dark_zoul df4c7d504b feat: add app version to about page 2026-06-03 18:07:11 +03:00
dark_zoul ac5a98a09c refactor: about page now uses buttons for links 2026-06-03 18:00:56 +03:00
dark_zoul 811ff45dc9 feat: dynamic navbar width hopefully 2026-06-03 17:56:43 +03:00
dark_zoul c658b9a90d refactor: change about page layout 2026-06-03 17:53:24 +03:00
dark_zoul b06ab55f99 change about page formating 2026-06-03 17:46:30 +03:00
dark_zoul de315d07e0 feat: add issues link to abut page 2026-06-03 17:46:12 +03:00
dark_zoul 4dc7d95123 add about page 2026-06-03 17:42:16 +03:00
darkzoul5 42ba6310a3 Merge pull request #13 from darkzoul5/chore/refresh-yt-dlp
chore: bump yt-dlp to 2026.3.17
2026-06-03 17:16:31 +03:00
dark_zoul 0a49676c72 ci: bump gh action version 2026-06-03 17:15:46 +03:00
dark_zoul 8ec894fc1f ci: change name of yt-dlp workflow 2026-06-03 17:14:51 +03:00
5 changed files with 239 additions and 11 deletions
+8 -2
View File
@@ -107,6 +107,12 @@ jobs:
echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
- name: Write bundled version file
shell: bash
run: |
set -euo pipefail
printf '%s\n' "${{ steps.version.outputs.version }}" > version.txt
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: "3.12" python-version: "3.12"
@@ -124,14 +130,14 @@ jobs:
run: | run: |
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$ws = "${{ github.workspace }}" $ws = "${{ github.workspace }}"
pyinstaller --noconfirm --onefile --noconsole --name "ytpl-sync" --icon "$ws/assets/icon.ico" --add-data "$ws/assets/icon.png;assets" --distpath "dist/pyinstaller" --workpath "build/pyinstaller" --specpath "build/pyinstaller" --paths "src" "ytpl-sync-entry.py" pyinstaller --noconfirm --onefile --noconsole --name "ytpl-sync" --icon "$ws/assets/icon.ico" --add-data "$ws/assets/icon.png;assets" --add-data "$ws/version.txt;." --distpath "dist/pyinstaller" --workpath "build/pyinstaller" --specpath "build/pyinstaller" --paths "src" "ytpl-sync-entry.py"
- name: Build binary (Linux) - name: Build binary (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
pyinstaller --noconfirm --onefile --noconsole --name "ytpl-sync" --icon "${GITHUB_WORKSPACE}/assets/icon.png" --add-data "${GITHUB_WORKSPACE}/assets/icon.png:assets" --distpath "dist/pyinstaller" --workpath "build/pyinstaller" --specpath "build/pyinstaller" --paths "src" "ytpl-sync-entry.py" pyinstaller --noconfirm --onefile --noconsole --name "ytpl-sync" --icon "${GITHUB_WORKSPACE}/assets/icon.png" --add-data "${GITHUB_WORKSPACE}/assets/icon.png:assets" --add-data "${GITHUB_WORKSPACE}/version.txt:." --distpath "dist/pyinstaller" --workpath "build/pyinstaller" --specpath "build/pyinstaller" --paths "src" "ytpl-sync-entry.py"
- name: Stage package - name: Stage package
shell: bash shell: bash
@@ -1,5 +1,4 @@
name: update yt-dlp and open PR name: update yt-dlp
on: on:
schedule: schedule:
- cron: "0 10 * * *" - cron: "0 10 * * *"
@@ -77,7 +76,7 @@ jobs:
- name: Create or update pull request - name: Create or update pull request
if: steps.detect.outputs.needs_update == 'true' if: steps.detect.outputs.needs_update == 'true'
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
branch: chore/refresh-yt-dlp branch: chore/refresh-yt-dlp
commit-message: "chore: bump yt-dlp to ${{ steps.detect.outputs.latest_yt_dlp }}" commit-message: "chore: bump yt-dlp to ${{ steps.detect.outputs.latest_yt_dlp }}"
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
import sys
from pathlib import Path
def _resource_base() -> Path:
# PyInstaller sets sys._MEIPASS to the temp extraction dir.
base = getattr(sys, "_MEIPASS", None)
if base:
return Path(str(base))
return Path.cwd()
def _read_text(path: Path) -> str | None:
try:
if path.exists():
text = path.read_text(encoding="utf-8").strip()
return text or None
except Exception:
pass
return None
def get_app_version() -> str:
"""
Returns the packaged app version.
In release builds this reads from `version.txt` bundled into the EXE.
"""
candidates = [
Path("version.txt"),
_resource_base() / "version.txt",
]
for candidate in candidates:
text = _read_text(candidate)
if text:
return text
return "dev"
+65 -6
View File
@@ -15,6 +15,7 @@ from .pages.playlists import PlaylistManagerPage
from .pages.queue import QueuePage from .pages.queue import QueuePage
from .pages.logs import LogsPage from .pages.logs import LogsPage
from .pages.settings import SettingsPage from .pages.settings import SettingsPage
from .pages.about import AboutPage
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
@@ -38,29 +39,37 @@ class MainWindow(QtWidgets.QMainWindow):
# Sidebar navigation # Sidebar navigation
self._nav = QtWidgets.QListWidget() self._nav = QtWidgets.QListWidget()
self._nav.setObjectName("sidebar") self._nav.setObjectName("sidebar")
self._nav.setFixedWidth(220)
self._nav.setSpacing(2) self._nav.setSpacing(2)
self._nav.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
)
self._nav.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
self._nav.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) self._nav.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection)
self._nav.model().rowsInserted.connect(self._update_sidebar_width)
self._nav.model().dataChanged.connect(self._update_sidebar_width)
self._nav.model().rowsRemoved.connect(self._update_sidebar_width)
self._stack = QtWidgets.QStackedWidget() self._stack = QtWidgets.QStackedWidget()
self._playlists_page = PlaylistManagerPage(self._settings) self._playlists_page = PlaylistManagerPage(self._settings)
self._queue_page = QueuePage() self._queue_page = QueuePage()
self._logs_page = LogsPage() self._logs_page = LogsPage()
self._settings_page = SettingsPage() self._settings_page = SettingsPage()
self._about_page = AboutPage()
self._pages: list[QtWidgets.QWidget] = [ self._pages: list[QtWidgets.QWidget] = [
self._playlists_page, self._playlists_page,
self._queue_page, self._queue_page,
self._logs_page, self._logs_page,
self._settings_page, self._settings_page,
self._about_page,
] ]
for p in self._pages: for p in self._pages:
self._stack.addWidget(p) self._stack.addWidget(p)
for label in ("Playlists", "Queue", "Logs", "Settings"): for label in ("Playlists", "Queue", "Logs", "Settings", "About"):
item = QtWidgets.QListWidgetItem(label) self._add_sidebar_item(label)
item.setSizeHint(QtCore.QSize(200, 36))
self._nav.addItem(item)
self._nav.currentRowChanged.connect(self._stack.setCurrentIndex) self._nav.currentRowChanged.connect(self._stack.setCurrentIndex)
self._nav.setCurrentRow(0) self._nav.setCurrentRow(0)
@@ -93,6 +102,29 @@ class MainWindow(QtWidgets.QMainWindow):
self._refresh_queue_labels() self._refresh_queue_labels()
self._init_tray() self._init_tray()
QtCore.QTimer.singleShot(0, self._update_sidebar_width)
def _add_sidebar_item(self, label: str) -> None:
item = QtWidgets.QListWidgetItem(label)
self._nav.addItem(item)
self._update_sidebar_width()
def _update_sidebar_width(self, *_args: object) -> None:
metrics = self._nav.fontMetrics()
max_text_width = 0
for row in range(self._nav.count()):
item = self._nav.item(row)
if item is None:
continue
max_text_width = max(max_text_width, metrics.horizontalAdvance(item.text()))
if max_text_width <= 0:
return
frame = self._nav.frameWidth() * 2
padding = 44
target_width = max_text_width + frame + padding
self._nav.setFixedWidth(max(120, min(220, target_width)))
def _tray_config(self) -> dict: def _tray_config(self) -> dict:
# Read from disk so toggles apply immediately (no restart required). # Read from disk so toggles apply immediately (no restart required).
@@ -325,8 +357,11 @@ class MainWindow(QtWidgets.QMainWindow):
self.setStyleSheet( self.setStyleSheet(
""" """
QMainWindow { background: #0f1115; color: #e6e6e6; } QMainWindow { background: #0f1115; color: #e6e6e6; }
QWidget { font-size: 13px; } QWidget { font-size: 13px; color: #e6e6e6; }
QLabel#pageTitle { font-size: 18px; font-weight: 600; padding: 4px 0; } QLabel#pageTitle { font-size: 18px; font-weight: 600; padding: 4px 0; }
QLabel[muted="true"] { color: #aeb6c2; }
QLabel[link="true"] { color: #8fb8ff; }
QLabel[link="true"]:hover { color: #b8d2ff; }
QListWidget#sidebar { QListWidget#sidebar {
background: #0b0d11; background: #0b0d11;
@@ -348,6 +383,30 @@ class MainWindow(QtWidgets.QMainWindow):
gridline-color: #20242d; gridline-color: #20242d;
border: 1px solid #20242d; border: 1px solid #20242d;
} }
QGroupBox {
border: 1px solid #20242d;
border-radius: 10px;
margin-top: 14px;
padding: 12px;
background: #0b0d11;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 6px;
color: #d5dae3;
background: #0b0d11;
}
QFrame#aboutCard {
background: #0b0d11;
border: 1px solid #20242d;
border-radius: 14px;
}
QLabel#cardTitle {
font-size: 15px;
font-weight: 600;
color: #f2f4f8;
}
QHeaderView::section { QHeaderView::section {
background: #0b0d11; background: #0b0d11;
color: #cfd3da; color: #cfd3da;
+126
View File
@@ -0,0 +1,126 @@
from __future__ import annotations
from PySide6 import QtCore, QtGui, QtWidgets
from ...core.utils.version import get_app_version
class AboutPage(QtWidgets.QWidget):
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
super().__init__(parent)
self.setObjectName("aboutPage")
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(14)
title = QtWidgets.QLabel("About")
title.setObjectName("pageTitle")
layout.addWidget(title)
layout.addWidget(self._hero_card())
layout.addWidget(self._project_card())
layout.addWidget(self._suggestions_card())
layout.addStretch(1)
def _card(self) -> tuple[QtWidgets.QFrame, QtWidgets.QVBoxLayout]:
card = QtWidgets.QFrame()
card.setObjectName("aboutCard")
card_layout = QtWidgets.QVBoxLayout(card)
card_layout.setContentsMargins(16, 16, 16, 16)
card_layout.setSpacing(10)
return card, card_layout
def _card_title(self, text: str) -> QtWidgets.QLabel:
label = QtWidgets.QLabel(text)
label.setObjectName("cardTitle")
return label
def _muted_label(self, text: str) -> QtWidgets.QLabel:
label = QtWidgets.QLabel(text)
label.setWordWrap(True)
label.setProperty("muted", True)
return label
def _link_button(self, text: str, url: str) -> QtWidgets.QPushButton:
button = QtWidgets.QPushButton(text)
button.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
button.clicked.connect(
lambda: QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
)
return button
def _hero_card(self) -> QtWidgets.QFrame:
card, layout = self._card()
layout.addWidget(self._card_title("About this project"))
layout.addWidget(
self._muted_label(
"ytpl-sync is a desktop app for keeping local copies of YouTube playlists in sync."
)
)
layout.addWidget(
self._muted_label(
"This is a student project."
)
)
return card
def _project_card(self) -> QtWidgets.QFrame:
card, layout = self._card()
layout.addWidget(self._card_title("Project"))
form = QtWidgets.QFormLayout()
form.setLabelAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
form.setFormAlignment(
QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft
)
form.setHorizontalSpacing(14)
form.setVerticalSpacing(10)
author = self._muted_label("Dark_Zoul")
form.addRow("Author", author)
version_text = get_app_version()
version = self._muted_label(f"v{version_text}" if version_text != "unknown" else version_text)
form.addRow("Version", version)
repo_row = QtWidgets.QHBoxLayout()
repo_row.setContentsMargins(0, 0, 0, 0)
repo_row.setSpacing(10)
repo_row.addWidget(
self._link_button(
"Open",
"https://github.com/darkzoul5/YoutubePlaylistSync",
)
)
repo_row.addStretch(1)
form.addRow("Repository", repo_row)
issue_row = QtWidgets.QHBoxLayout()
issue_row.setContentsMargins(0, 0, 0, 0)
issue_row.setSpacing(10)
issue_row.addWidget(
self._link_button(
"Open",
"https://github.com/darkzoul5/YoutubePlaylistSync/issues",
)
)
issue_row.addStretch(1)
form.addRow("Issues", issue_row)
layout.addLayout(form)
return card
def _suggestions_card(self) -> QtWidgets.QFrame:
card, layout = self._card()
layout.addWidget(self._card_title("Suggestions"))
suggestions = [
"Keep the app updated regularly so that YouTube extraction stays reliable."
]
for text in suggestions:
layout.addWidget(self._muted_label(f"{text}"))
layout.addStretch(1)
return card