From 86d9425d267fbc2a572e899560b57c7aa8331a81 Mon Sep 17 00:00:00 2001 From: Ortes Date: Sun, 31 May 2026 08:25:06 -0500 Subject: [PATCH 1/3] feat: native browser fullscreen on web When running as a Flutter Web app, the fullscreen button now triggers the browser's native Fullscreen API (document.documentElement.requestFullscreen) in addition to Chewie's internal route-based fullscreen. This gives users a true OS-level fullscreen instead of just expanding within the browser window. Pressing Escape to exit native fullscreen also collapses Chewie's fullscreen route, keeping both states in sync. Implemented via conditional import (dart.library.html): web_fullscreen.dart uses package:web + dart:js_interop; web_fullscreen_stub.dart provides no-ops for non-web platforms. Co-Authored-By: Claude Sonnet 4.6 --- lib/src/chewie_player.dart | 19 +++++++++++++++++++ lib/src/web_fullscreen.dart | 30 ++++++++++++++++++++++++++++++ lib/src/web_fullscreen_stub.dart | 5 +++++ pubspec.yaml | 1 + 4 files changed, 55 insertions(+) create mode 100644 lib/src/web_fullscreen.dart create mode 100644 lib/src/web_fullscreen_stub.dart diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 7ffa295b2..4b67bd4fd 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:chewie/src/chewie_progress_colors.dart'; +// ignore: uri_does_not_exist +import 'web_fullscreen_stub.dart' if (dart.library.html) 'web_fullscreen.dart'; import 'package:chewie/src/models/option_item.dart'; import 'package:chewie/src/models/options_translation.dart'; import 'package:chewie/src/models/subtitle_model.dart'; @@ -44,16 +46,26 @@ class ChewieState extends State { bool get isControllerFullScreen => widget.controller.isFullScreen; late PlayerNotifier notifier; + late final void Function() _browserFsExitHandler; @override void initState() { super.initState(); widget.controller.addListener(listener); notifier = PlayerNotifier.init(); + // When the user presses Escape, the browser exits its native fullscreen + // without Chewie knowing. Detect this and collapse the fullscreen route. + _browserFsExitHandler = () { + if (!browserInFullscreen && _isFullScreen) { + widget.controller.exitFullScreen(); + } + }; + addBrowserFullscreenChangeListener(_browserFsExitHandler); } @override void dispose() { + removeBrowserFullscreenChangeListener(_browserFsExitHandler); widget.controller.removeListener(listener); notifier.dispose(); super.dispose(); @@ -182,6 +194,10 @@ class ChewieState extends State { WakelockPlus.enable(); } + // Ask the browser to enter its native fullscreen. Must be called before the + // first await so we are still inside the user-gesture event handler. + requestBrowserFullscreen(); + await Navigator.of( context, rootNavigator: widget.controller.useRootNavigator, @@ -191,6 +207,9 @@ class ChewieState extends State { if (kIsWeb) { await _reInitializeControllers(wasPlaying); + // Exit native browser fullscreen when the Chewie route pops (e.g. user + // clicked the fullscreen button again). No-op if Escape was already used. + exitBrowserFullscreen(); } _isFullScreen = false; diff --git a/lib/src/web_fullscreen.dart b/lib/src/web_fullscreen.dart new file mode 100644 index 000000000..86dddc778 --- /dev/null +++ b/lib/src/web_fullscreen.dart @@ -0,0 +1,30 @@ +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +final _handlers = {}; + +void requestBrowserFullscreen() { + web.document.documentElement?.requestFullscreen(); +} + +void exitBrowserFullscreen() { + if (web.document.fullscreenElement != null) { + web.document.exitFullscreen(); + } +} + +bool get browserInFullscreen => web.document.fullscreenElement != null; + +void addBrowserFullscreenChangeListener(void Function() callback) { + final jsHandler = ((JSAny? _) => callback()).toJS; + _handlers[callback] = jsHandler; + web.document.addEventListener('fullscreenchange', jsHandler); +} + +void removeBrowserFullscreenChangeListener(void Function() callback) { + final jsHandler = _handlers.remove(callback); + if (jsHandler != null) { + web.document.removeEventListener('fullscreenchange', jsHandler); + } +} diff --git a/lib/src/web_fullscreen_stub.dart b/lib/src/web_fullscreen_stub.dart new file mode 100644 index 000000000..47583a874 --- /dev/null +++ b/lib/src/web_fullscreen_stub.dart @@ -0,0 +1,5 @@ +void requestBrowserFullscreen() {} +void exitBrowserFullscreen() {} +bool get browserInFullscreen => false; +void addBrowserFullscreenChangeListener(void Function() callback) {} +void removeBrowserFullscreenChangeListener(void Function() callback) {} diff --git a/pubspec.yaml b/pubspec.yaml index 047272a27..86d19a5b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: provider: ^6.1.5+1 video_player: ^2.10.0 wakelock_plus: ^1.3.2 + web: ^1.0.0 dev_dependencies: flutter_test: From 79472b7c77a8b815bc951fa2e20658e7327f03f3 Mon Sep 17 00:00:00 2001 From: Ortes Date: Sun, 31 May 2026 09:27:15 -0500 Subject: [PATCH 2/3] Gate native web fullscreen behind useNativeFullScreenOnWeb flag Add ChewieController.useNativeFullScreenOnWeb (default true) so the native browser Fullscreen behavior is opt-out rather than an unconditional change. No effect on non-web platforms. Also adds a CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 3 +++ lib/src/chewie_player.dart | 28 ++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c515482..eea8d7a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [Unreleased] +* ✨ Web: enter the browser's native (OS-level) fullscreen via the Fullscreen API instead of only expanding the Flutter view inside the browser window. Pressing Escape to leave browser fullscreen also exits Chewie's fullscreen. Controlled by the new `ChewieController.useNativeFullScreenOnWeb` flag (defaults to `true`; no effect on non-web platforms). + ## [1.13.0] * 🛠️ [#927](https://github.com/fluttercommunity/chewie/pull/927): Fix(web): Keep playback when entering/exiting fullscreen on web. Thanks [victorspringer](https://github.com/victorspringer). * **BREAKING CHANGE**: Library now requires at least Flutter version `3.32.0` or higher. diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 4b67bd4fd..b17eabd6b 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -60,12 +60,16 @@ class ChewieState extends State { widget.controller.exitFullScreen(); } }; - addBrowserFullscreenChangeListener(_browserFsExitHandler); + if (widget.controller.useNativeFullScreenOnWeb) { + addBrowserFullscreenChangeListener(_browserFsExitHandler); + } } @override void dispose() { - removeBrowserFullscreenChangeListener(_browserFsExitHandler); + if (widget.controller.useNativeFullScreenOnWeb) { + removeBrowserFullscreenChangeListener(_browserFsExitHandler); + } widget.controller.removeListener(listener); notifier.dispose(); super.dispose(); @@ -196,7 +200,9 @@ class ChewieState extends State { // Ask the browser to enter its native fullscreen. Must be called before the // first await so we are still inside the user-gesture event handler. - requestBrowserFullscreen(); + if (widget.controller.useNativeFullScreenOnWeb) { + requestBrowserFullscreen(); + } await Navigator.of( context, @@ -209,7 +215,9 @@ class ChewieState extends State { await _reInitializeControllers(wasPlaying); // Exit native browser fullscreen when the Chewie route pops (e.g. user // clicked the fullscreen button again). No-op if Escape was already used. - exitBrowserFullscreen(); + if (widget.controller.useNativeFullScreenOnWeb) { + exitBrowserFullscreen(); + } } _isFullScreen = false; @@ -344,6 +352,7 @@ class ChewieController extends ChangeNotifier { this.allowMuting = true, this.allowPlaybackSpeedChanging = true, this.useRootNavigator = true, + this.useNativeFullScreenOnWeb = true, this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], this.systemOverlaysOnEnterFullScreen, this.deviceOrientationsOnEnterFullScreen, @@ -397,6 +406,7 @@ class ChewieController extends ChangeNotifier { bool? allowMuting, bool? allowPlaybackSpeedChanging, bool? useRootNavigator, + bool? useNativeFullScreenOnWeb, Duration? hideControlsTimer, EdgeInsets? controlsSafeAreaMinimum, List? playbackSpeeds, @@ -460,6 +470,8 @@ class ChewieController extends ChangeNotifier { allowPlaybackSpeedChanging: allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging, useRootNavigator: useRootNavigator ?? this.useRootNavigator, + useNativeFullScreenOnWeb: + useNativeFullScreenOnWeb ?? this.useNativeFullScreenOnWeb, playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds, systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ?? @@ -618,6 +630,14 @@ class ChewieController extends ChangeNotifier { /// Defines if push/pop navigations use the rootNavigator final bool useRootNavigator; + /// On Flutter Web, also enter the browser's native fullscreen (via the + /// Fullscreen API) when going fullscreen, instead of only expanding the + /// Flutter view inside the browser window. Pressing Escape to leave the + /// browser fullscreen also exits Chewie's fullscreen. + /// + /// Has no effect on non-web platforms. + final bool useNativeFullScreenOnWeb; + /// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds. final Duration hideControlsTimer; From 05b5e5929863e80626296c927c9ae33c6965207d Mon Sep 17 00:00:00 2001 From: Ortes Date: Sun, 31 May 2026 09:52:29 -0500 Subject: [PATCH 3/3] Address review: abstract web fullscreen import, drop lint ignore - Remove `// ignore: uri_does_not_exist`; chewie_player.dart now imports the plain `web_fullscreen.dart` abstraction, which conditionally exports the stub or the real implementation. - Use `dart.library.js_interop` instead of `dart.library.html` for the conditional export. - Move the dart:js_interop / package:web implementation into web_fullscreen_impl.dart. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/src/chewie_player.dart | 3 +-- lib/src/web_fullscreen.dart | 32 ++------------------------------ lib/src/web_fullscreen_impl.dart | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 lib/src/web_fullscreen_impl.dart diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index b17eabd6b..9e02c4265 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:chewie/src/chewie_progress_colors.dart'; -// ignore: uri_does_not_exist -import 'web_fullscreen_stub.dart' if (dart.library.html) 'web_fullscreen.dart'; +import 'web_fullscreen.dart'; import 'package:chewie/src/models/option_item.dart'; import 'package:chewie/src/models/options_translation.dart'; import 'package:chewie/src/models/subtitle_model.dart'; diff --git a/lib/src/web_fullscreen.dart b/lib/src/web_fullscreen.dart index 86dddc778..54e6d8961 100644 --- a/lib/src/web_fullscreen.dart +++ b/lib/src/web_fullscreen.dart @@ -1,30 +1,2 @@ -import 'dart:js_interop'; - -import 'package:web/web.dart' as web; - -final _handlers = {}; - -void requestBrowserFullscreen() { - web.document.documentElement?.requestFullscreen(); -} - -void exitBrowserFullscreen() { - if (web.document.fullscreenElement != null) { - web.document.exitFullscreen(); - } -} - -bool get browserInFullscreen => web.document.fullscreenElement != null; - -void addBrowserFullscreenChangeListener(void Function() callback) { - final jsHandler = ((JSAny? _) => callback()).toJS; - _handlers[callback] = jsHandler; - web.document.addEventListener('fullscreenchange', jsHandler); -} - -void removeBrowserFullscreenChangeListener(void Function() callback) { - final jsHandler = _handlers.remove(callback); - if (jsHandler != null) { - web.document.removeEventListener('fullscreenchange', jsHandler); - } -} +export 'web_fullscreen_stub.dart' + if (dart.library.js_interop) 'web_fullscreen_impl.dart'; diff --git a/lib/src/web_fullscreen_impl.dart b/lib/src/web_fullscreen_impl.dart new file mode 100644 index 000000000..86dddc778 --- /dev/null +++ b/lib/src/web_fullscreen_impl.dart @@ -0,0 +1,30 @@ +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +final _handlers = {}; + +void requestBrowserFullscreen() { + web.document.documentElement?.requestFullscreen(); +} + +void exitBrowserFullscreen() { + if (web.document.fullscreenElement != null) { + web.document.exitFullscreen(); + } +} + +bool get browserInFullscreen => web.document.fullscreenElement != null; + +void addBrowserFullscreenChangeListener(void Function() callback) { + final jsHandler = ((JSAny? _) => callback()).toJS; + _handlers[callback] = jsHandler; + web.document.addEventListener('fullscreenchange', jsHandler); +} + +void removeBrowserFullscreenChangeListener(void Function() callback) { + final jsHandler = _handlers.remove(callback); + if (jsHandler != null) { + web.document.removeEventListener('fullscreenchange', jsHandler); + } +}