Never start fullscreen, and let the cookie dialog open

Two regressions from 5.5.0, both mine.

Fullscreen became a state of the main window, which is what stopped it
destroying the player's GL context -- but the window's geometry is saved on
exit and restoreGeometry() replays the state it was saved with. Quitting while
fullscreen therefore brought the app back fullscreen: no title bar, no close
button, and nothing able to leave it, because the player's F and Escape are
scoped to the player and the Watch tab need not even be visible. Fullscreen is
no longer restored at startup, it is left before the geometry is saved, and
F11/Escape are now window-level shortcuts so there is always a way out. The
controller also trusts the window rather than its own flag, so a fullscreen it
did not set is still escapable.

"Sign in with cookies" and the account button both call
show_cookie_login_dialog, which selects the Cookies tab by index before
showing the dialog. CustomOptionsDialog builds its tabs on SmoothTabWidget,
which stands in for a QTabWidget and says so in its docstring, but implemented
only set_current_index -- so setCurrentIndex raised and the dialog never
opened. That line had never run before: the method had no callers until 5.5.0
wired it up. SmoothTabWidget now provides the Qt-compatible API it claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 01:31:47 +02:00
parent b07a56082a
commit ba05a4a7ef
6 changed files with 121 additions and 7 deletions
+20
View File
@@ -8,6 +8,26 @@ 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.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
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "sagetube"
version = "5.5.0"
version = "5.5.1"
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
View File
@@ -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.1"
__author__ = "Houmeres"
+58 -2
View File
@@ -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"))
+15 -3
View File
@@ -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:
+26
View File
@@ -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)