mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-07-04 12:54:08 +03:00
Compare commits
10 Commits
868b419d9c
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| df4c7d504b | |||
| ac5a98a09c | |||
| 811ff45dc9 | |||
| c658b9a90d | |||
| b06ab55f99 | |||
| de315d07e0 | |||
| 4dc7d95123 | |||
| 42ba6310a3 | |||
| 0a49676c72 | |||
| 8ec894fc1f |
@@ -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
|
||||||
|
|||||||
+2
-3
@@ -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 }}"
|
||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user