Compare commits
2 Commits
b07a56082a
..
v5.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
996d581a07
|
|||
|
ba05a4a7ef
|
@@ -8,6 +8,41 @@ Kept as the work happens, not assembled at release time. Format is
|
||||
with its history. SageTube's own versions start below and are the ones this file
|
||||
records.
|
||||
|
||||
## 5.5.2 — 2026-08-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The account feed loaded but never appeared.** Refresh reported "60 videos in
|
||||
feed" above a "No subscriptions yet" message and an empty page: the empty-state
|
||||
hint hides the grid so it can sit centred, and the account path set its entries
|
||||
without re-running that check — so the grid stayed hidden from startup. The
|
||||
local feed was unaffected, which is why it went unnoticed.
|
||||
- The empty-state message was written for local subscriptions and was wrong
|
||||
advice in account mode, where local subscriptions are not used at all. It now
|
||||
says to press Refresh instead of telling you to go and subscribe to something.
|
||||
- The Subscriptions panel is hidden in account mode rather than sitting empty
|
||||
beside a full feed, implying something failed.
|
||||
|
||||
## 5.5.1 — 2026-08-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The app could start fullscreen with no way out.** Fullscreen became a state
|
||||
of the main window in 5.5.0, and the window's geometry is saved on exit — so
|
||||
quitting from fullscreen brought the app back fullscreen, with no title bar,
|
||||
no close button, and nothing bound to leave it (the player's F and Escape only
|
||||
work while the player has focus, and it need not even be the visible tab).
|
||||
Fullscreen is now never restored at startup, leaving it is done before the
|
||||
geometry is saved, and **F11 toggles fullscreen and Escape leaves it from
|
||||
anywhere in the window**.
|
||||
- **"Sign in with cookies" and the account button did nothing.** Both open the
|
||||
cookie dialog on its Cookies tab, which meant calling `setCurrentIndex` on
|
||||
the dialog's tab widget — a `SmoothTabWidget`, which despite standing in for
|
||||
a `QTabWidget` implemented only `set_current_index`. The call raised, so the
|
||||
dialog never appeared. `SmoothTabWidget` now provides the Qt-compatible API
|
||||
it claimed to have (`setCurrentIndex`, `count`, `widget`, `indexOf`,
|
||||
`tabText`, `setTabText`).
|
||||
|
||||
## 5.5.0 — 2026-08-09
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "sagetube"
|
||||
version = "5.5.0"
|
||||
version = "5.5.2"
|
||||
description = "Watch-first YouTube client with embedded mpv playback and yt-dlp downloading. Fork of YTSage."
|
||||
authors = [
|
||||
{ name = "Houmeres", email = "admin@ecoposta.sk" },
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@ match `version` in pyproject.toml and the repository's latest signed tag --
|
||||
this package's tag line continues YTSage's, which is why it starts at 5.x.
|
||||
"""
|
||||
|
||||
__version__ = "5.5.0"
|
||||
__version__ = "5.5.2"
|
||||
__author__ = "Houmeres"
|
||||
|
||||
@@ -148,7 +148,10 @@ class FeedPage(QWidget):
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal, self)
|
||||
layout.addWidget(splitter, stretch=1)
|
||||
|
||||
side = QWidget()
|
||||
# Kept as an attribute: account mode does not use local subscriptions,
|
||||
# so the panel is hidden there rather than sitting empty beside a full
|
||||
# feed and implying something is missing.
|
||||
self.subs_panel = side = QWidget()
|
||||
side_layout = QVBoxLayout(side)
|
||||
side_layout.setContentsMargins(0, 0, 4, 0)
|
||||
subs_label = QLabel(_("feed.subscriptions"))
|
||||
@@ -352,16 +355,26 @@ class FeedPage(QWidget):
|
||||
Feed is the first tab now, so an empty one is the first thing a new
|
||||
user sees. Say what to do about it rather than showing a blank grid.
|
||||
"""
|
||||
account_mode = (self.mode_combo.currentData() or "local") == "account"
|
||||
self.subs_panel.setVisible(not account_mode)
|
||||
|
||||
self.grid.empty_label.hide()
|
||||
if self.grid.card_count() > 0:
|
||||
self.empty_hint.hide()
|
||||
self.grid.show()
|
||||
return
|
||||
self.empty_hint.setText(
|
||||
_("feed.empty_no_subscriptions")
|
||||
if not LibraryManager.subscriptions()
|
||||
else _("feed.empty_not_refreshed")
|
||||
)
|
||||
|
||||
# The message depends on the mode: account mode does not use local
|
||||
# subscriptions at all, so telling someone to go and subscribe to
|
||||
# something would be wrong advice.
|
||||
if account_mode:
|
||||
message = _("feed.empty_account")
|
||||
elif not LibraryManager.subscriptions():
|
||||
message = _("feed.empty_no_subscriptions")
|
||||
else:
|
||||
message = _("feed.empty_not_refreshed")
|
||||
self.empty_hint.setText(message)
|
||||
|
||||
# Hide the empty grid so the hint sits in the middle of the page
|
||||
# rather than pinned under a large blank area.
|
||||
self.grid.hide()
|
||||
@@ -383,10 +396,15 @@ class FeedPage(QWidget):
|
||||
def _on_account_feed(self, entries: List[Dict[str, Any]]) -> None:
|
||||
self.grid.set_entries(entries)
|
||||
self.status_label.setText(_("feed.refreshed", count=len(entries)))
|
||||
# Without this the grid stays hidden behind the empty-state hint that
|
||||
# was shown at startup, so a successful refresh reported "60 videos in
|
||||
# feed" above a "No subscriptions yet" message and no videos.
|
||||
self._update_empty_state()
|
||||
|
||||
def _on_account_error(self, message: str) -> None:
|
||||
logger.error(f"Account feed failed: {message}")
|
||||
self.status_label.setText(_("feed.account_failed", error=message[:200]))
|
||||
self._update_empty_state()
|
||||
|
||||
def _on_account_finished(self) -> None:
|
||||
self.refresh_btn.setEnabled(True)
|
||||
|
||||
@@ -31,7 +31,7 @@ from PySide6.QtWidgets import (
|
||||
QGraphicsScene,
|
||||
QGraphicsPixmapItem,
|
||||
)
|
||||
from PySide6.QtGui import QIcon, QPixmap, QPainter, QBrush, QColor
|
||||
from PySide6.QtGui import QIcon, QPixmap, QPainter, QBrush, QColor, QShortcut, QKeySequence
|
||||
|
||||
from .. import __version__ as APP_VERSION
|
||||
from ..core import ytsage_app_update as app_update # SageTube's own release check
|
||||
@@ -230,6 +230,16 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to restore state: {e}")
|
||||
|
||||
# Never start fullscreen. restoreGeometry() replays the window state
|
||||
# it was saved with, and since fullscreen became a state of the *main*
|
||||
# window rather than a separate one, quitting from fullscreen used to
|
||||
# bring the app back with no title bar, no close button, and no way
|
||||
# out -- fullscreen is driven from the Watch tab, which may not even be
|
||||
# the visible tab at startup. Maximised is kept: it is escapable.
|
||||
if self.windowState() & Qt.WindowState.WindowFullScreen:
|
||||
logger.info("Ignoring saved fullscreen state; starting windowed.")
|
||||
self.setWindowState(self.windowState() & ~Qt.WindowState.WindowFullScreen)
|
||||
|
||||
def _perform_startup_checks(self):
|
||||
"""Perform potentially blocking startup checks after UI is shown."""
|
||||
|
||||
@@ -598,6 +608,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
)
|
||||
self.watch_page.player.set_fullscreen_controller(self.fullscreen_controller)
|
||||
self.fullscreen_controller.changed.connect(self.watch_page.player.on_fullscreen_changed)
|
||||
self._install_fullscreen_shortcuts()
|
||||
|
||||
self.router.playVideo.connect(self._route_play_video)
|
||||
self.router.queueVideo.connect(self.watch_page.enqueue)
|
||||
@@ -621,6 +632,42 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
if isinstance(self.watch_page.player, PlayerPanel):
|
||||
self.router.cookiesChanged.connect(self.watch_page.player.reload_auth)
|
||||
|
||||
def _install_fullscreen_shortcuts(self) -> None:
|
||||
"""
|
||||
Window-level F11 and Escape.
|
||||
|
||||
The player's own F and Escape are scoped to the player, so they only
|
||||
fire while it has focus. Fullscreen now belongs to the whole window, so
|
||||
there must be a way out of it from anywhere -- otherwise a window with
|
||||
no title bar and no close button has no exit at all.
|
||||
"""
|
||||
self._fullscreen_shortcuts = []
|
||||
for sequence, handler in (
|
||||
("F11", self._toggle_fullscreen_from_window),
|
||||
("Escape", self._escape_fullscreen),
|
||||
):
|
||||
shortcut = QShortcut(QKeySequence(sequence), self)
|
||||
shortcut.setContext(Qt.ShortcutContext.WindowShortcut)
|
||||
shortcut.activated.connect(handler)
|
||||
self._fullscreen_shortcuts.append(shortcut)
|
||||
|
||||
def _toggle_fullscreen_from_window(self) -> None:
|
||||
controller = getattr(self, "fullscreen_controller", None)
|
||||
if controller is None:
|
||||
# No player, so no controller -- still honour F11 as a plain
|
||||
# window toggle rather than doing nothing.
|
||||
self.showNormal() if self.isFullScreen() else self.showFullScreen()
|
||||
return
|
||||
controller.toggle()
|
||||
|
||||
def _escape_fullscreen(self) -> None:
|
||||
"""Escape leaves fullscreen and is otherwise ignored."""
|
||||
controller = getattr(self, "fullscreen_controller", None)
|
||||
if controller is not None and controller.is_active():
|
||||
controller.exit()
|
||||
elif self.isFullScreen():
|
||||
self.showNormal()
|
||||
|
||||
def _on_main_tab_changed(self, index: int) -> None:
|
||||
"""
|
||||
Tell a page it has become visible.
|
||||
@@ -1294,7 +1341,16 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
self.current_download.terminate()
|
||||
self.current_download.wait(1000) # Wait for termination
|
||||
|
||||
# Save the window size and state
|
||||
# Save the window size and state. Leave fullscreen first, so what
|
||||
# gets stored is the windowed geometry -- saving while fullscreen
|
||||
# records a size covering the whole screen, which is not a useful
|
||||
# window to come back to.
|
||||
try:
|
||||
if getattr(self, "fullscreen_controller", None) is not None and self.fullscreen_controller.is_active():
|
||||
self.fullscreen_controller.exit()
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not leave fullscreen before saving geometry: {e}")
|
||||
|
||||
try:
|
||||
ConfigManager.set("window_geometry", self.saveGeometry().toBase64().data().decode("ascii"))
|
||||
ConfigManager.set("window_state", self.saveState().toBase64().data().decode("ascii"))
|
||||
|
||||
@@ -58,7 +58,14 @@ class FullscreenController(QObject):
|
||||
return self._active
|
||||
|
||||
def toggle(self) -> None:
|
||||
self.exit() if self._active else self.enter()
|
||||
# Trust the window, not just the flag. If something else put the
|
||||
# window into fullscreen -- a restored geometry, the window manager,
|
||||
# the user's own shortcut -- toggling must get *out* of it rather than
|
||||
# trying to enter a state it is already in and appearing to do nothing.
|
||||
if self._active or self._window.isFullScreen():
|
||||
self.exit()
|
||||
else:
|
||||
self.enter()
|
||||
|
||||
# ------------------------------------------------------------ transitions
|
||||
|
||||
@@ -97,7 +104,9 @@ class FullscreenController(QObject):
|
||||
self.exit()
|
||||
|
||||
def exit(self) -> None:
|
||||
if not self._active and self._prev_window_state is None:
|
||||
# Also runs when the controller was never active but the window is
|
||||
# fullscreen anyway, so restoring the chrome is unconditional.
|
||||
if not self._active and self._prev_window_state is None and not self._window.isFullScreen():
|
||||
return
|
||||
try:
|
||||
tab_bar = getattr(self._tabs, "tab_bar", None)
|
||||
@@ -116,9 +125,12 @@ class FullscreenController(QObject):
|
||||
layout.setContentsMargins(self._prev_margins)
|
||||
|
||||
if self._prev_window_state is not None:
|
||||
self._window.setWindowState(self._prev_window_state)
|
||||
# Never restore *into* fullscreen: that is what we are leaving.
|
||||
self._window.setWindowState(self._prev_window_state & ~Qt.WindowState.WindowFullScreen)
|
||||
else:
|
||||
self._window.showNormal()
|
||||
if self._window.isFullScreen():
|
||||
self._window.showNormal()
|
||||
except Exception as e:
|
||||
logger.exception(f"Leaving fullscreen failed: {e}")
|
||||
finally:
|
||||
|
||||
@@ -183,3 +183,29 @@ class SmoothTabWidget(QWidget):
|
||||
|
||||
def currentIndex(self):
|
||||
return self.stack.currentIndex()
|
||||
|
||||
# --- QTabWidget-compatible API ---------------------------------------
|
||||
# This class claims to "behave like a QTabWidget" and is used as a drop-in
|
||||
# for one (CustomOptionsDialog builds its tabs on it), but it only ever
|
||||
# offered set_current_index. Anything calling the Qt spelling raised
|
||||
# AttributeError -- which is why "Sign in with cookies" appeared to do
|
||||
# nothing: show_cookie_login_dialog selects the Cookies tab by index
|
||||
# before showing the dialog, and died on that line.
|
||||
|
||||
def setCurrentIndex(self, index):
|
||||
self.set_current_index(index)
|
||||
|
||||
def count(self):
|
||||
return self.stack.count()
|
||||
|
||||
def widget(self, index):
|
||||
return self.stack.widget(index)
|
||||
|
||||
def indexOf(self, widget):
|
||||
return self.stack.indexOf(widget)
|
||||
|
||||
def tabText(self, index):
|
||||
return self.tab_bar.tabText(index)
|
||||
|
||||
def setTabText(self, index, text):
|
||||
self.tab_bar.setTabText(index, text)
|
||||
|
||||
@@ -760,7 +760,8 @@
|
||||
"empty_not_refreshed": "Nothing here yet.\n\nPress Refresh to fetch the latest uploads from your subscriptions.",
|
||||
"sign_in": "Sign in with cookies…",
|
||||
"sign_in_tooltip": "Use your YouTube account via browser cookies, which enables the account feed",
|
||||
"account_requires_cookies": "Requires signing in with cookies"
|
||||
"account_requires_cookies": "Requires signing in with cookies",
|
||||
"empty_account": "Nothing loaded yet.\n\nPress Refresh to fetch your YouTube subscription feed."
|
||||
},
|
||||
"account": {
|
||||
"signed_out": "Not signed in",
|
||||
|
||||
@@ -143,7 +143,8 @@ class LocalizationManager:
|
||||
"empty_not_refreshed": "Nothing here yet.\n\nPress Refresh to fetch the latest uploads from your subscriptions.",
|
||||
"sign_in": "Sign in with cookies…",
|
||||
"sign_in_tooltip": "Use your YouTube account via browser cookies, which enables the account feed",
|
||||
"account_requires_cookies": "Requires signing in with cookies"
|
||||
"account_requires_cookies": "Requires signing in with cookies",
|
||||
"empty_account": "Nothing loaded yet.\n\nPress Refresh to fetch your YouTube subscription feed."
|
||||
},
|
||||
"cards": {
|
||||
"play": "▶ Play",
|
||||
|
||||
Reference in New Issue
Block a user