diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d24c8622..3e7447deb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Video: improved seek accuracy, HDR support, AV1 support, playback speed from x0.25 to x4 - support for animated AVIF (requires rescan) - Collection: filtering by rating range +- Viewer: optionally show histogram on overlay - About: data usage ### Changed diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 486b494e7..aa4bd3f2a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -233,6 +233,10 @@ "nameConflictStrategyReplace": "Replace", "nameConflictStrategySkip": "Skip", + "overlayHistogramNone": "None", + "overlayHistogramRGB": "RGB", + "overlayHistogramLuminance": "Luminance", + "subtitlePositionTop": "Top", "subtitlePositionBottom": "Bottom", @@ -809,6 +813,7 @@ "settingsViewerOverlayTile": "Overlay", "settingsViewerOverlayPageTitle": "Overlay", "settingsViewerShowOverlayOnOpening": "Show on opening", + "settingsViewerShowHistogram": "Show histogram", "settingsViewerShowMinimap": "Show minimap", "settingsViewerShowInformation": "Show information", "settingsViewerShowInformationSubtitle": "Show title, date, location, etc.", diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index ce55394e4..416364766 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -72,6 +72,7 @@ class SettingsDefaults { ]; static const showOverlayOnOpening = true; static const showOverlayMinimap = false; + static const overlayHistogramStyle = OverlayHistogramStyle.none; static const showOverlayInfo = true; static const showOverlayDescription = false; static const showOverlayRatingTags = false; diff --git a/lib/model/settings/modules/viewer.dart b/lib/model/settings/modules/viewer.dart index 1f8ad5729..5d5762468 100644 --- a/lib/model/settings/modules/viewer.dart +++ b/lib/model/settings/modules/viewer.dart @@ -14,6 +14,10 @@ mixin ViewerSettings on SettingsAccess { set showOverlayMinimap(bool newValue) => set(SettingKeys.showOverlayMinimapKey, newValue); + OverlayHistogramStyle get overlayHistogramStyle => getEnumOrDefault(SettingKeys.overlayHistogramStyleKey, SettingsDefaults.overlayHistogramStyle, OverlayHistogramStyle.values); + + set overlayHistogramStyle(OverlayHistogramStyle newValue) => set(SettingKeys.overlayHistogramStyleKey, newValue.toString()); + bool get showOverlayInfo => getBool(SettingKeys.showOverlayInfoKey) ?? SettingsDefaults.showOverlayInfo; set showOverlayInfo(bool newValue) => set(SettingKeys.showOverlayInfoKey, newValue); diff --git a/lib/view/src/settings/enums.dart b/lib/view/src/settings/enums.dart index 8b7abc0c5..7a47008b6 100644 --- a/lib/view/src/settings/enums.dart +++ b/lib/view/src/settings/enums.dart @@ -151,6 +151,19 @@ extension ExtraSlideshowVideoPlaybackView on SlideshowVideoPlayback { } } +extension ExtraOverlayHistogramStyleView on OverlayHistogramStyle { + String getName(BuildContext context) { + switch (this) { + case OverlayHistogramStyle.none: + return context.l10n.overlayHistogramNone; + case OverlayHistogramStyle.rgb: + return context.l10n.overlayHistogramRGB; + case OverlayHistogramStyle.luminance: + return context.l10n.overlayHistogramLuminance; + } + } +} + extension ExtraSubtitlePositionView on SubtitlePosition { String getName(BuildContext context) { switch (this) { diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index 33110575e..f58326f1f 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -1,7 +1,9 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -82,6 +84,14 @@ class ViewerOverlayPage extends StatelessWidget { onChanged: (v) => settings.showOverlayThumbnailPreview = v, title: context.l10n.settingsViewerShowOverlayThumbnails, ), + if (!useTvLayout) + SettingsSelectionListTile( + values: OverlayHistogramStyle.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.overlayHistogramStyle, + onSelection: (v) => settings.overlayHistogramStyle = v, + tileTitle: context.l10n.settingsViewerShowHistogram, + ), ], ), ), diff --git a/lib/widgets/viewer/controls/notifications.dart b/lib/widgets/viewer/controls/notifications.dart index 1d65991e9..a170bb91e 100644 --- a/lib/widgets/viewer/controls/notifications.dart +++ b/lib/widgets/viewer/controls/notifications.dart @@ -108,3 +108,11 @@ class EntryMovedNotification extends Notification with EquatableMixin { const EntryMovedNotification(this.moveType, this.entries); } + +@immutable +class FullImageLoadedNotification extends Notification { + final AvesEntry entry; + final ImageProvider image; + + const FullImageLoadedNotification(this.entry, this.image); +} diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index eea57a7b3..3296399cc 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -33,7 +33,7 @@ import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/overlay/video/video.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:aves/widgets/viewer/view/conductor.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; import 'package:aves_model/aves_model.dart'; import 'package:aves_utils/aves_utils.dart'; @@ -512,6 +512,10 @@ class _EntryViewerStackState extends State with EntryViewContr bool _handleNotification(dynamic notification) { if (notification is FilterSelectedNotification) { _goToCollection(notification.filter); + } else if (notification is FullImageLoadedNotification) { + final viewStateController = context.read().getOrCreateController(notification.entry); + // microtask so that listeners do not trigger during build + scheduleMicrotask(() => viewStateController.fullImageNotifier.value = notification.image); } else if (notification is EntryDeletedNotification) { _onEntryRemoved(context, notification.entries); } else if (notification is EntryMovedNotification) { diff --git a/lib/widgets/viewer/overlay/histogram.dart b/lib/widgets/viewer/overlay/histogram.dart new file mode 100644 index 000000000..290bd4972 --- /dev/null +++ b/lib/widgets/viewer/overlay/histogram.dart @@ -0,0 +1,202 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/viewer/overlay/top.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class ImageHistogram extends StatefulWidget { + final AvesEntry entry; + final ImageProvider image; + + const ImageHistogram({ + super.key, + required this.entry, + required this.image, + }); + + @override + State createState() => _ImageHistogramState(); +} + +class _ImageHistogramState extends State { + Map> _levels = {}; + ImageStream? _imageStream; + late ImageStreamListener _imageListener; + + ImageProvider get imageProvider => widget.image; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant ImageHistogram oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(ImageHistogram widget) { + _imageStream = imageProvider.resolve(ImageConfiguration.empty); + _imageListener = ImageStreamListener((image, synchronousCall) { + _updateLevels(image); + }); + _imageStream?.addListener(_imageListener); + } + + void _unregisterWidget(ImageHistogram widget) { + _imageStream?.removeListener(_imageListener); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: CustomPaint( + painter: _HistogramPainter( + levels: _levels, + borderColor: ViewerTopOverlay.componentBorderColor, + ), + size: const Size(ViewerTopOverlay.componentDimension, ViewerTopOverlay.componentDimension * .6), + ), + ); + } + + static const int bins = 256; + static const int normMax = bins - 1; + + Future _updateLevels(ImageInfo info) async { + final image = info.image; + final data = (await image.toByteData(format: ImageByteFormat.rawExtendedRgba128))!; + final floats = Float32List.view(data.buffer); + + final newLevels = switch (settings.overlayHistogramStyle) { + OverlayHistogramStyle.rgb => _computeRgbLevels(floats), + OverlayHistogramStyle.luminance => _computeLuminanceLevels(floats), + _ => >{}, + }; + + setState(() => _levels = newLevels); + } + + Map> _computeRgbLevels(Float32List floats) { + final redLevels = List.filled(bins, 0); + final greenLevels = List.filled(bins, 0); + final blueLevels = List.filled(bins, 0); + + final pixelCount = floats.length / 4; + for (var i = 0; i < pixelCount; i += 4) { + final a = floats[i + 3]; + if (a > 0) { + final r = floats[i + 0]; + final g = floats[i + 1]; + final b = floats[i + 2]; + redLevels[(r * normMax).round()]++; + greenLevels[(g * normMax).round()]++; + blueLevels[(b * normMax).round()]++; + } + } + + final max = [ + redLevels.max, + greenLevels.max, + blueLevels.max, + ].max; + if (max == 0) return {}; + + final f = 1.0 / max; + return { + Colors.red: redLevels.map((v) => v * f).toList(), + Colors.green: greenLevels.map((v) => v * f).toList(), + Colors.blue: blueLevels.map((v) => v * f).toList(), + }; + } + + Map> _computeLuminanceLevels(Float32List floats) { + final lumLevels = List.filled(bins, 0); + + final pixelCount = floats.length / 4; + for (var i = 0; i < pixelCount; i += 4) { + final a = floats[i + 3]; + if (a > 0) { + final r = floats[i + 0]; + final g = floats[i + 1]; + final b = floats[i + 2]; + final c = Color.fromARGB((a * 255).round(), (r * 255).round(), (g * 255).round(), (b * 255).round()); + lumLevels[(c.computeLuminance() * normMax).round()]++; + } + } + + final max = lumLevels.max; + if (max == 0) return {}; + + final f = 1.0 / max; + return { + Colors.white: lumLevels.map((v) => v * f).toList(), + }; + } +} + +class _HistogramPainter extends CustomPainter { + final Map> levels; + final Color borderColor; + + late final Paint fill, borderStroke; + + _HistogramPainter({ + required this.levels, + this.borderColor = Colors.white, + }) { + fill = Paint() + ..style = PaintingStyle.fill + ..color = const Color(0x33000000); + borderStroke = Paint() + ..style = PaintingStyle.stroke + ..color = borderColor; + } + + @override + void paint(Canvas canvas, Size size) { + final backgroundRect = Rect.fromPoints(Offset.zero, Offset(size.width, size.height)); + canvas.drawRect(backgroundRect, fill); + levels.forEach((color, values) => _drawLevels(canvas, size, color, values)); + canvas.drawRect(backgroundRect, borderStroke); + } + + void _drawLevels(Canvas canvas, Size size, Color color, List values) { + if (values.length < 2) return; + + final xFactor = size.width / (values.length - 1); + final yFactor = size.height; + + final polyline = values.mapIndexed((i, v) => Offset(i * xFactor, size.height - v * yFactor)).toList(); + canvas.drawPoints( + PointMode.polygon, + polyline, + Paint() + ..style = PaintingStyle.stroke + ..color = color); + + polyline.add(Offset(size.width, size.height)); + polyline.add(Offset(0, size.height)); + canvas.drawPath( + Path()..addPolygon(polyline, true), + Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(.5)); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 59ca726f8..a29be6f86 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:aves/model/view_state.dart'; import 'package:aves/widgets/editor/transform/controller.dart'; import 'package:aves/widgets/editor/transform/transformation.dart'; +import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,8 +12,6 @@ import 'package:provider/provider.dart'; class Minimap extends StatelessWidget { final ValueNotifier viewStateNotifier; - static const Size minimapSize = Size(96, 96); - const Minimap({ super.key, required this.viewStateNotifier, @@ -28,44 +27,44 @@ class Minimap extends StatelessWidget { final contentSize = viewState.contentSize; if (viewportSize == null || contentSize == null) return const SizedBox(); return StreamBuilder( - stream: context.select>((v) => v?.transformationStream ?? Stream.value(null)), - builder: (context, snapshot) { - final transformation = snapshot.data; - return CustomPaint( - painter: MinimapPainter( - viewportSize: viewportSize, - contentSize: contentSize, - viewCenterOffset: viewState.position, - viewScale: viewState.scale!, - transformation: transformation, - minimapBorderColor: Colors.white30, - ), - size: minimapSize, - ); - }); + stream: context.select>((v) => v?.transformationStream ?? Stream.value(null)), + builder: (context, snapshot) { + final transformation = snapshot.data; + return CustomPaint( + painter: _MinimapPainter( + viewportSize: viewportSize, + contentSize: contentSize, + viewCenterOffset: viewState.position, + viewScale: viewState.scale!, + transformation: transformation, + minimapBorderColor: ViewerTopOverlay.componentBorderColor, + ), + size: const Size.square(ViewerTopOverlay.componentDimension), + ); + }, + ); }, ), ); } } -class MinimapPainter extends CustomPainter { +class _MinimapPainter extends CustomPainter { final Size contentSize, viewportSize; final Offset viewCenterOffset; final double viewScale; final Transformation? transformation; - final Color minimapBorderColor, viewportBorderColor; + final Color minimapBorderColor; late final Paint fill, minimapStroke, viewportStroke; - MinimapPainter({ + _MinimapPainter({ required this.viewportSize, required this.contentSize, required this.viewCenterOffset, required this.viewScale, this.transformation, this.minimapBorderColor = Colors.white, - this.viewportBorderColor = Colors.white, }) { fill = Paint() ..style = PaintingStyle.fill @@ -75,7 +74,7 @@ class MinimapPainter extends CustomPainter { ..color = minimapBorderColor; viewportStroke = Paint() ..style = PaintingStyle.stroke - ..color = viewportBorderColor; + ..color = Colors.white; } @override diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index d1313890b..50c775a96 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -1,7 +1,7 @@ import 'package:aves/model/multipage.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; -import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:aves/widgets/viewer/view/conductor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 67cd07c31..7b3e8ee94 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -5,9 +5,11 @@ import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:aves/widgets/viewer/overlay/histogram.dart'; import 'package:aves/widgets/viewer/overlay/minimap.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; -import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:aves/widgets/viewer/view/conductor.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -21,6 +23,9 @@ class ViewerTopOverlay extends StatelessWidget { final Size availableSize; final EdgeInsets? viewInsets, viewPadding; + static const Color componentBorderColor = Colors.white30; + static const double componentDimension = 96; + const ViewerTopOverlay({ super.key, required this.entries, @@ -45,7 +50,7 @@ class ViewerTopOverlay extends StatelessWidget { final showInfo = settings.showOverlayInfo; final viewStateConductor = context.read(); - final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry); + final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry).viewStateNotifier; final blurred = settings.enableBlurEffect; final viewInsetsPadding = (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero); @@ -79,23 +84,58 @@ class ViewerTopOverlay extends StatelessWidget { ), ), ), - if (settings.showOverlayMinimap) - SafeArea( - top: !showInfo, - minimum: EdgeInsets.only( - left: viewInsetsPadding.left, - right: viewInsetsPadding.right, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: FadeTransition( - opacity: scale, - child: Minimap( - viewStateNotifier: viewStateNotifier, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (settings.showOverlayMinimap) + SafeArea( + top: !showInfo, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: FadeTransition( + opacity: scale, + child: Minimap( + viewStateNotifier: viewStateNotifier, + ), + ), ), ), - ), - ) + const Spacer(), + if (settings.overlayHistogramStyle != OverlayHistogramStyle.none) + SafeArea( + top: !showInfo, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: FadeTransition( + opacity: scale, + child: Selector>( + selector: (context, vsc) => vsc.getOrCreateController(pageEntry!).fullImageNotifier, + builder: (context, fullImageNotifier, child) { + return ValueListenableBuilder( + valueListenable: fullImageNotifier, + builder: (context, fullImage, child) { + if (fullImage == null || pageEntry == null) return const SizedBox(); + return ImageHistogram( + entry: pageEntry, + image: fullImage, + ); + }, + ); + }, + ), + ), + ), + ), + ], + ), ], ); }, diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index 606116c56..9099e46e6 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -13,7 +13,7 @@ import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/dialogs/wallpaper_settings_dialog.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:aves/widgets/viewer/view/conductor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; @@ -94,7 +94,7 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { } Rect? _getVisibleRegion(BuildContext context) { - final viewState = context.read().getOrCreateController(entry).value; + final viewState = context.read().getOrCreateController(entry).viewState; final viewportSize = viewState.viewportSize; final contentSize = viewState.contentSize; final scale = viewState.scale; @@ -107,7 +107,7 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { } Future _getBytes(BuildContext context, Rect displayRegion) async { - final viewState = context.read().getOrCreateController(entry).value; + final viewState = context.read().getOrCreateController(entry).viewState; final scale = viewState.scale; final displaySize = entry.displaySize; diff --git a/lib/widgets/viewer/providers.dart b/lib/widgets/viewer/providers.dart index 1a76a0a35..e0c04af67 100644 --- a/lib/widgets/viewer/providers.dart +++ b/lib/widgets/viewer/providers.dart @@ -1,7 +1,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:aves/widgets/viewer/view/conductor.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/view/conductor.dart b/lib/widgets/viewer/view/conductor.dart new file mode 100644 index 000000000..be2a6a751 --- /dev/null +++ b/lib/widgets/viewer/view/conductor.dart @@ -0,0 +1,65 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/view_state.dart'; +import 'package:aves/widgets/viewer/view/controller.dart'; +import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +class ViewStateConductor { + final List _controllers = []; + Size _viewportSize = Size.zero; + + static const maxControllerCount = 3; + + Future dispose() async { + _controllers.forEach((v) => v.dispose()); + _controllers.clear(); + } + + set viewportSize(Size size) => _viewportSize = size; + + ViewStateController getOrCreateController(AvesEntry entry) { + var controller = getController(entry); + if (controller != null) { + _controllers.remove(controller); + } else { + // try to initialize the view state to match magnifier initial state + const initialScale = ScaleLevel(ref: ScaleReference.contained); + final initialValue = ViewState( + position: Offset.zero, + scale: ScaleBoundaries( + allowOriginalScaleBeyondRange: true, + minScale: initialScale, + maxScale: initialScale, + initialScale: initialScale, + viewportSize: _viewportSize, + contentSize: entry.displaySize, + ).initialScale, + viewportSize: _viewportSize, + contentSize: entry.displaySize, + ); + controller = ViewStateController( + entry: entry, + viewStateNotifier: ValueNotifier(initialValue), + ); + } + _controllers.insert(0, controller); + while (_controllers.length > maxControllerCount) { + _controllers.removeLast().dispose(); + } + return controller; + } + + ViewStateController? getController(AvesEntry entry) { + return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); + } + + void reset(AvesEntry entry) { + final uris = { + entry, + ...?entry.burstEntries, + }.map((v) => v.uri).toSet(); + _controllers.removeWhere((v) => uris.contains(v.entry.uri)); + } +} diff --git a/lib/widgets/viewer/view/controller.dart b/lib/widgets/viewer/view/controller.dart new file mode 100644 index 000000000..e0ec23538 --- /dev/null +++ b/lib/widgets/viewer/view/controller.dart @@ -0,0 +1,21 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/view_state.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; + +class ViewStateController { + final AvesEntry entry; + final ValueNotifier viewStateNotifier; + final ValueNotifier fullImageNotifier = ValueNotifier(null); + + ViewState get viewState => viewStateNotifier.value; + + ViewStateController({ + required this.entry, + required this.viewStateNotifier, + }); + + void dispose() { + viewStateNotifier.dispose(); + } +} diff --git a/lib/widgets/viewer/visual/conductor.dart b/lib/widgets/viewer/visual/conductor.dart deleted file mode 100644 index 26f6a3b96..000000000 --- a/lib/widgets/viewer/visual/conductor.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/view_state.dart'; -import 'package:aves_magnifier/aves_magnifier.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:tuple/tuple.dart'; - -class ViewStateConductor { - final List>> _controllers = []; - Size _viewportSize = Size.zero; - - static const maxControllerCount = 3; - - Future dispose() async { - _controllers.clear(); - } - - set viewportSize(Size size) => _viewportSize = size; - - ValueNotifier getOrCreateController(AvesEntry entry) { - var controller = _controllers.firstOrNull; - if (controller == null || controller.item1 != entry.uri) { - controller = _controllers.firstWhereOrNull((kv) => kv.item1 == entry.uri); - if (controller != null) { - _controllers.remove(controller); - } else { - // try to initialize the view state to match magnifier initial state - const initialScale = ScaleLevel(ref: ScaleReference.contained); - final initialValue = ViewState( - position: Offset.zero, - scale: ScaleBoundaries( - allowOriginalScaleBeyondRange: true, - minScale: initialScale, - maxScale: initialScale, - initialScale: initialScale, - viewportSize: _viewportSize, - contentSize: entry.displaySize, - ).initialScale, - viewportSize: _viewportSize, - contentSize: entry.displaySize, - ); - controller = Tuple2(entry.uri, ValueNotifier(initialValue)); - } - _controllers.insert(0, controller); - while (_controllers.length > maxControllerCount) { - _controllers.removeLast().item2.dispose(); - } - } - return controller.item2; - } - - void reset(AvesEntry entry) { - final uris = { - entry, - ...?entry.burstEntries, - }.map((v) => v.uri).toSet(); - _controllers.removeWhere((kv) => uris.contains(kv.item1)); - } -} diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 29f463b99..b9759cd40 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -15,7 +15,7 @@ import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:aves/widgets/viewer/view/conductor.dart'; import 'package:aves/widgets/viewer/visual/error.dart'; import 'package:aves/widgets/viewer/visual/raster.dart'; import 'package:aves/model/view_state.dart'; @@ -90,7 +90,7 @@ class _EntryPageViewState extends State with SingleTickerProvider void _registerWidget(EntryPageView widget) { final entry = widget.pageEntry; - _viewStateNotifier = context.read().getOrCreateController(entry); + _viewStateNotifier = context.read().getOrCreateController(entry).viewStateNotifier; _magnifierController = AvesMagnifierController(); _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 0386b9128..65251c618 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -6,9 +6,10 @@ import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/common/fx/checkered_decoration.dart'; -import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/model/view_state.dart'; +import 'package:aves/widgets/common/fx/checkered_decoration.dart'; +import 'package:aves/widgets/viewer/controls/notifications.dart'; +import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -105,6 +106,7 @@ class _RasterImageViewState extends State { void _onFullImageCompleted(ImageInfo image, bool synchronousCall) { _unregisterFullImage(); _fullImageLoaded.value = true; + FullImageLoadedNotification(entry, fullImageProvider).dispatch(context); } @override diff --git a/plugins/aves_model/lib/src/settings/enums.dart b/plugins/aves_model/lib/src/settings/enums.dart index 3f327feca..fecfac6ca 100644 --- a/plugins/aves_model/lib/src/settings/enums.dart +++ b/plugins/aves_model/lib/src/settings/enums.dart @@ -20,6 +20,8 @@ enum KeepScreenOn { never, videoPlayback, viewerOnly, always } enum MaxBrightness { never, viewerOnly, always } +enum OverlayHistogramStyle { none, rgb, luminance } + enum SlideshowVideoPlayback { skip, playMuted, playWithSound } enum SubtitlePosition { top, bottom } diff --git a/plugins/aves_model/lib/src/settings/keys.dart b/plugins/aves_model/lib/src/settings/keys.dart index 82ac1f37e..36f03e422 100644 --- a/plugins/aves_model/lib/src/settings/keys.dart +++ b/plugins/aves_model/lib/src/settings/keys.dart @@ -87,6 +87,7 @@ class SettingKeys { static const viewerQuickActionsKey = 'viewer_quick_actions'; static const showOverlayOnOpeningKey = 'show_overlay_on_opening'; static const showOverlayMinimapKey = 'show_overlay_minimap'; + static const overlayHistogramStyleKey = 'show_overlay_histogram'; static const showOverlayInfoKey = 'show_overlay_info'; static const showOverlayDescriptionKey = 'show_overlay_description'; static const showOverlayRatingTagsKey = 'show_overlay_rating_tags'; diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index 9ec6e62f2..337f1eb82 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -98,10 +98,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: diff --git a/plugins/aves_video_mpv/lib/src/controller.dart b/plugins/aves_video_mpv/lib/src/controller.dart index 2089d53c9..09a785d5d 100644 --- a/plugins/aves_video_mpv/lib/src/controller.dart +++ b/plugins/aves_video_mpv/lib/src/controller.dart @@ -13,6 +13,7 @@ class MpvVideoController extends AvesVideoController { late Player _instance; late VideoController _controller; late VideoStatus _status; + bool _firstFrameRendered = false; final List _subscriptions = []; final StreamController _statusStreamController = StreamController.broadcast(); final StreamController _timedTextStreamController = StreamController.broadcast(); @@ -113,12 +114,17 @@ class MpvVideoController extends AvesVideoController { } void _initController() { + _firstFrameRendered = false; _controller = VideoController( _instance, configuration: VideoControllerConfiguration( enableHardwareAcceleration: settings.enableVideoHardwareAcceleration, ), ); + _controller.waitUntilFirstFrameRendered.then((v) { + _firstFrameRendered = true; + _statusStreamController.add(_status); + }); } @override @@ -171,7 +177,7 @@ class MpvVideoController extends AvesVideoController { case VideoStatus.paused: case VideoStatus.playing: case VideoStatus.completed: - return true; + return _firstFrameRendered; } } diff --git a/plugins/aves_video_mpv/pubspec.lock b/plugins/aves_video_mpv/pubspec.lock index 2f1bda5bf..f26d3d07b 100644 --- a/plugins/aves_video_mpv/pubspec.lock +++ b/plugins/aves_video_mpv/pubspec.lock @@ -98,10 +98,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" flutter: dependency: "direct main" description: flutter @@ -172,34 +172,34 @@ packages: dependency: "direct main" description: name: media_kit - sha256: "272a9f1dd77ed57b48707fdb0ec0e4a048ef958feccc0d0dd751135fe924b63a" + sha256: f19151ff1a1724ed8675f066b40e74af6d155fc859cb74487daeae2cbeff53e0 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3+1" media_kit_libs_android_video: dependency: "direct main" description: name: media_kit_libs_android_video - sha256: ddb0d26ecba72bf7117e37e29b6a50f4ba198bbccb4e47246cae1812087dc721 + sha256: "0a533497d0a982c7146af7dbe226856ef13b05f6d87a6405b1d09d8b40aa2685" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" media_kit_native_event_loop: dependency: "direct main" description: name: media_kit_native_event_loop - sha256: "5351f0c28124b5358756515d8619abad182cdefe967468d7fb5b274737cc2f59" + sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" media_kit_video: dependency: "direct main" description: name: media_kit_video - sha256: "3ac0403d67710dfb2bf6aabfa6caff1b163e70fb7e1a88423bc1be569b4df6b3" + sha256: e286992beee857fee78ce79ed21fd38addb5af0469e10a322d82cf074beb6214 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" meta: dependency: transitive description: diff --git a/plugins/aves_video_mpv/pubspec.yaml b/plugins/aves_video_mpv/pubspec.yaml index 04163965a..4a15a0563 100644 --- a/plugins/aves_video_mpv/pubspec.yaml +++ b/plugins/aves_video_mpv/pubspec.yaml @@ -16,9 +16,9 @@ dependencies: path: ../aves_utils collection: media_kit: - media_kit_video: - media_kit_native_event_loop: media_kit_libs_android_video: + media_kit_native_event_loop: + media_kit_video: dev_dependencies: flutter_lints: diff --git a/pubspec.lock b/pubspec.lock index 9cf0da8e0..52bef2916 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,10 +347,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" ffmpeg_kit_flutter: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "4b1bfbb802d76320a1a46d9ce984106135093efd9d969765d07c2125af107bdf" + sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431" url: "https://pub.dev" source: hosted - version: "0.6.17" + version: "0.6.17+1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -835,34 +835,34 @@ packages: dependency: transitive description: name: media_kit - sha256: "272a9f1dd77ed57b48707fdb0ec0e4a048ef958feccc0d0dd751135fe924b63a" + sha256: f19151ff1a1724ed8675f066b40e74af6d155fc859cb74487daeae2cbeff53e0 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3+1" media_kit_libs_android_video: dependency: transitive description: name: media_kit_libs_android_video - sha256: ddb0d26ecba72bf7117e37e29b6a50f4ba198bbccb4e47246cae1812087dc721 + sha256: "0a533497d0a982c7146af7dbe226856ef13b05f6d87a6405b1d09d8b40aa2685" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" media_kit_native_event_loop: dependency: transitive description: name: media_kit_native_event_loop - sha256: "5351f0c28124b5358756515d8619abad182cdefe967468d7fb5b274737cc2f59" + sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" media_kit_video: dependency: transitive description: name: media_kit_video - sha256: "3ac0403d67710dfb2bf6aabfa6caff1b163e70fb7e1a88423bc1be569b4df6b3" + sha256: e286992beee857fee78ce79ed21fd38addb5af0469e10a322d82cf074beb6214 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" meta: dependency: transitive description: @@ -1531,10 +1531,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" + sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" url: "https://pub.dev" source: hosted - version: "6.0.37" + version: "6.0.38" url_launcher_ios: dependency: transitive description: @@ -1691,10 +1691,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" xml: dependency: "direct main" description: diff --git a/untranslated.json b/untranslated.json index b45b690ce..0eb149fa3 100644 --- a/untranslated.json +++ b/untranslated.json @@ -142,6 +142,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -487,6 +490,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -640,6 +644,9 @@ ], "be": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "viewerTransitionSlide", "viewerTransitionFade", "viewerTransitionZoomIn", @@ -920,6 +927,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -1127,6 +1135,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -1473,6 +1484,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -1638,14 +1650,46 @@ "filePickerUseThisFolder" ], + "cs": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + + "de": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "el": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "aboutDataUsageSectionTitle", "aboutDataUsageData", "aboutDataUsageCache", "aboutDataUsageDatabase", "aboutDataUsageMisc", "aboutDataUsageInternal", - "aboutDataUsageExternal" + "aboutDataUsageExternal", + "settingsViewerShowHistogram" + ], + + "es": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + + "eu": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" ], "fa": [ @@ -1700,6 +1744,9 @@ "maxBrightnessNever", "maxBrightnessAlways", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "vaultLockTypePin", "vaultLockTypePassword", @@ -1996,6 +2043,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -2159,6 +2207,13 @@ "filePickerUseThisFolder" ], + "fr": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "gl": [ "columnCount", "saveCopyButtonLabel", @@ -2200,6 +2255,9 @@ "lengthUnitPercent", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -2537,6 +2595,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -2863,6 +2922,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -3209,6 +3271,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -3515,6 +3578,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -3861,6 +3927,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -4026,6 +4093,27 @@ "filePickerUseThisFolder" ], + "hu": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + + "id": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + + "it": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "ja": [ "columnCount", "saveCopyButtonLabel", @@ -4046,6 +4134,9 @@ "albumTierVaults", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionBottom", "videoResumptionModeNever", "videoResumptionModeAlways", @@ -4068,6 +4159,7 @@ "settingsConfirmationVaultDataLoss", "settingsCollectionBurstPatternsTile", "settingsCollectionBurstPatternsNone", + "settingsViewerShowHistogram", "settingsViewerShowDescription", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", @@ -4218,6 +4310,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -4564,6 +4659,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -4729,6 +4825,13 @@ "filePickerUseThisFolder" ], + "ko": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "lt": [ "columnCount", "saveCopyButtonLabel", @@ -4754,6 +4857,9 @@ "lengthUnitPercent", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "vaultLockTypePin", "vaultLockTypePassword", @@ -4796,6 +4902,7 @@ "settingsConfirmationVaultDataLoss", "settingsCollectionBurstPatternsTile", "settingsCollectionBurstPatternsNone", + "settingsViewerShowHistogram", "settingsViewerShowDescription", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", @@ -4977,6 +5084,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -5323,6 +5433,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -5608,6 +5719,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -5954,6 +6068,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -6133,6 +6248,9 @@ "cropAspectRatioSquare", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "settingsVideoEnablePip", "videoResumptionModeNever", @@ -6154,6 +6272,7 @@ "settingsAskEverytime", "settingsCollectionBurstPatternsTile", "settingsCollectionBurstPatternsNone", + "settingsViewerShowHistogram", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", "settingsVideoResumptionModeTile", @@ -6184,6 +6303,9 @@ "albumTierVaults", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "vaultLockTypePattern", @@ -6225,6 +6347,7 @@ "settingsConfirmationVaultDataLoss", "settingsCollectionBurstPatternsTile", "settingsCollectionBurstPatternsNone", + "settingsViewerShowHistogram", "settingsViewerShowRatingTags", "settingsViewerShowDescription", "settingsVideoPlaybackTile", @@ -6247,6 +6370,9 @@ "nn": [ "sourceStateCataloguing", "accessibilityAnimationsKeep", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "settingsVideoEnablePip", "widgetTapUpdateWidget", "authenticateToConfigureVault", @@ -6266,6 +6392,7 @@ "collectionActionShowTitleSearch", "collectionActionHideTitleSearch", "drawerCollectionAnimated", + "settingsViewerShowHistogram", "settingsSlideshowAnimatedZoomEffect", "settingsHiddenItemsTabFilters", "settingsHiddenFiltersBanner", @@ -6427,6 +6554,9 @@ "maxBrightnessNever", "maxBrightnessAlways", "nameConflictStrategyReplace", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "unitSystemMetric", @@ -6721,6 +6851,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformationSubtitle", "settingsViewerShowRatingTags", @@ -6883,14 +7014,25 @@ "filePickerUseThisFolder" ], + "pl": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "pt": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "aboutDataUsageSectionTitle", "aboutDataUsageData", "aboutDataUsageCache", "aboutDataUsageDatabase", "aboutDataUsageMisc", "aboutDataUsageInternal", - "aboutDataUsageExternal" + "aboutDataUsageExternal", + "settingsViewerShowHistogram" ], "ro": [ @@ -6904,6 +7046,9 @@ "cropAspectRatioSquare", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "videoResumptionModeNever", "videoResumptionModeAlways", "widgetTapUpdateWidget", @@ -6916,6 +7061,7 @@ "aboutDataUsageInternal", "aboutDataUsageExternal", "settingsAskEverytime", + "settingsViewerShowHistogram", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", "settingsVideoResumptionModeTile", @@ -6923,6 +7069,13 @@ "tagEditorDiscardDialogMessage" ], + "ru": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "sk": [ "itemCount", "columnCount", @@ -6950,6 +7103,9 @@ "lengthUnitPercent", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "vaultLockTypePin", "vaultLockTypePassword", @@ -7218,6 +7374,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -7544,6 +7701,9 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "subtitlePositionTop", "subtitlePositionBottom", "themeBrightnessLight", @@ -7890,6 +8050,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -8083,6 +8244,9 @@ "lengthUnitPercent", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "vaultLockTypePin", "vaultLockTypePassword", @@ -8285,6 +8449,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowHistogram", "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", @@ -8471,6 +8636,9 @@ "lengthUnitPercent", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "vaultLockTypePin", "vaultLockTypePassword", @@ -8511,6 +8679,7 @@ "settingsConfirmationVaultDataLoss", "settingsCollectionBurstPatternsTile", "settingsCollectionBurstPatternsNone", + "settingsViewerShowHistogram", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", "settingsVideoResumptionModeTile", @@ -8523,6 +8692,13 @@ "tagPlaceholderState" ], + "uk": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", + "settingsViewerShowHistogram" + ], + "zh": [ "saveCopyButtonLabel", "chipActionGoToPlacePage", @@ -8537,6 +8713,9 @@ "lengthUnitPercent", "maxBrightnessNever", "maxBrightnessAlways", + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "vaultLockTypePattern", "vaultLockTypePin", "settingsVideoEnablePip", @@ -8573,6 +8752,7 @@ "settingsConfirmationVaultDataLoss", "settingsCollectionBurstPatternsTile", "settingsCollectionBurstPatternsNone", + "settingsViewerShowHistogram", "settingsViewerShowDescription", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", @@ -8588,12 +8768,16 @@ ], "zh_Hant": [ + "overlayHistogramNone", + "overlayHistogramRGB", + "overlayHistogramLuminance", "aboutDataUsageSectionTitle", "aboutDataUsageData", "aboutDataUsageCache", "aboutDataUsageDatabase", "aboutDataUsageMisc", "aboutDataUsageInternal", - "aboutDataUsageExternal" + "aboutDataUsageExternal", + "settingsViewerShowHistogram" ] }