import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:screen_retriever/screen_retriever.dart'; import 'package:window_manager/window_manager.dart'; import '../../../shared/utils/app_logger.dart'; class FullscreenManager { static const _tag = 'Fullscreen'; static bool get _isDesktop => Platform.isWindows || Platform.isMacOS; FullscreenManager({required this._onStateChanged}); final VoidCallback _onStateChanged; bool _isFullscreen = false; bool _isChanging = false; Rect? _windowBoundsBeforeFullscreen; bool _wasMaximized = false; bool _wasResizable = true; bool _wasAlwaysOnTop = false; bool _isDisposed = false; Future? _disposeRestoreFuture; bool get isFullscreen => _isFullscreen; Future toggle() async { if (_isDisposed) return; if (!_isDesktop) return; if (_isChanging) return; _isChanging = true; final wasFullscreen = _isFullscreen; try { if (Platform.isWindows) { if (wasFullscreen) { await _leaveWindowsFullscreen(); } else { await _enterWindowsFullscreen(); } } else { await _toggleNativeFullscreen(); } } catch (error) { AppLogger.warn(_tag, 'Failed to toggle', error); if (Platform.isWindows && !wasFullscreen) { try { await _leaveWindowsFullscreen(updateState: false); } catch (_) {} _setFullscreenState(false); } else if (!Platform.isWindows) { final actual = await _readNativeFullscreenState(); if (actual != null) _setFullscreenState(actual); } } finally { _isChanging = false; } } Future leave({bool updateState = true}) async { if (_isDisposed && updateState) return; if (!_isFullscreen) return; try { if (Platform.isWindows) { await _leaveWindowsFullscreen(updateState: updateState); } else { await windowManager.setFullScreen(false); } } catch (_) {} _isFullscreen = false; } void dispose() { if (_isDisposed) return; _isDisposed = true; if (_isFullscreen) { _scheduleDisposeRestore(); } } @visibleForTesting Future? get disposeRestoreFuture => _disposeRestoreFuture; void _scheduleDisposeRestore() { if (_disposeRestoreFuture != null) return; final restore = _restoreAfterDispose(); _disposeRestoreFuture = restore; unawaited(restore); } Future _restoreAfterDispose() async { while (_isChanging) { await Future.delayed(Duration.zero); } if (!_isFullscreen) return; _isChanging = true; try { if (Platform.isWindows) { await _leaveWindowsFullscreen(updateState: false); } else { await windowManager.setFullScreen(false); _isFullscreen = false; } } catch (error) { AppLogger.warn(_tag, 'Failed to restore window on dispose', error); } finally { _isChanging = false; } } void _setFullscreenState(bool fullscreen) { if (_isFullscreen == fullscreen) return; _isFullscreen = fullscreen; if (!_isDisposed) { _onStateChanged(); } } Future _readNativeFullscreenState() async { try { return await windowManager.isFullScreen(); } catch (error) { AppLogger.warn(_tag, 'Failed to read state', error); return null; } } Future _toggleNativeFullscreen() async { final current = await _readNativeFullscreenState() ?? _isFullscreen; final next = !current; await windowManager.setFullScreen(next); final actual = await _readNativeFullscreenState() ?? next; _setFullscreenState(actual); if (_isDisposed) { _scheduleDisposeRestore(); } } Future _enterWindowsFullscreen() async { final currentBounds = await windowManager.getBounds(); final display = await _displayForBounds(currentBounds); _windowBoundsBeforeFullscreen = currentBounds; _wasMaximized = await windowManager.isMaximized(); _wasResizable = await windowManager.isResizable(); _wasAlwaysOnTop = await windowManager.isAlwaysOnTop(); if (_wasMaximized) { await windowManager.unmaximize(); } await windowManager.setResizable(false); await windowManager.setAlwaysOnTop(true); await windowManager.setTitleBarStyle( TitleBarStyle.hidden, windowButtonVisibility: false, ); await windowManager.setBounds(fullscreenBoundsForDisplay(display)); await windowManager.focus(); _setFullscreenState(true); if (_isDisposed) { _scheduleDisposeRestore(); } } Future _leaveWindowsFullscreen({bool updateState = true}) async { await windowManager.setTitleBarStyle( TitleBarStyle.hidden, windowButtonVisibility: false, ); await windowManager.setResizable(_wasResizable); await windowManager.setAlwaysOnTop(_wasAlwaysOnTop); if (_wasMaximized) { await windowManager.maximize(); } else { final bounds = _windowBoundsBeforeFullscreen; if (bounds != null) { await windowManager.setBounds(bounds); } } await windowManager.focus(); if (updateState) { _setFullscreenState(false); } else { _isFullscreen = false; } } Future _displayForBounds(Rect bounds) async { final displays = await screenRetriever.getAllDisplays(); final center = bounds.center; for (final display in displays) { final rect = fullscreenBoundsForDisplay(display); if (rect.contains(center)) return display; } if (displays.isNotEmpty) return displays.first; return screenRetriever.getPrimaryDisplay(); } @visibleForTesting static Rect fullscreenBoundsForDisplay(Display display) { final position = display.visiblePosition ?? Offset.zero; final size = display.size; if (Platform.isWindows) { const border = 8.0; return Rect.fromLTWH( position.dx - border, position.dy, size.width + border * 2, size.height + border, ); } return position & size; } }