diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt index bf4c37bcd..065d5ba30 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves.channel.calls.window import android.app.Activity +import android.content.pm.ActivityInfo import android.os.Build import android.view.WindowManager import deckers.thibault.aves.utils.getDisplayCompat @@ -75,4 +76,21 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti ) ) } + + override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) { + result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.getDisplayCompat()?.isHdr ?: false) + } + + override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) { + val on = call.argument("on") + if (on == null) { + result.error("setHdrColorMode-args", "missing arguments", null) + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.window.colorMode = if (on) ActivityInfo.COLOR_MODE_HDR else ActivityInfo.COLOR_MODE_DEFAULT + } + result.success(null) + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt index 1332bbb9a..347669239 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt @@ -28,4 +28,12 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) { override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) { result.success(HashMap()) } + + override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) { + result.success(false) + } + + override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) { + result.success(null) + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt index 11472d6a5..051451024 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt @@ -18,6 +18,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware) "getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets) + "supportsHdr" -> Coresult.safe(call, result, ::supportsHdr) + "setHdrColorMode" -> Coresult.safe(call, result, ::setHdrColorMode) else -> result.notImplemented() } } @@ -44,6 +46,10 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) + abstract fun supportsHdr(call: MethodCall, result: MethodChannel.Result) + + abstract fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) + companion object { private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/window" diff --git a/lib/model/settings/enums/viewer_transition.dart b/lib/model/settings/enums/viewer_transition.dart index 81aefe62a..b9c0cc0cd 100644 --- a/lib/model/settings/enums/viewer_transition.dart +++ b/lib/model/settings/enums/viewer_transition.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/widgets/viewer/controls/controller.dart'; +import 'package:aves/widgets/viewer/controls/transitions.dart'; import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 44f9a9d73..437f620c8 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -17,11 +17,17 @@ abstract class WindowService { Future isCutoutAware(); Future getCutoutInsets(); + + Future supportsHdr(); + + Future setHdrColorMode(bool on); } class PlatformWindowService implements WindowService { static const _platform = MethodChannel('deckers.thibault/aves/window'); + bool? _isCutoutAware, _supportsHdr; + @override Future isActivity() async { try { @@ -90,8 +96,6 @@ class PlatformWindowService implements WindowService { } } - bool? _isCutoutAware; - @override Future isCutoutAware() async { if (_isCutoutAware != null) return SynchronousFuture(_isCutoutAware!); @@ -121,4 +125,27 @@ class PlatformWindowService implements WindowService { } return EdgeInsets.zero; } + + @override + Future supportsHdr() async { + if (_supportsHdr != null) return SynchronousFuture(_supportsHdr!); + try { + final result = await _platform.invokeMethod('supportsHdr'); + _supportsHdr = result as bool?; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return _supportsHdr ?? false; + } + + @override + Future setHdrColorMode(bool on) async { + try { + await _platform.invokeMethod('setHdrColorMode', { + 'on': on, + }); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } } diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 671b3e0b6..74c28797b 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -154,6 +154,7 @@ class _BugReportState extends State with FeedbackMixin { final androidInfo = await DeviceInfoPlugin().androidInfo; final storageVolumes = await storageService.getStorageVolumes(); final storageGrants = await storageService.getGrantedDirectories(); + final supportsHdr = await windowService.supportsHdr(); return [ 'Package: ${device.packageName}', 'Installer: ${packageInfo.installerStore}', @@ -162,7 +163,7 @@ class _BugReportState extends State with FeedbackMixin { 'Android version: ${androidInfo.version.release}, API ${androidInfo.version.sdkInt}', 'Android build: ${androidInfo.display}', 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', - 'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}', + 'Support: dynamic colors=${device.isDynamicColorAvailable}, geocoder=${device.hasGeocoder}, HDR=$supportsHdr', 'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}', 'System locales: ${WidgetsBinding.instance.platformDispatcher.locales.join(', ')}', 'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}', diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 21c99d2d7..1cf9498de 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -96,10 +96,10 @@ class GridThemeData { else if (entry.isAnimated) const AnimatedImageIcon() else ...[ + if (entry.isHdr && showHdr) const HdrIcon(), if (entry.isRaw && showRaw) const RawIcon(), if (entry.is360) const PanoramaIcon(), ], - if (entry.isHdr && showHdr) const HdrIcon(), if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(), if (entry.isMultiPage && !entry.isMotionPhoto) MultiPageIcon(entry: entry), if (entry.isGeotiff) const GeoTiffIcon(), diff --git a/lib/widgets/viewer/controls/controller.dart b/lib/widgets/viewer/controls/controller.dart index 084cd2ce9..5eaf6f114 100644 --- a/lib/widgets/viewer/controls/controller.dart +++ b/lib/widgets/viewer/controls/controller.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/multipage.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/viewer/controls/cast.dart'; import 'package:aves/widgets/viewer/controls/events.dart'; @@ -57,6 +59,7 @@ class ViewerController with CastMixin { ); } _initialScale = initialScale; + entryNotifier.addListener(_onEntryChanged); _autopilotNotifier = ValueNotifier(autopilot); _autopilotNotifier.addListener(_onAutopilotChanged); _onAutopilotChanged(); @@ -66,12 +69,21 @@ class ViewerController with CastMixin { if (kFlutterMemoryAllocationsEnabled) { MemoryAllocations.instance.dispatchObjectDisposed(object: this); } + entryNotifier.removeListener(_onEntryChanged); + windowService.setHdrColorMode(false); _autopilotNotifier.dispose(); _clearAutopilotAnimations(); _stopPlayTimer(); _streamController.close(); } + Future _onEntryChanged() async { + if (await windowService.supportsHdr()) { + final enabled = entryNotifier.value?.isHdr ?? false; + await windowService.setHdrColorMode(enabled); + } + } + void _onAutopilotChanged() { _clearAutopilotAnimations(); _stopPlayTimer(); @@ -115,79 +127,3 @@ class ViewerController with CastMixin { Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward()); } } - -class PageTransitionEffects { - static TransitionBuilder fade( - PageController pageController, - int index, { - required bool zoomIn, - }) => - (context, child) { - double opacity = 0; - double dx = 0; - double scale = 1; - if (pageController.hasClients && pageController.position.haveDimensions) { - final position = (pageController.page! - index).clamp(-1.0, 1.0); - final width = pageController.position.viewportDimension; - opacity = (1 - position.abs()).clamp(0, 1); - dx = position * width; - if (zoomIn) { - scale = 1 + position; - } - } - return Opacity( - opacity: opacity, - child: Transform.translate( - offset: Offset(dx, 0), - child: Transform.scale( - scale: scale, - child: child, - ), - ), - ); - }; - - static TransitionBuilder slide( - PageController pageController, - int index, { - required bool parallax, - }) => - (context, child) { - double dx = 0; - if (pageController.hasClients && pageController.position.haveDimensions) { - final position = (pageController.page! - index).clamp(-1.0, 1.0); - final width = pageController.position.viewportDimension; - if (parallax) { - dx = position * width / 2; - } - } - return ClipRect( - child: Transform.translate( - offset: Offset(dx, 0), - child: child, - ), - ); - }; - - static TransitionBuilder none( - PageController pageController, - int index, - ) => - (context, child) { - double opacity = 0; - double dx = 0; - if (pageController.hasClients && pageController.position.haveDimensions) { - final position = (pageController.page! - index).clamp(-1.0, 1.0); - final width = pageController.position.viewportDimension; - opacity = (1 - position.abs()).roundToDouble().clamp(0, 1); - dx = position * width; - } - return Opacity( - opacity: opacity, - child: Transform.translate( - offset: Offset(dx, 0), - child: child, - ), - ); - }; -} diff --git a/lib/widgets/viewer/controls/transitions.dart b/lib/widgets/viewer/controls/transitions.dart new file mode 100644 index 000000000..e76d699b1 --- /dev/null +++ b/lib/widgets/viewer/controls/transitions.dart @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart'; + +class PageTransitionEffects { + static TransitionBuilder fade( + PageController pageController, + int index, { + required bool zoomIn, + }) => + (context, child) { + double opacity = 0; + double dx = 0; + double scale = 1; + if (pageController.hasClients && pageController.position.haveDimensions) { + final position = (pageController.page! - index).clamp(-1.0, 1.0); + final width = pageController.position.viewportDimension; + opacity = (1 - position.abs()).clamp(0, 1); + dx = position * width; + if (zoomIn) { + scale = 1 + position; + } + } + return Opacity( + opacity: opacity, + child: Transform.translate( + offset: Offset(dx, 0), + child: Transform.scale( + scale: scale, + child: child, + ), + ), + ); + }; + + static TransitionBuilder slide( + PageController pageController, + int index, { + required bool parallax, + }) => + (context, child) { + double dx = 0; + if (pageController.hasClients && pageController.position.haveDimensions) { + final position = (pageController.page! - index).clamp(-1.0, 1.0); + final width = pageController.position.viewportDimension; + if (parallax) { + dx = position * width / 2; + } + } + return ClipRect( + child: Transform.translate( + offset: Offset(dx, 0), + child: child, + ), + ); + }; + + static TransitionBuilder none( + PageController pageController, + int index, + ) => + (context, child) { + double opacity = 0; + double dx = 0; + if (pageController.hasClients && pageController.position.haveDimensions) { + final position = (pageController.page! - index).clamp(-1.0, 1.0); + final width = pageController.position.viewportDimension; + opacity = (1 - position.abs()).roundToDouble().clamp(0, 1); + dx = position * width; + } + return Opacity( + opacity: opacity, + child: Transform.translate( + offset: Offset(dx, 0), + child: child, + ), + ); + }; +} diff --git a/test/fake/window_service.dart b/test/fake/window_service.dart index ea856236c..ab9ddce72 100644 --- a/test/fake/window_service.dart +++ b/test/fake/window_service.dart @@ -21,4 +21,10 @@ class FakeWindowService extends Fake implements WindowService { @override Future getCutoutInsets() => SynchronousFuture(EdgeInsets.zero); + + @override + Future supportsHdr() => SynchronousFuture(false); + + @override + Future setHdrColorMode(bool on) => SynchronousFuture(null); }