diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ff22efb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "dart.lineLength": 120, + "[dart]": { + "editor.rulers": [ + 120 + ] + } +} \ No newline at end of file diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..777c837 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "41456452f29d64e8deb623a3c927524bcf9f111b" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: web + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 54fe7f7..f3bb530 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -25,4 +25,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9f7eea9d6ca88cd46f4792c528e42c66122012eb -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 56af9e7..2fad4f5 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -216,7 +216,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a0..8e3ca5d 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { FloatingActionButton( child: const Text('UI'), onPressed: () async { - final orientation = await NativeDeviceOrientationCommunicator().orientation(useSensor: false); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + try { + print("Requesting orientation"); + final orientation = await NativeDeviceOrientationCommunicator().orientation(useSensor: false); + print("Done requesting orientation. Mounted: ${context.mounted}"); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Native Orientation read: $orientation'), + duration: const Duration(milliseconds: 500), + ), + ); + } catch (e) { + print("Couldn't retrieve orientation: $e"); SnackBar( - content: Text('Native Orientation read: $orientation'), + content: Text("Couldn't retrieve orientation: $e"), duration: const Duration(milliseconds: 500), - ), - ); + ); + } }, ), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index 847ef95..099308b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -71,15 +71,20 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -90,6 +95,30 @@ packages: description: flutter source: sdk version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -102,49 +131,49 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.0" native_device_orientation: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.0.1" + version: "2.0.3" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" platform: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -157,10 +186,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" sky_engine: dependency: transitive description: flutter @@ -218,10 +247,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" vector_math: dependency: transitive description: @@ -234,26 +263,26 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "14.2.1" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.1.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..45cf2ca --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/src/native_device_orientation_web.dart b/lib/src/native_device_orientation_web.dart new file mode 100644 index 0000000..08d85ff --- /dev/null +++ b/lib/src/native_device_orientation_web.dart @@ -0,0 +1,231 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:native_device_orientation/src/native_device_orientation.dart'; +import 'package:native_device_orientation/src/native_device_orientation_platform_interface.dart'; +import 'package:web/web.dart' as web; + +// Convert the screen orientation string to NativeDeviceOrientation +NativeDeviceOrientation _fromString(String? orientationString) { + if (orientationString == null) { + return NativeDeviceOrientation.unknown; + } + + switch (orientationString) { + case 'portrait-primary': + return NativeDeviceOrientation.portraitUp; + case 'portrait-secondary': + return NativeDeviceOrientation.portraitDown; + case 'landscape-primary': + return NativeDeviceOrientation.landscapeLeft; + case 'landscape-secondary': + return NativeDeviceOrientation.landscapeRight; + default: + return NativeDeviceOrientation.unknown; + } +} + +NativeDeviceOrientation _fromInt(int? orientation) { + if (orientation == null) { + return NativeDeviceOrientation.unknown; + } + + switch (orientation) { + case -90: + return NativeDeviceOrientation.landscapeRight; + case 0: + return NativeDeviceOrientation.portraitUp; + case 90: + return NativeDeviceOrientation.landscapeLeft; + case 180: + return NativeDeviceOrientation.portraitDown; + default: + return NativeDeviceOrientation.unknown; + } +} + +/// Convert DeviceOrientationEvent to NativeDeviceOrientation +NativeDeviceOrientation _fromDeviceOrientation( + // The alpha angle is 0° when top of the device is pointed directly toward the Earth's north pole, and increases as the device is rotated counterclockwise. + // As such, 90° corresponds with pointing west, 180° with south, and 270° with east. + num alpha, + // The beta angle is 0° when the device's top and bottom are the same distance from the Earth's surface; + // it increases toward 180° as the device is tipped forward toward the user, and it decreases toward -180° as the device is tipped backward away from the user. + num beta, + // The gamma angle is 0° when the device's left and right sides are the same distance from the surface of the Earth, + // and increases toward 90° as the device is tipped toward the right, and toward -90° as the device is tipped toward the left. + num gamma, +) { + if (beta.abs() > gamma.abs()) { + return beta > 0 ? NativeDeviceOrientation.portraitUp : NativeDeviceOrientation.portraitDown; + } else { + return gamma > 0 ? NativeDeviceOrientation.landscapeRight : NativeDeviceOrientation.landscapeLeft; + } +} + +class NativeDeviceOrientationWeb extends NativeDeviceOrientationPlatform { + // A broadcast stream controller to listen to screen-based orientation changes + final StreamController _screenOrientationStreamController = + StreamController.broadcast(); + + // A broadcast stream controller to listen to sensor-based orientation changes + final StreamController _sensorStreamController = + StreamController.broadcast(); + + static void registerWith(Registrar registrar) { + NativeDeviceOrientationPlatform.instance = NativeDeviceOrientationWeb(); + } + + // Check if the browser supports the DeviceOrientationEvent + bool get _hasSensorSupport => globalContext.hasProperty('DeviceOrientationEvent'.toJS).toDart; + + NativeDeviceOrientationWeb() { + // Set up the listeners for the stream controllers + // we only listen to the orientation changes when there is a listener + _screenOrientationStreamController.onListen = () { + _startListenToScreenOrientationChanges(); + }; + _screenOrientationStreamController.onCancel = () { + _stopListenToScreenOrientationChanges(); + }; + _sensorStreamController.onListen = () { + _startListenToSensorChanges(); + }; + _sensorStreamController.onCancel = () { + _stopListenToSensorChanges(); + }; + } + + // A callback to listen to screen-based orientation changes + void _onScreenOrientationChange(web.Event event) { + final orientation = _getCurrentOrientation(); + _screenOrientationStreamController.add(orientation); + } + + // Starts listening to screen-based orientation changes + void _startListenToScreenOrientationChanges() { + web.window.screen.orientation.addEventListener('change', _onScreenOrientationChange.toJS); + } + + // Stops listening to screen-based orientation changes + void _stopListenToScreenOrientationChanges() { + web.window.screen.orientation.removeEventListener('change', _onScreenOrientationChange.toJS); + } + + // A callback to listen to sensor-based orientation changes + void _onSensorChange(web.DeviceOrientationEvent event) { + final orientation = _fromDeviceOrientation( + event.alpha!, + event.beta!, + event.gamma!, + ); + _sensorStreamController.add(orientation); + } + + /// Starts listening to sensor-based orientation changes + /// + /// Uses the [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent) + /// This feature is available only in secure contexts (HTTPS) + Future _startListenToSensorChanges() async { + final event = globalContext.getProperty('DeviceOrientationEvent'.toJS); + // check if we need to ask for permission + if (event.hasProperty('requestPermission'.toJS).toDart) { + final permission = await event.callMethod('requestPermission'.toJS).toDart; + if ((permission as JSString).toDart != 'granted') { + _sensorStreamController.addError( + 'Permission denied for DeviceOrientationEvent', + ); + } + } + + web.window.addEventListener('deviceorientation', _onSensorChange.toJS); + } + + /// Stops listening to sensor-based orientation changes + void _stopListenToSensorChanges() { + web.window.removeEventListener('deviceorientation', _onSensorChange.toJS); + } + + /// Get the current screen-based orientation + NativeDeviceOrientation _getCurrentOrientation() { + final screenOrientation = web.window.screen.getProperty("orientation".toJS); + if (screenOrientation != null) { + return _fromString(web.window.screen.orientation.type); + } else { + // probably on mobile safari, try to get orientation from window + final windowOrientation = web.window.getProperty("orientation".toJS); + return _fromInt((windowOrientation as JSNumber).toDartInt); + } + } + + @override + Stream onOrientationChanged({ + bool useSensor = false, + NativeDeviceOrientation defaultOrientation = NativeDeviceOrientation.portraitUp, + }) { + if (!useSensor || !_hasSensorSupport) { + return _screenOrientationStreamController.stream.transform( + StreamTransformer.fromHandlers( + handleData: (data, sink) { + sink.add( + data == NativeDeviceOrientation.unknown ? defaultOrientation : data, + ); + }, + handleError: (error, stackTrace, sink) { + sink.add(defaultOrientation); + }, + ), + ); + } + + return _sensorStreamController.stream.transform( + StreamTransformer.fromHandlers( + handleData: (data, sink) { + sink.add( + data == NativeDeviceOrientation.unknown ? defaultOrientation : data, + ); + }, + handleError: (error, stackTrace, sink) { + sink.add(defaultOrientation); + }, + ), + ); + } + + @override + Future orientation({ + bool useSensor = false, + NativeDeviceOrientation defaultOrientation = NativeDeviceOrientation.portraitUp, + }) async { + if (!useSensor || !_hasSensorSupport) { + return _getCurrentOrientation(); + } + + return onOrientationChanged( + useSensor: true, + defaultOrientation: defaultOrientation, + ).first; + } + + @override + Future pause() async { + if (_screenOrientationStreamController.hasListener) { + _stopListenToScreenOrientationChanges(); + } + if (_sensorStreamController.hasListener) { + _stopListenToSensorChanges(); + } + } + + @override + Future resume() async { + if (_screenOrientationStreamController.hasListener) { + _startListenToScreenOrientationChanges(); + } + if (_sensorStreamController.hasListener) { + _startListenToSensorChanges(); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 19776e4..59830df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,10 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter plugin_platform_interface: ^2.0.2 + web: ">=0.4.0" dev_dependencies: flutter_test: @@ -25,3 +28,6 @@ flutter: pluginClass: NativeDeviceOrientationPlugin ios: pluginClass: NativeDeviceOrientationPlugin + web: + pluginClass: NativeDeviceOrientationWeb + fileName: src/native_device_orientation_web.dart