fix(macos): sync restored fullscreen chrome
This commit is contained in:
+5
-3
@@ -167,7 +167,11 @@ Future<void> _bootstrapApp() async {
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
if (PlatformDetector.isDesktopOS()) {
|
||||
futures.add(windowManager.ensureInitialized());
|
||||
if (Platform.isMacOS) {
|
||||
futures.add(windowManager.ensureInitialized().then((_) => MacOSWindowService.setupCustomTitlebar()));
|
||||
} else {
|
||||
futures.add(windowManager.ensureInitialized());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize TV detection (Android leanback or Apple TV) and PiP on Android.
|
||||
@@ -178,8 +182,6 @@ Future<void> _bootstrapApp() async {
|
||||
PipService();
|
||||
}
|
||||
|
||||
futures.add(MacOSWindowService.setupCustomTitlebar());
|
||||
|
||||
// Hook Windows native fullscreen callback (no-op elsewhere).
|
||||
NativeWindowService.initialize();
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class FullscreenStateManager extends ChangeNotifier with WindowListener {
|
||||
/// Toggle fullscreen state, handling maximized-to-fullscreen transition on Windows/Linux
|
||||
Future<void> toggleFullscreen() async {
|
||||
if (Platform.isMacOS) {
|
||||
final isCurrentlyFullscreen = await windowManager.isFullScreen();
|
||||
final isCurrentlyFullscreen = await MacOSWindowService.isFullscreen();
|
||||
if (isCurrentlyFullscreen) {
|
||||
await MacOSWindowService.exitFullscreen();
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,7 @@ class MacOSWindowService {
|
||||
static bool _initialized = false;
|
||||
static bool _delegateEnabled = false;
|
||||
static final List<MacOSWindowDelegate> _delegates = [];
|
||||
static final MacOSWindowDelegate _fullscreenDelegate = _FullscreenWindowDelegate();
|
||||
|
||||
static Future<void> _invoke(String method, [Map<String, dynamic>? args]) async {
|
||||
if (!Platform.isMacOS) return;
|
||||
@@ -71,10 +72,18 @@ class MacOSWindowService {
|
||||
///
|
||||
/// This method sets up the Dart-side callbacks for fullscreen state tracking.
|
||||
static Future<void> setupCustomTitlebar() async {
|
||||
if (!Platform.isMacOS || _initialized) return;
|
||||
if (!Platform.isMacOS) return;
|
||||
|
||||
if (_initialized && _delegateEnabled) {
|
||||
await syncWindowChrome();
|
||||
FullscreenStateManager().setFullscreen(await isFullscreen());
|
||||
return;
|
||||
}
|
||||
|
||||
await initialize(enableWindowDelegate: true);
|
||||
addWindowDelegate(_FullscreenWindowDelegate());
|
||||
addWindowDelegate(_fullscreenDelegate);
|
||||
await syncWindowChrome();
|
||||
FullscreenStateManager().setFullscreen(await isFullscreen());
|
||||
}
|
||||
|
||||
/// Initialize the window service.
|
||||
@@ -83,7 +92,7 @@ class MacOSWindowService {
|
||||
static Future<void> initialize({bool enableWindowDelegate = false}) async {
|
||||
if (!Platform.isMacOS) return;
|
||||
|
||||
if (!_initialized) {
|
||||
if (!_initialized || (enableWindowDelegate && !_delegateEnabled)) {
|
||||
await _channel.invokeMethod('initialize', {'enableWindowDelegate': enableWindowDelegate});
|
||||
_initialized = true;
|
||||
}
|
||||
@@ -107,6 +116,8 @@ class MacOSWindowService {
|
||||
|
||||
static Future<void> setTrafficLightsVisible(bool visible) => _invoke('setTrafficLightsVisible', {'visible': visible});
|
||||
|
||||
static Future<void> syncWindowChrome() => _invoke('syncWindowChrome');
|
||||
|
||||
static Future<void> enterFullscreen() => _invoke('enterFullscreen');
|
||||
|
||||
static Future<void> exitFullscreen() => _invoke('exitFullscreen');
|
||||
|
||||
@@ -256,7 +256,7 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState {
|
||||
// When maximized or fullscreen, always keep traffic lights visible so the
|
||||
// user can reach them without the controls-hide-on-mouse-leave race.
|
||||
// In normal windowed mode, toggle with controls as before.
|
||||
final isMaximizedOrFullscreen = await windowManager.isMaximized() || await windowManager.isFullScreen();
|
||||
final isMaximizedOrFullscreen = await windowManager.isMaximized() || await MacOSWindowService.isFullscreen();
|
||||
if (!mounted || generation != _trafficLightVisibilityGeneration) return;
|
||||
final visible = isMaximizedOrFullscreen || _forceShowControls ? true : _showControls;
|
||||
await MacOSWindowService.setTrafficLightsVisible(visible);
|
||||
|
||||
@@ -470,6 +470,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
_checkPipSupport();
|
||||
// Add window listener for tracking fullscreen state (for button icon)
|
||||
if (PlatformDetector.isDesktopOS()) {
|
||||
if (Platform.isMacOS) {
|
||||
_isFullscreen = FullscreenStateManager().isFullscreen;
|
||||
FullscreenStateManager().addListener(_onFullscreenStateChanged);
|
||||
}
|
||||
windowManager.addListener(this);
|
||||
_initAlwaysOnTopState();
|
||||
}
|
||||
@@ -530,6 +534,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
}
|
||||
}
|
||||
if (Platform.isMacOS) {
|
||||
FullscreenStateManager().removeListener(_onFullscreenStateChanged);
|
||||
_pipService.isPipActive.removeListener(_onMacPipChanged);
|
||||
_trafficLightVisibilityGeneration++;
|
||||
unawaited(MacOSWindowService.setTrafficLightsVisible(true));
|
||||
@@ -537,6 +542,15 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFullscreenStateChanged() {
|
||||
final isFullscreen = FullscreenStateManager().isFullscreen;
|
||||
if (!mounted || _isFullscreen == isFullscreen) return;
|
||||
setState(() {
|
||||
_isFullscreen = isFullscreen;
|
||||
});
|
||||
_updateTrafficLightVisibility();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() {
|
||||
if (mounted) {
|
||||
|
||||
@@ -31,14 +31,14 @@ class MainFlutterWindow: NSWindow {
|
||||
WindowUtilsPlugin.register(
|
||||
with: flutterViewController.registrar(forPlugin: "WindowUtilsPlugin"))
|
||||
WindowUtilsPlugin.setWindow(self)
|
||||
|
||||
// Set custom traffic light positions using centralized values from plugin
|
||||
WindowUtilsPlugin.setInitialTrafficLightPositions()
|
||||
WindowUtilsPlugin.installWindowDelegate()
|
||||
WindowUtilsPlugin.syncWindowChrome()
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
// Enable window position/size persistence
|
||||
self.setFrameAutosaveName("com.edde746.plezy.MainWindow")
|
||||
WindowUtilsPlugin.syncWindowChrome()
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,36 @@ class WindowDelegate: NSObject, NSWindowDelegate {
|
||||
channel?.invokeMethod(method, arguments: nil)
|
||||
}
|
||||
|
||||
func syncWindowChrome() {
|
||||
guard let window = window else { return }
|
||||
if window.styleMask.contains(.fullScreen) {
|
||||
applyFullScreenChrome(to: window)
|
||||
} else {
|
||||
applyWindowedChrome(to: window)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyFullScreenChrome(to window: NSWindow) {
|
||||
window.toolbar = nil
|
||||
window.titleVisibility = .visible
|
||||
window.titlebarAppearsTransparent = false
|
||||
WindowUtilsPlugin.setTrafficLightsVisible(true, window: window)
|
||||
WindowUtilsPlugin.setTrafficLightPositions(custom: false, window: window)
|
||||
}
|
||||
|
||||
private func applyWindowedChrome(to window: NSWindow) {
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
|
||||
if window.toolbar == nil, let flutterVC = window.contentViewController {
|
||||
window.toolbar = ForwardingToolbar(flutterViewController: flutterVC)
|
||||
}
|
||||
|
||||
WindowUtilsPlugin.setTrafficLightsVisible(true, window: window)
|
||||
WindowUtilsPlugin.setTrafficLightPositions(custom: true, window: window)
|
||||
}
|
||||
|
||||
// MARK: - NSWindowDelegate
|
||||
|
||||
func window(
|
||||
@@ -31,13 +61,7 @@ class WindowDelegate: NSObject, NSWindowDelegate {
|
||||
|
||||
func windowWillEnterFullScreen(_ notification: Notification) {
|
||||
guard let window = window else { return }
|
||||
// Remove toolbar before entering fullscreen
|
||||
window.toolbar = nil
|
||||
// Show title and make titlebar opaque for native fullscreen look
|
||||
window.titleVisibility = .visible
|
||||
window.titlebarAppearsTransparent = false
|
||||
// Reset traffic light positions to default
|
||||
WindowUtilsPlugin.setTrafficLightPositions(custom: false, window: window)
|
||||
applyFullScreenChrome(to: window)
|
||||
// Notify Dart for state management only
|
||||
emit("windowWillEnterFullScreen")
|
||||
}
|
||||
@@ -56,13 +80,7 @@ class WindowDelegate: NSObject, NSWindowDelegate {
|
||||
|
||||
func windowDidExitFullScreen(_ notification: Notification) {
|
||||
guard let window = window else { return }
|
||||
// Restore toolbar
|
||||
if let flutterVC = window.contentViewController {
|
||||
let toolbar = ForwardingToolbar(flutterViewController: flutterVC)
|
||||
window.toolbar = toolbar
|
||||
}
|
||||
// Restore custom traffic light positions
|
||||
WindowUtilsPlugin.setTrafficLightPositions(custom: true, window: window)
|
||||
applyWindowedChrome(to: window)
|
||||
emit("windowDidExitFullScreen")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,12 +88,17 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
static func setWindow(_ window: NSWindow) {
|
||||
instance?.window = window
|
||||
instance?.windowDelegate?.window = window
|
||||
}
|
||||
|
||||
/// Apply custom traffic light positions. Called by MainFlutterWindow on startup.
|
||||
static func setInitialTrafficLightPositions() {
|
||||
static func installWindowDelegate() {
|
||||
guard let instance = instance, let window = instance.window else { return }
|
||||
instance.applyTrafficLightPositions(custom: true, window: window)
|
||||
instance.installWindowDelegate(window: window)
|
||||
}
|
||||
|
||||
static func syncWindowChrome() {
|
||||
guard let instance = instance else { return }
|
||||
instance.syncWindowChrome()
|
||||
}
|
||||
|
||||
/// Apply traffic light positions. Called by WindowDelegate during fullscreen transitions.
|
||||
@@ -102,6 +107,12 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
|
||||
instance.applyTrafficLightPositions(custom: custom, window: window)
|
||||
}
|
||||
|
||||
static func setTrafficLightsVisible(_ visible: Bool, window: NSWindow) {
|
||||
for buttonType in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] {
|
||||
window.standardWindowButton(buttonType)?.isHidden = !visible
|
||||
}
|
||||
}
|
||||
|
||||
private func applyTrafficLightPositions(custom: Bool, window: NSWindow) {
|
||||
if custom {
|
||||
for (buttonType, offset) in WindowUtilsPlugin.customButtonPositions {
|
||||
@@ -130,9 +141,11 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
|
||||
case "setTrafficLightsVisible":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let visible = args?["visible"] as? Bool ?? true
|
||||
for buttonType in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] {
|
||||
window.standardWindowButton(buttonType)?.isHidden = !visible
|
||||
}
|
||||
WindowUtilsPlugin.setTrafficLightsVisible(visible, window: window)
|
||||
result(nil)
|
||||
|
||||
case "syncWindowChrome":
|
||||
syncWindowChrome()
|
||||
result(nil)
|
||||
|
||||
case "enterFullscreen":
|
||||
@@ -159,12 +172,30 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
|
||||
self.window = window
|
||||
|
||||
if enableWindowDelegate {
|
||||
let delegate = WindowDelegate()
|
||||
delegate.channel = channel
|
||||
delegate.window = window
|
||||
windowDelegate = delegate
|
||||
window.delegate = delegate
|
||||
installWindowDelegate(window: window)
|
||||
}
|
||||
|
||||
windowDelegate?.syncWindowChrome()
|
||||
}
|
||||
|
||||
private func installWindowDelegate(window: NSWindow) {
|
||||
let delegate = windowDelegate ?? WindowDelegate()
|
||||
delegate.channel = channel
|
||||
delegate.window = window
|
||||
windowDelegate = delegate
|
||||
window.delegate = delegate
|
||||
}
|
||||
|
||||
private func syncWindowChrome() {
|
||||
guard let window = window else { return }
|
||||
if windowDelegate == nil {
|
||||
installWindowDelegate(window: window)
|
||||
} else {
|
||||
windowDelegate?.channel = channel
|
||||
windowDelegate?.window = window
|
||||
window.delegate = windowDelegate
|
||||
}
|
||||
windowDelegate?.syncWindowChrome()
|
||||
}
|
||||
|
||||
private func withButton(
|
||||
|
||||
Reference in New Issue
Block a user