Initial commit
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user