diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e964f15..d55cc5b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +- Viewer: optionally show rating & tags on overlay - Viewer: long press on rating quick action for quicker rating - Search: missing address filter - Lithuanian translation (thanks Gediminas Murauskas) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index aad5260d7..9fc8bfa26 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -717,6 +717,7 @@ "settingsViewerShowMinimap": "Show minimap", "settingsViewerShowInformation": "Show information", "settingsViewerShowInformationSubtitle": "Show title, date, location, etc.", + "settingsViewerShowRatingTags": "Show rating & tags", "settingsViewerShowShootingDetails": "Show shooting details", "settingsViewerShowOverlayThumbnails": "Show thumbnails", "settingsViewerEnableOverlayBlurEffect": "Blur effect", diff --git a/lib/model/entry_info.dart b/lib/model/entry_info.dart index d0f6c030b..4589b5f6f 100644 --- a/lib/model/entry_info.dart +++ b/lib/model/entry_info.dart @@ -8,6 +8,7 @@ import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/theme/colors.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -109,7 +110,10 @@ extension ExtraAvesEntryInfo on AvesEntry { for (final stream in knownStreams) { final index = (stream[Keys.index] ?? 0) + 1; final typeText = getTypeText(stream); - final dirName = 'Stream ${index.toString().padLeft(indexDigits, '0')} • $typeText'; + final dirName = [ + 'Stream ${index.toString().padLeft(indexDigits, '0')}', + typeText, + ].join(Constants.separator); final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream); if (formattedStreamTags.isNotEmpty) { final color = colors.fromString(typeText); diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index e419d7847..b45e542d8 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -79,6 +79,7 @@ class SettingsDefaults { static const showOverlayOnOpening = true; static const showOverlayMinimap = false; static const showOverlayInfo = true; + static const showOverlayRatingTags = false; static const showOverlayShootingDetails = false; static const showOverlayThumbnailPreview = false; static const viewerGestureSideTapNext = false; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 5a0494a9b..e0e83353e 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -103,6 +103,7 @@ class Settings extends ChangeNotifier { static const showOverlayOnOpeningKey = 'show_overlay_on_opening'; static const showOverlayMinimapKey = 'show_overlay_minimap'; static const showOverlayInfoKey = 'show_overlay_info'; + static const showOverlayRatingTagsKey = 'show_overlay_rating_tags'; static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details'; static const showOverlayThumbnailPreviewKey = 'show_overlay_thumbnail_preview'; static const viewerGestureSideTapNextKey = 'viewer_gesture_side_tap_next'; @@ -507,6 +508,10 @@ class Settings extends ChangeNotifier { set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue); + bool get showOverlayRatingTags => getBool(showOverlayRatingTagsKey) ?? SettingsDefaults.showOverlayRatingTags; + + set showOverlayRatingTags(bool newValue) => setAndNotify(showOverlayRatingTagsKey, newValue); + bool get showOverlayShootingDetails => getBool(showOverlayShootingDetailsKey) ?? SettingsDefaults.showOverlayShootingDetails; set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue); @@ -932,6 +937,7 @@ class Settings extends ChangeNotifier { case showOverlayOnOpeningKey: case showOverlayMinimapKey: case showOverlayInfoKey: + case showOverlayRatingTagsKey: case showOverlayShootingDetailsKey: case showOverlayThumbnailPreviewKey: case viewerGestureSideTapNextKey: diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index 496e9ad0c..b1c85159f 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/utils/constants.dart'; import 'package:flutter/services.dart'; abstract class EmbeddedDataService { @@ -37,7 +38,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - 'displayName': '${entry.bestTitle} • Video', + 'displayName': ['${entry.bestTitle}', 'Video'].join(Constants.separator), }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { @@ -51,7 +52,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { try { final result = await _platform.invokeMethod('extractVideoEmbeddedPicture', { 'uri': entry.uri, - 'displayName': '${entry.bestTitle} • Cover', + 'displayName': ['${entry.bestTitle}', 'Cover'].join(Constants.separator), }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { @@ -67,7 +68,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - 'displayName': '${entry.bestTitle} • $props', + 'displayName': ['${entry.bestTitle}', '$props'].join(Constants.separator), 'propPath': props, 'propMimeType': propMimeType, }); diff --git a/lib/theme/format.dart b/lib/theme/format.dart index e0ce13501..854299a08 100644 --- a/lib/theme/format.dart +++ b/lib/theme/format.dart @@ -1,10 +1,14 @@ +import 'package:aves/utils/constants.dart'; import 'package:intl/intl.dart'; String formatDay(DateTime date, String locale) => DateFormat.yMMMd(locale).format(date); String formatTime(DateTime date, String locale, bool use24hour) => (use24hour ? DateFormat.Hm(locale) : DateFormat.jm(locale)).format(date); -String formatDateTime(DateTime date, String locale, bool use24hour) => '${formatDay(date, locale)} • ${formatTime(date, locale, use24hour)}'; +String formatDateTime(DateTime date, String locale, bool use24hour) => [ + formatDay(date, locale), + formatTime(date, locale, use24hour), + ].join(Constants.separator); String formatFriendlyDuration(Duration d) { final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0'); diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 8e662de47..aa618c6c7 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; class Constants { + static const separator = ' • '; + // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) // when used in gradients or lerping to it static const transparentWhite = Color(0x00FFFFFF); diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index b13c66d4d..2f6bcba83 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -138,7 +138,7 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> TextSpan( children: [ ...widget.spans.expandIndexed((i, v) => [ - if (i != 0) const TextSpan(text: ' • '), + if (i != 0) const TextSpan(text: Constants.separator), TextSpan(text: v, style: i == _highlightedIndex ? _animatedStyle.value : _baseStyle), ]) ], diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index c8cae6351..ff980c231 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -30,6 +30,18 @@ class ViewerOverlayPage extends StatelessWidget { title: context.l10n.settingsViewerShowInformation, subtitle: context.l10n.settingsViewerShowInformationSubtitle, ), + Selector>( + selector: (context, s) => Tuple2(s.showOverlayInfo, s.showOverlayRatingTags), + builder: (context, s, child) { + final showInfo = s.item1; + final current = s.item2; + return SwitchListTile( + value: current, + onChanged: showInfo ? (v) => settings.showOverlayRatingTags = v : null, + title: Text(context.l10n.settingsViewerShowRatingTags), + ); + }, + ), Selector>( selector: (context, s) => Tuple2(s.showOverlayInfo, s.showOverlayShootingDetails), builder: (context, s, child) { diff --git a/lib/widgets/viewer/overlay/details/date.dart b/lib/widgets/viewer/overlay/details/date.dart new file mode 100644 index 000000000..da3bdb092 --- /dev/null +++ b/lib/widgets/viewer/overlay/details/date.dart @@ -0,0 +1,44 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/theme/format.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class OverlayDateRow extends StatelessWidget { + final AvesEntry entry; + final MultiPageController? multiPageController; + + const OverlayDateRow({ + super.key, + required this.entry, + required this.multiPageController, + }); + + @override + Widget build(BuildContext context) { + final locale = context.l10n.localeName; + final use24hour = context.select((v) => v.alwaysUse24HourFormat); + + final date = entry.bestDate; + final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; + final resolutionText = entry.isSvg + ? entry.aspectRatioText + : entry.isSized + ? entry.resolutionText + : ''; + + return Row( + children: [ + DecoratedIcon(AIcons.date, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), + const SizedBox(width: ViewerDetailOverlayContent.iconPadding), + Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), + Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), + ], + ); + } +} diff --git a/lib/widgets/viewer/overlay/details.dart b/lib/widgets/viewer/overlay/details/details.dart similarity index 60% rename from lib/widgets/viewer/overlay/details.dart rename to lib/widgets/viewer/overlay/details/details.dart index fb4e69f8c..38ff26049 100644 --- a/lib/widgets/viewer/overlay/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -2,30 +2,21 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata/overlay.dart'; -import 'package:aves/model/multipage.dart'; -import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/format.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:aves/widgets/viewer/overlay/details/date.dart'; +import 'package:aves/widgets/viewer/overlay/details/location.dart'; +import 'package:aves/widgets/viewer/overlay/details/position_title.dart'; +import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart'; +import 'package:aves/widgets/viewer/overlay/details/shooting.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; -import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -const double _iconPadding = 8.0; -const double _iconSize = 16.0; -const double _interRowPadding = 2.0; -const double _subRowMinWidth = 300.0; - -List? _shadows(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null; - class ViewerDetailOverlay extends StatefulWidget { final List entries; final int index; @@ -124,7 +115,13 @@ class ViewerDetailOverlayContent extends StatelessWidget { final double availableWidth; final MultiPageController? multiPageController; + static const double _interRowPadding = 2.0; + static const double _subRowMinWidth = 300.0; static const padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8); + static const double iconPadding = 8.0; + static const double iconSize = 16.0; + + static List? shadows(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null; const ViewerDetailOverlayContent({ super.key, @@ -138,15 +135,16 @@ class ViewerDetailOverlayContent extends StatelessWidget { @override Widget build(BuildContext context) { final infoMaxWidth = availableWidth - padding.horizontal; + final showRatingTags = settings.showOverlayRatingTags; final showShooting = settings.showOverlayShootingDetails; return AnimatedBuilder( animation: pageEntry.metadataChangeNotifier, builder: (context, child) { - final positionTitle = _PositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); + final positionTitle = OverlayPositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); return DefaultTextStyle( style: Theme.of(context).textTheme.bodyMedium!.copyWith( - shadows: _shadows(context), + shadows: shadows(context), ), softWrap: false, overflow: TextOverflow.fade, @@ -185,6 +183,9 @@ class ViewerDetailOverlayContent extends StatelessWidget { if (!collapsedLocation) { rows.add(_buildLocationFullRow(context)); } + if (showRatingTags) { + rows.add(_buildRatingTagsFullRow(context)); + } return Column( mainAxisSize: MainAxisSize.min, @@ -201,18 +202,24 @@ class ViewerDetailOverlayContent extends StatelessWidget { Widget _buildDateSubRow(double subRowWidth) => SizedBox( width: subRowWidth, - child: _DateRow( + child: OverlayDateRow( entry: pageEntry, multiPageController: multiPageController, ), ); + Widget _buildRatingTagsFullRow(BuildContext context) => _buildFullRowSwitcher( + context: context, + visible: pageEntry.rating != 0 || pageEntry.tags.isNotEmpty, + builder: (context) => OverlayRatingTagsRow(entry: pageEntry), + ); + Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher( context: context, visible: details != null && details!.isNotEmpty, builder: (context) => SizedBox( width: subRowWidth, - child: _ShootingRow(details!), + child: OverlayShootingRow(details: details!), ), ); @@ -220,20 +227,20 @@ class ViewerDetailOverlayContent extends StatelessWidget { context: context, subRowWidth: subRowWidth, visible: details != null && details!.isNotEmpty, - builder: (context) => _ShootingRow(details!), + builder: (context) => OverlayShootingRow(details: details!), ); Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher( context: context, visible: pageEntry.hasGps, - builder: (context) => _LocationRow(entry: pageEntry), + builder: (context) => OverlayLocationRow(entry: pageEntry), ); Widget _buildLocationSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher( context: context, subRowWidth: subRowWidth, visible: pageEntry.hasGps, - builder: (context) => _LocationRow(entry: pageEntry), + builder: (context) => OverlayLocationRow(entry: pageEntry), ); Widget _buildSubRowSwitcher({ @@ -283,146 +290,3 @@ class ViewerDetailOverlayContent extends StatelessWidget { : const SizedBox(), ); } - -class _LocationRow extends AnimatedWidget { - final AvesEntry entry; - - _LocationRow({ - required this.entry, - }) : super(listenable: entry.addressChangeNotifier); - - @override - Widget build(BuildContext context) { - late final String location; - if (entry.hasAddress) { - location = entry.shortAddress; - } else { - final latLng = entry.latLng; - if (latLng != null) { - location = settings.coordinateFormat.format(context.l10n, latLng); - } else { - location = ''; - } - } - return Row( - children: [ - DecoratedIcon(AIcons.location, size: _iconSize, shadows: _shadows(context)), - const SizedBox(width: _iconPadding), - Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), - ], - ); - } -} - -class _PositionTitleRow extends StatelessWidget { - final AvesEntry entry; - final String? collectionPosition; - final MultiPageController? multiPageController; - - const _PositionTitleRow({ - required this.entry, - required this.collectionPosition, - required this.multiPageController, - }); - - String? get title => entry.bestTitle; - - bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null; - - static const separator = ' • '; - - @override - Widget build(BuildContext context) { - Text toText({String? pagePosition}) => Text( - [ - if (collectionPosition != null) collectionPosition, - if (pagePosition != null) pagePosition, - if (title != null) '${Constants.fsi}$title${Constants.pdi}', - ].join(separator), - strutStyle: Constants.overflowStrutStyle); - - if (multiPageController == null) return toText(); - - return StreamBuilder( - stream: multiPageController!.infoStream, - builder: (context, snapshot) { - final multiPageInfo = multiPageController!.info; - String? pagePosition; - if (multiPageInfo != null) { - // page count may be 0 when we know an entry to have multiple pages - // but fail to get information about these pages - final pageCount = multiPageInfo.pageCount; - if (pageCount > 0) { - final page = multiPageInfo.getById(entry.pageId ?? entry.id) ?? multiPageInfo.defaultPage; - pagePosition = '${(page?.index ?? 0) + 1}/$pageCount'; - } - } - return toText(pagePosition: pagePosition); - }, - ); - } -} - -class _DateRow extends StatelessWidget { - final AvesEntry entry; - final MultiPageController? multiPageController; - - const _DateRow({ - required this.entry, - required this.multiPageController, - }); - - @override - Widget build(BuildContext context) { - final locale = context.l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); - - final date = entry.bestDate; - final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; - final resolutionText = entry.isSvg - ? entry.aspectRatioText - : entry.isSized - ? entry.resolutionText - : ''; - - return Row( - children: [ - DecoratedIcon(AIcons.date, size: _iconSize, shadows: _shadows(context)), - const SizedBox(width: _iconPadding), - Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), - Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), - ], - ); - } -} - -class _ShootingRow extends StatelessWidget { - final OverlayMetadata details; - - const _ShootingRow(this.details); - - @override - Widget build(BuildContext context) { - final locale = context.l10n.localeName; - - final aperture = details.aperture; - final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown; - - final focalLength = details.focalLength; - final focalLengthText = focalLength != null ? context.l10n.focalLength(NumberFormat('0.#', locale).format(focalLength)) : Constants.overlayUnknown; - - final iso = details.iso; - final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown; - - return Row( - children: [ - DecoratedIcon(AIcons.shooting, size: _iconSize, shadows: _shadows(context)), - const SizedBox(width: _iconPadding), - Expanded(child: Text(apertureText, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(focalLengthText, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(isoText, strutStyle: Constants.overflowStrutStyle)), - ], - ); - } -} diff --git a/lib/widgets/viewer/overlay/details/location.dart b/lib/widgets/viewer/overlay/details/location.dart new file mode 100644 index 000000000..aecf9dd2a --- /dev/null +++ b/lib/widgets/viewer/overlay/details/location.dart @@ -0,0 +1,40 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; + +class OverlayLocationRow extends AnimatedWidget { + final AvesEntry entry; + + OverlayLocationRow({ + super.key, + required this.entry, + }) : super(listenable: entry.addressChangeNotifier); + + @override + Widget build(BuildContext context) { + late final String location; + if (entry.hasAddress) { + location = entry.shortAddress; + } else { + final latLng = entry.latLng; + if (latLng != null) { + location = settings.coordinateFormat.format(context.l10n, latLng); + } else { + location = ''; + } + } + return Row( + children: [ + DecoratedIcon(AIcons.location, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), + const SizedBox(width: ViewerDetailOverlayContent.iconPadding), + Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), + ], + ); + } +} diff --git a/lib/widgets/viewer/overlay/details/position_title.dart b/lib/widgets/viewer/overlay/details/position_title.dart new file mode 100644 index 000000000..9d4767925 --- /dev/null +++ b/lib/widgets/viewer/overlay/details/position_title.dart @@ -0,0 +1,53 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/model/multipage.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:flutter/material.dart'; + +class OverlayPositionTitleRow extends StatelessWidget { + final AvesEntry entry; + final String? collectionPosition; + final MultiPageController? multiPageController; + + const OverlayPositionTitleRow({ + super.key, + required this.entry, + required this.collectionPosition, + required this.multiPageController, + }); + + String? get title => entry.bestTitle; + + bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null; + + @override + Widget build(BuildContext context) { + Text toText({String? pagePosition}) => Text( + [ + if (collectionPosition != null) collectionPosition, + if (pagePosition != null) pagePosition, + if (title != null) '${Constants.fsi}$title${Constants.pdi}', + ].join(Constants.separator), + strutStyle: Constants.overflowStrutStyle); + + if (multiPageController == null) return toText(); + + return StreamBuilder( + stream: multiPageController!.infoStream, + builder: (context, snapshot) { + final multiPageInfo = multiPageController!.info; + String? pagePosition; + if (multiPageInfo != null) { + // page count may be 0 when we know an entry to have multiple pages + // but fail to get information about these pages + final pageCount = multiPageInfo.pageCount; + if (pageCount > 0) { + final page = multiPageInfo.getById(entry.pageId ?? entry.id) ?? multiPageInfo.defaultPage; + pagePosition = '${(page?.index ?? 0) + 1}/$pageCount'; + } + } + return toText(pagePosition: pagePosition); + }, + ); + } +} diff --git a/lib/widgets/viewer/overlay/details/rating_tags.dart b/lib/widgets/viewer/overlay/details/rating_tags.dart new file mode 100644 index 000000000..92aced804 --- /dev/null +++ b/lib/widgets/viewer/overlay/details/rating_tags.dart @@ -0,0 +1,50 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; + +class OverlayRatingTagsRow extends AnimatedWidget { + final AvesEntry entry; + + OverlayRatingTagsRow({ + super.key, + required this.entry, + }) : super(listenable: entry.metadataChangeNotifier); + + @override + Widget build(BuildContext context) { + final String ratingString; + final rating = entry.rating.clamp(-1, 5); + switch (rating) { + case -1: + ratingString = context.l10n.filterRatingRejectedLabel; + break; + case 0: + ratingString = ''; + break; + default: + ratingString = '${'★' * rating}${'☆' * (5 - rating)}'; + break; + } + + final tags = entry.tags.join(Constants.separator); + final hasTags = tags.isNotEmpty; + + return Row( + children: [ + if (ratingString.isNotEmpty) ...[ + Text(ratingString), + if (hasTags) const Text(Constants.separator), + ], + if (hasTags) ...[ + DecoratedIcon(AIcons.tag, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), + const SizedBox(width: ViewerDetailOverlayContent.iconPadding), + Expanded(child: Text(tags)), + ], + ], + ); + } +} diff --git a/lib/widgets/viewer/overlay/details/shooting.dart b/lib/widgets/viewer/overlay/details/shooting.dart new file mode 100644 index 000000000..e28dfb7c8 --- /dev/null +++ b/lib/widgets/viewer/overlay/details/shooting.dart @@ -0,0 +1,39 @@ +import 'package:aves/model/metadata/overlay.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class OverlayShootingRow extends StatelessWidget { + final OverlayMetadata details; + + const OverlayShootingRow({super.key, required this.details}); + + @override + Widget build(BuildContext context) { + final locale = context.l10n.localeName; + + final aperture = details.aperture; + final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown; + + final focalLength = details.focalLength; + final focalLengthText = focalLength != null ? context.l10n.focalLength(NumberFormat('0.#', locale).format(focalLength)) : Constants.overlayUnknown; + + final iso = details.iso; + final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown; + + return Row( + children: [ + DecoratedIcon(AIcons.shooting, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), + const SizedBox(width: ViewerDetailOverlayContent.iconPadding), + Expanded(child: Text(apertureText, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(focalLengthText, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(isoText, strutStyle: Constants.overflowStrutStyle)), + ], + ); + } +} diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 6842773c5..657eee86d 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -3,7 +3,7 @@ import 'package:aves/model/settings/settings.dart'; 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.dart'; +import 'package:aves/widgets/viewer/overlay/details/details.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'; diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 91712fbbd..f043aa223 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -2,6 +2,7 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; @@ -155,7 +156,7 @@ class _WelcomePageState extends State { value: settings.isInstalledAppAccessAllowed, onChanged: (v) => setState(() => settings.isInstalledAppAccessAllowed = v), title: Text(l10n.settingsAllowInstalledAppAccess), - subtitle: Text([l10n.welcomeOptional, l10n.settingsAllowInstalledAppAccessSubtitle].join(' • ')), + subtitle: Text([l10n.welcomeOptional, l10n.settingsAllowInstalledAppAccessSubtitle].join(Constants.separator)), contentPadding: contentPadding, ), if (canEnableErrorReporting) diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index abda8fdee..755179fb1 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -46,6 +46,7 @@ Future configureAndLaunch() async { ..showOverlayOnOpening = true ..showOverlayMinimap = false ..showOverlayInfo = true + ..showOverlayRatingTags = false ..showOverlayShootingDetails = false ..showOverlayThumbnailPreview = false ..viewerUseCutout = true diff --git a/untranslated.json b/untranslated.json index 57a0f16aa..8c4d05da4 100644 --- a/untranslated.json +++ b/untranslated.json @@ -443,6 +443,7 @@ "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", @@ -596,17 +597,20 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsViewerShowRatingTags", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsWidgetDisplayedItem" ], "el": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "es": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "fa": [ @@ -1053,6 +1057,7 @@ "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", @@ -1201,7 +1206,8 @@ ], "fr": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "gl": [ @@ -1515,6 +1521,7 @@ "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", @@ -1669,13 +1676,15 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsViewerShowRatingTags", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsWidgetDisplayedItem" ], "it": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "ja": [ @@ -1686,17 +1695,20 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsViewerShowRatingTags", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsWidgetDisplayedItem" ], "ko": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "lt": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "nb": [ @@ -1788,6 +1800,7 @@ "settingsViewerOverlayTile", "settingsViewerOverlayPageTitle", "settingsViewerShowOverlayOnOpening", + "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", "settingsViewerEnableOverlayBlurEffect", "settingsSlideshowShuffle", @@ -1828,6 +1841,7 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsViewerShowRatingTags", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsWidgetDisplayedItem" @@ -2184,6 +2198,7 @@ "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", @@ -2338,17 +2353,20 @@ "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "settingsViewerShowRatingTags", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsWidgetDisplayedItem" ], "ro": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "ru": [ - "filterNoAddressLabel" + "filterNoAddressLabel", + "settingsViewerShowRatingTags" ], "th": [ @@ -2575,6 +2593,7 @@ "settingsViewerShowMinimap", "settingsViewerShowInformation", "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", @@ -2723,20 +2742,14 @@ ], "tr": [ - "entryInfoActionExportMetadata", "filterNoAddressLabel", - "subtitlePositionTop", - "subtitlePositionBottom", - "widgetDisplayedItemRandom", - "widgetDisplayedItemMostRecent", - "settingsSubtitleThemeTextPositionTile", - "settingsSubtitleThemeTextPositionDialogTitle", - "settingsWidgetDisplayedItem" + "settingsViewerShowRatingTags" ], "zh": [ "filterNoAddressLabel", "aboutLicensesFlutterPackagesSectionTitle", - "aboutLicensesDartPackagesSectionTitle" + "aboutLicensesDartPackagesSectionTitle", + "settingsViewerShowRatingTags" ] }