diff --git a/wakelock_plus/example/.gitignore b/wakelock_plus/example/.gitignore index d0a10c6..143f480 100644 --- a/wakelock_plus/example/.gitignore +++ b/wakelock_plus/example/.gitignore @@ -31,12 +31,9 @@ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # Symbolication related app.*.symbols # Obfuscation related app.*.map.json -!/ios/Podfile \ No newline at end of file +!/ios/Podfile diff --git a/wakelock_plus/example/web/index.html b/wakelock_plus/example/web/index.html index be820e8..14967bb 100644 --- a/wakelock_plus/example/web/index.html +++ b/wakelock_plus/example/web/index.html @@ -31,29 +31,8 @@ example - - - - - + diff --git a/wakelock_plus/assets/no_sleep.js b/wakelock_plus/lib/assets/no_sleep.js similarity index 77% rename from wakelock_plus/assets/no_sleep.js rename to wakelock_plus/lib/assets/no_sleep.js index ccfab74..3ae1af0 100644 --- a/wakelock_plus/assets/no_sleep.js +++ b/wakelock_plus/lib/assets/no_sleep.js @@ -1,3 +1,32 @@ +/*! Based On NoSleep.js v0.12.0 - git.io/vfn01 - Rich Tibbett - MIT license */ + +class PromiseCompleter { + _promise; + _resolve; + _reject; + constructor() { + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + + isCompleted = false; + + get future() { + return this._promise; + } + + complete(value) { + this.isCompleted = true; + this._resolve(value); + } + + completeError(error) { + this._reject(error); + } +} + var webm = 'data:video/webm;base64,GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA=' var mp4 = @@ -26,48 +55,28 @@ function _classCallCheck(instance, Constructor) { } } -// Detect iOS browsers < version 10 -var oldIOS = - typeof navigator !== 'undefined' && - parseFloat( - ( - '' + - (/CPU.*OS ([0-9_]{3,4})[0-9_]{0,1}|(CPU like).*AppleWebKit.*Mobile/i.exec( - navigator.userAgent - ) || [0, ''])[1] - ) - .replace('undefined', '3_2') - .replace('_', '.') - .replace('_', '') - ) < 10 && - !window.MSStream - // Detect native Wake Lock API support var nativeWakeLock = 'wakeLock' in navigator var NoSleep = (function () { - var _releasedNative = true - var _nativeRequestInProgress = false + var _nativeEnabledCompleter; + var _playVideoCompleter; function NoSleep() { var _this = this _classCallCheck(this, NoSleep) + this.nativeEnabled = false if (nativeWakeLock) { this._wakeLock = null var handleVisibilityChange = function handleVisibilityChange() { - if ( - _this._wakeLock !== null && - document.visibilityState === 'visible' - ) { + if (_this._wakeLock !== null && document.visibilityState === 'visible') { _this.enable() } } document.addEventListener('visibilitychange', handleVisibilityChange) document.addEventListener('fullscreenchange', handleVisibilityChange) - } else if (oldIOS) { - this.noSleepTimer = null } else { // Set up no sleep video element this.noSleepVideo = document.createElement('video') @@ -106,90 +115,92 @@ var NoSleep = (function () { }, { key: 'enable', - value: function enable() { + value: async function enable() { var _this2 = this if (nativeWakeLock) { - _nativeRequestInProgress = true + // Disable any previously held wakelocks. + await this.disable() + if (_nativeEnabledCompleter == null) { + _nativeEnabledCompleter = new PromiseCompleter() + } navigator.wakeLock .request('screen') .then(function (wakeLock) { - _releasedNative = false - _nativeRequestInProgress = false - _this2._wakeLock = wakeLock + _this2.nativeEnabled = true + + // We now have a wakelock. Notify all of the existing callers. _this2._wakeLock.addEventListener('release', function () { - _releasedNative = true + _this2.nativeEnabled = false _this2._wakeLock = null }) + + _nativeEnabledCompleter.complete() + _nativeEnabledCompleter = null }) .catch(function (err) { - _nativeRequestInProgress = false - console.error(err.name + ', ' + err.message) + _this2.nativeEnabled = false + var errorMessage = err.name + ', ' + err.message + _nativeEnabledCompleter.completeError(errorMessage) + _nativeEnabledCompleter = null }) - } else if (oldIOS) { - this.disable() - console.warn( - '\n NoSleep enabled for older iOS devices. This can interrupt\n active or long-running network requests from completing successfully.\n See https://github.com/richtr/NoSleep.js/issues/15 for more details.\n ' - ) - this.noSleepTimer = window.setInterval(function () { - if (!document.hidden) { - window.location.href = window.location.href.split('#')[0] - window.setTimeout(window.stop, 0) - } - }, 15000) + // We then wait for screen to be made available. + return _nativeEnabledCompleter.future } else { - this.noSleepVideo.play() + if (_playVideoCompleter == null) { + _playVideoCompleter = new PromiseCompleter() + } + var playPromise = this.noSleepVideo.play() + playPromise.then(function (res) { + _playVideoCompleter.complete() + _playVideoCompleter = null + }).catch(function (err) { + var errorMessage = err.name + ', ' + err.message + _playVideoCompleter.completeError(errorMessage) + _playVideoCompleter = null + }); + return _playVideoCompleter.future } }, }, { key: 'disable', - value: function disable() { + value: async function disable() { if (nativeWakeLock) { + // If we're still trying to enable the wakelock, wait for it to be enabled + if (_nativeEnabledCompleter != null) { + await _nativeEnabledCompleter.future + } if (this._wakeLock != null) { - _releasedNative = true + this.nativeEnabled = false this._wakeLock.release() } this._wakeLock = null - } else if (oldIOS) { - if (this.noSleepTimer) { - console.warn( - '\n NoSleep now disabled for older iOS devices.\n ' - ) - window.clearInterval(this.noSleepTimer) - this.noSleepTimer = null - } } else { + if (_playVideoCompleter != null) { + await _playVideoCompleter.future + } this.noSleepVideo.pause() } + return Promise.resolve(); }, }, { - key: 'enabled', - value: async function enabled() { + key: 'isEnabled', + value: async function isEnabled() { if (nativeWakeLock) { - if (_nativeRequestInProgress == true) { - // Wait until the request is done. - while (true) { - // Wait for 42 milliseconds. - await new Promise((resolve, reject) => setTimeout(resolve, 42)) - if (_nativeRequestInProgress == false) { - break - } - } - } - - // todo: use WakeLockSentinel.released when that is available (https://developer.mozilla.org/en-US/docs/Web/API/WakeLockSentinel/released) - if (_releasedNative != false) { - return false + // If we're still trying to enable the wakelock, wait for it to be enabled + if (_nativeEnabledCompleter != null) { + await _nativeEnabledCompleter.future } - return true - } else if (oldIOS) { - return this.noSleepTimer != null + return this.nativeEnabled } else { + if (_playVideoCompleter != null) { + await _playVideoCompleter.future + } if (this.noSleepVideo == undefined) { return false } @@ -208,17 +219,22 @@ var noSleep = new NoSleep() var Wakelock = { enabled: async function () { try { - return noSleep.enabled() + return noSleep.isEnabled() } catch (e) { return false } }, toggle: async function (enable) { - if (enable) { - noSleep.enable() - } else { - noSleep.disable() + try { + if (enable) { + await noSleep.enable() + } else { + await noSleep.disable() + } + } catch (e) { + return Promise.reject(e); } + return Promise.resolve() }, } diff --git a/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart b/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart index 73cf3f3..c131d4b 100644 --- a/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart +++ b/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'dart:js_interop'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart'; import 'package:wakelock_plus/src/web_impl/import_js_library.dart'; import 'package:wakelock_plus/src/web_impl/js_wakelock.dart' as wakelock_plus_web; +import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart'; /// The web implementation of the [WakelockPlatformInterface]. /// @@ -14,43 +13,35 @@ class WakelockPlusWebPlugin extends WakelockPlusPlatformInterface { /// Registers [WakelockPlusWebPlugin] as the default instance of the /// [WakelockPlatformInterface]. static void registerWith(Registrar registrar) { - // Import a version of `NoSleep.js` that was adjusted for the wakelock - // plugin. - _jsLoaded = importJsLibrary( - url: 'assets/no_sleep.js', flutterPluginName: 'wakelock_plus'); - WakelockPlusPlatformInterface.instance = WakelockPlusWebPlugin(); } - // The future that resolves when the JS library is loaded. - static late Future _jsLoaded; + // The future that signals when the JS is loaded. + // This needs to be `await`ed before accessing any methods of the + // JS-interop layer. + Future? _jsLoaded; + + // + // Lazily imports the JS library once, then awaits to ensure that + // it's loaded into the DOM. + // + Future _ensureJsLoaded() async { + _jsLoaded ??= importJsLibrary( + url: 'assets/no_sleep.js', flutterPluginName: 'wakelock_plus'); + return _jsLoaded; + } @override Future toggle({required bool enable}) async { // Make sure the JS library is loaded before calling it. - await _jsLoaded; - - wakelock_plus_web.toggle(enable); + await _ensureJsLoaded(); + await wakelock_plus_web.toggle(enable); } @override Future get enabled async { // Make sure the JS library is loaded before calling it. - await _jsLoaded; - - final completer = Completer(); - - wakelock_plus_web.enabled().toDart.then( - // onResolve - (value) { - completer.complete(value.toDart); - }, - // onReject - onError: (error) { - completer.completeError(error); - }, - ); - - return completer.future; + await _ensureJsLoaded(); + return wakelock_plus_web.enabled(); } } diff --git a/wakelock_plus/lib/src/web_impl/import_js_library.dart b/wakelock_plus/lib/src/web_impl/import_js_library.dart index b998c6d..41b34d6 100644 --- a/wakelock_plus/lib/src/web_impl/import_js_library.dart +++ b/wakelock_plus/lib/src/web_impl/import_js_library.dart @@ -1,6 +1,7 @@ -import 'dart:js_interop'; +import 'dart:async'; +import 'dart:ui_web' as ui_web; -import 'package:web/web.dart'; +import 'package:web/web.dart' as web; /// This is an implementation of the `import_js_library` plugin that is used /// until that plugin is migrated to null safety. @@ -18,59 +19,85 @@ Future importJsLibrary( } String _libraryUrl(String url, String pluginName) { + // Added suggested changes as per + // https://github.com/fluttercommunity/wakelock_plus/issues/19#issuecomment-2301963609 if (url.startsWith('./')) { url = url.replaceFirst('./', ''); - return './assets/packages/$pluginName/$url'; } + if (url.startsWith('assets/')) { - return './assets/packages/$pluginName/$url'; - } else { - return url; + return ui_web.assetManager.getAssetUrl( + 'packages/$pluginName/$url', + ); } + + return url; } -HTMLScriptElement _createScriptTag(String library) { - final script = document.createElement('script') as HTMLScriptElement +Future? _importRunning; +Map _loadedLibraries = {}; +int _nextLibraryId = 0; + +web.HTMLScriptElement _createScriptTag(String library) { + final scriptId = 'imported-js-library-${_nextLibraryId++}'; + final script = web.document.createElement('script') as web.HTMLScriptElement ..type = 'text/javascript' ..charset = 'utf-8' ..async = true - ..src = library; + ..src = library + ..id = scriptId; return script; } /// Injects a bunch of libraries in the `` and returns a /// Future that resolves when all load. -Future _importJSLibraries(List libraries) { +Future _importJSLibraries(List libraries) async { + // we add the library to _loadedLibraries asynchronously, so we need locking. + // Dart uses voluntary preemption, so everything between two `await`s can be + // considered locked + while (_importRunning != null) { + await _importRunning; + } + final importLockCompleter = Completer(); + _importRunning = importLockCompleter.future; final loading = >[]; - final head = document.head; + final head = web.document.head; for (final library in libraries) { if (!_isImported(library)) { final scriptTag = _createScriptTag(library); head!.appendChild(scriptTag); - loading.add(scriptTag.onLoad.first); + final completer = Completer(); + loading.add(completer.future); + + scriptTag.onLoad.first.then((_) { + _loadedLibraries[library] = scriptTag.id; + completer.complete(); + }); + scriptTag.onError.first.then((event) => + completer.completeError(Exception('Error loading: $library'))); } } - return Future.wait(loading); + try { + await Future.wait(loading, eagerError: true); + } finally { + // first "unlock" future, then complete the completer for anyone already waiting. + // I'm not sure if `.complete()` is yielding execution, so this is the safe order + _importRunning = null; + importLockCompleter.complete(); + } } bool _isImported(String url) { - final head = document.head!; + final head = web.document.head!; return _isLoaded(head, url); } -bool _isLoaded(HTMLHeadElement head, String url) { - if (url.startsWith('./')) { - url = url.replaceFirst('./', ''); - } - for (int i = 0; i < head.children.length; i++) { - final element = head.children.item(i)!; - if (element.instanceOfString('HTMLScriptElement')) { - if ((element as HTMLScriptElement).src.endsWith(url)) { - return true; - } - } +bool _isLoaded(web.HTMLHeadElement head, String url) { + final scriptId = _loadedLibraries[url]; + if (scriptId == null) { + return false; } - return false; + return head.querySelector('#$scriptId') != null; } diff --git a/wakelock_plus/lib/src/web_impl/js_wakelock.dart b/wakelock_plus/lib/src/web_impl/js_wakelock.dart index 38b7822..972c77e 100644 --- a/wakelock_plus/lib/src/web_impl/js_wakelock.dart +++ b/wakelock_plus/lib/src/web_impl/js_wakelock.dart @@ -3,10 +3,18 @@ library; import 'dart:js_interop'; +@JS('toggle') +external JSPromise _toggle(JSBoolean enable); + /// Toggles the JS wakelock. -@JS() -external void toggle(bool enable); +Future toggle(bool enable) { + return _toggle(enable.toJS).toDart.then((_) => null); +} + +@JS('enabled') +external JSPromise _enabled(); /// Returns a JS promise of whether the wakelock is enabled or not. -@JS() -external JSPromise enabled(); +Future enabled() { + return _enabled().toDart.then((enabled) => enabled.toDart); +} diff --git a/wakelock_plus/pubspec.yaml b/wakelock_plus/pubspec.yaml index deae737..892153e 100644 --- a/wakelock_plus/pubspec.yaml +++ b/wakelock_plus/pubspec.yaml @@ -99,4 +99,4 @@ flutter: # https://flutter.dev/custom-fonts/#from-packages assets: - - assets/no_sleep.js + - packages/wakelock_plus/assets/no_sleep.js diff --git a/wakelock_plus/test/wakelock_plus_web_plugin_test.dart b/wakelock_plus/test/wakelock_plus_web_plugin_test.dart index 869b576..f205f1d 100644 --- a/wakelock_plus/test/wakelock_plus_web_plugin_test.dart +++ b/wakelock_plus/test/wakelock_plus_web_plugin_test.dart @@ -6,13 +6,21 @@ import 'package:wakelock_plus/src/wakelock_plus_web_plugin.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart'; +/// +/// Run these tests with: +/// flutter run -d chrome test/wakelock_plus_web_plugin_test.dart +/// void main() { group('$WakelockPlusWebPlugin', () { setUpAll(() async { - // todo: the web tests do not work as the JS library import does not work. + // todo: the web tests do not work as the JS library import does not work when using flutter run test --platform chrome. WakelockPlusPlatformInterface.instance = WakelockPlusWebPlugin(); }); + tearDown(() async { + await WakelockPlus.disable(); + }); + test('$WakelockPlusWebPlugin set as default instance', () { expect( WakelockPlusPlatformInterface.instance, isA()); @@ -27,7 +35,23 @@ void main() { expect(WakelockPlus.enabled, completion(isTrue)); }); + test('enable more than once', () async { + await WakelockPlus.enable(); + await WakelockPlus.enable(); + await WakelockPlus.enable(); + expect(WakelockPlus.enabled, completion(isTrue)); + }); + test('disable', () async { + await WakelockPlus.enable(); + await WakelockPlus.disable(); + expect(WakelockPlus.enabled, completion(isFalse)); + }); + + test('disable more than once', () async { + await WakelockPlus.enable(); + await WakelockPlus.disable(); + await WakelockPlus.disable(); await WakelockPlus.disable(); expect(WakelockPlus.enabled, completion(isFalse)); });