Files
sm-emby-share/lib/features/player/services/fullscreen_manager.dart
T

223 lines
6.0 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
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<void>? _disposeRestoreFuture;
bool get isFullscreen => _isFullscreen;
Future<void> 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<void> 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<void>? get disposeRestoreFuture => _disposeRestoreFuture;
void _scheduleDisposeRestore() {
if (_disposeRestoreFuture != null) return;
final restore = _restoreAfterDispose();
_disposeRestoreFuture = restore;
unawaited(restore);
}
Future<void> _restoreAfterDispose() async {
while (_isChanging) {
await Future<void>.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<bool?> _readNativeFullscreenState() async {
try {
return await windowManager.isFullScreen();
} catch (error) {
AppLogger.warn(_tag, 'Failed to read state', error);
return null;
}
}
Future<void> _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<void> _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<void> _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<Display> _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;
}
}