Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 62 additions & 15 deletions lib/bootstrap/platform/desktop_platform_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:macos_window_utils/window_manipulator.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart';
import 'package:window_manager/window_manager.dart';

import 'package:fladder/bootstrap/platform/base_app_wrapper.dart';
Expand All @@ -25,11 +24,12 @@ class DesktopAppWrapper extends BaseAppWrapper {
}

class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> with WindowListener {
bool _windowPlacementInitialized = false;
bool _windowIsMaximized = false;
bool _windowIsFullScreen = false;

@override
Future<void> platformInit() async {
if (defaultTargetPlatform == TargetPlatform.windows) {
await SMTCWindows.initialize();
}
if (defaultTargetPlatform == TargetPlatform.macOS) {
await WindowManipulator.initialize(enableWindowDelegate: true);
}
Expand All @@ -38,6 +38,8 @@ class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> wit

await WindowManager.instance.ensureInitialized();
windowManager.addListener(this);
_windowIsMaximized = await windowManager.isMaximized();
_windowIsFullScreen = await windowManager.isFullScreen();

final packageInfo = await PackageInfo.fromPlatform();
final clientSettings = ref.read(clientSettingsProvider);
Expand All @@ -47,9 +49,24 @@ class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> wit
clientSettings,
packageInfo,
);
if (defaultTargetPlatform == TargetPlatform.windows) {
unawaited(_enableWindowPlacementPersistenceAfterStartup());
} else {
_windowPlacementInitialized = true;
}
await toggleMacTrafficLights(await windowManager.isFullScreen());
}

Future<void> _enableWindowPlacementPersistenceAfterStartup() async {
await Future<void>.delayed(windowsWindowPlacementPersistenceDelay);
if (!mounted) return;

_windowIsMaximized = await windowManager.isMaximized();
_windowIsFullScreen = await windowManager.isFullScreen();
if (!mounted) return;
_windowPlacementInitialized = true;
}

@override
void dispose() {
windowManager.removeListener(this);
Expand All @@ -63,43 +80,73 @@ class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> wit
super.onWindowClose();
}

@override
void onWindowResize() async {
bool get _canPersistWindowBounds => shouldPersistWindowBounds(
startupSettled: _windowPlacementInitialized,
isFullScreen: _windowIsFullScreen,
isMaximized: _windowIsMaximized,
);

Future<void> _persistWindowSize() async {
if (!_canPersistWindowBounds) return;
final size = await windowManager.getSize();
if (!_canPersistWindowBounds) return;
ref.read(clientSettingsProvider.notifier).setWindowSize(size);
}

Future<void> _persistWindowPosition() async {
if (!_canPersistWindowBounds) return;
final position = await windowManager.getPosition();
if (!_canPersistWindowBounds) return;
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
}

@override
void onWindowResize() {
unawaited(_persistWindowSize());
super.onWindowResize();
}

@override
void onWindowResized() async {
final size = await windowManager.getSize();
ref.read(clientSettingsProvider.notifier).setWindowSize(size);
void onWindowResized() {
unawaited(_persistWindowSize());
super.onWindowResized();
}

@override
void onWindowMove() async {
final position = await windowManager.getPosition();
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
void onWindowMove() {
unawaited(_persistWindowPosition());
super.onWindowMove();
}

@override
void onWindowMoved() async {
final position = await windowManager.getPosition();
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
void onWindowMoved() {
unawaited(_persistWindowPosition());
super.onWindowMoved();
}

@override
void onWindowMaximize() {
_windowIsMaximized = true;
super.onWindowMaximize();
}

@override
void onWindowUnmaximize() {
_windowIsMaximized = false;
super.onWindowUnmaximize();
}

@override
void onWindowEnterFullScreen() {
_windowIsFullScreen = true;
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: true));
unawaited(toggleMacTrafficLights(true));
super.onWindowEnterFullScreen();
}

@override
void onWindowLeaveFullScreen() {
_windowIsFullScreen = false;
unawaited(toggleMacTrafficLights(false));
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: false));
super.onWindowLeaveFullScreen();
Expand Down
47 changes: 43 additions & 4 deletions lib/providers/video_player_provider.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -20,29 +21,67 @@ final playBackModel = StateProvider<PlaybackModel?>((ref) => null);

final videoPlayerProvider = StateNotifierProvider<VideoPlayerNotifier, MediaControlsWrapper>((ref) {
final videoPlayer = VideoPlayerNotifier(ref);
videoPlayer.init();
if (defaultTargetPlatform != TargetPlatform.windows) {
unawaited(videoPlayer.init());
}
return videoPlayer;
});

typedef VideoPlayerInitializer = Future<void> Function();

class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
VideoPlayerNotifier(this.ref) : super(MediaControlsWrapper(ref: ref));
VideoPlayerNotifier(
this.ref, {
VideoPlayerInitializer? initializeOverride,
}) : _initializeOverride = initializeOverride,
super(MediaControlsWrapper(ref: ref));

final Ref ref;
final VideoPlayerInitializer? _initializeOverride;

List<StreamSubscription> subscriptions = [];
Future<void>? _initialization;
bool _hasCompletedInitialization = false;

late final mediaState = ref.read(mediaPlaybackProvider.notifier);

MediaPlaybackModel get playbackState => ref.read(mediaPlaybackProvider);
bool get initializationInProgress => _initialization != null;
bool get hasCompletedInitialization => _hasCompletedInitialization;

Future<void> init() {
final activeInitialization = _initialization;
if (activeInitialization != null) return activeInitialization;

late final Future<void> operation;
operation = _runInitialization().whenComplete(() {
if (identical(_initialization, operation)) {
_initialization = null;
}
});
_initialization = operation;
return operation;
}

Future<void> _runInitialization() async {
final initializeOverride = _initializeOverride;
if (initializeOverride != null) {
await initializeOverride();
} else {
await _initializePlayer();
}
_hasCompletedInitialization = true;
}

Future<void> init() async {
Future<void> _initializePlayer() async {
await state.stop();
await state.dispose();
await state.init();

for (final s in subscriptions) {
s.cancel();
await s.cancel();
}
subscriptions.clear();

final subscription = state.stateStream.listen((value) {
updateBuffering(value.buffering);
Expand Down
31 changes: 31 additions & 0 deletions lib/util/single_flight_initializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Runs an asynchronous initializer once and shares the in-flight operation.
///
/// A failed attempt is not cached, so a later call can retry safely.
class SingleFlightInitializer<T> {
Future<T>? _active;
T? _value;
bool _hasValue = false;

bool get isRunning => _active != null;
bool get hasValue => _hasValue;

Future<T> run(Future<T> Function() initialize) {
if (_hasValue) return Future.value(_value as T);

final active = _active;
if (active != null) return active;

late final Future<T> operation;
operation = Future<T>.sync(initialize).then((value) {
_value = value;
_hasValue = true;
return value;
}).whenComplete(() {
if (identical(_active, operation)) {
_active = null;
}
});
_active = operation;
return operation;
}
}
Loading
Loading