Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba05a4a7ef
|
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user