From 4fc6b17de1a3d6eceff085123216c2c23d1c3857 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Fri, 21 Jun 2024 00:32:41 +0800 Subject: [PATCH] Add a dialog that lead user to the video preview help page --- app/lib/controller/pref_controller.dart | 9 ++++ app/lib/controller/pref_controller.g.dart | 9 ++++ app/lib/controller/pref_controller/util.dart | 7 +++ app/lib/entity/pref.dart | 3 ++ app/lib/help_utils.dart | 1 + app/lib/l10n/app_en.arb | 2 + app/lib/l10n/untranslated-messages.txt | 56 +++++++++++++++----- app/lib/session_storage.dart | 2 + app/lib/widget/home_photos/bloc.dart | 10 ++++ app/lib/widget/home_photos/state_event.dart | 12 +++++ app/lib/widget/home_photos/type.dart | 3 ++ app/lib/widget/home_photos/view.dart | 27 ++++++++++ app/lib/widget/home_photos2.dart | 20 +++++++ app/lib/widget/home_photos2.g.dart | 13 ++++- app/lib/widget/photo_list_item.dart | 33 +++++++----- 15 files changed, 180 insertions(+), 27 deletions(-) diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index ed2973b5..e218318d 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -152,6 +152,12 @@ class PrefController { value: value, ); + Future setDontShowVideoPreviewHint(bool value) => _set( + controller: _isDontShowVideoPreviewHintController, + setter: (pref, value) => pref.setDontShowVideoPreviewHint(value), + value: value, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -249,6 +255,9 @@ class PrefController { @NpSubjectAccessor(type: "Color?") late final _secondarySeedColorController = BehaviorSubject.seeded( _c.pref.getSecondarySeedColor()?.run(Color.new)); + @npSubjectAccessor + late final _isDontShowVideoPreviewHintController = + BehaviorSubject.seeded(_c.pref.isDontShowVideoPreviewHintOr(false)); } @npSubjectAccessor diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 0bcaa4ea..edef3a3e 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -148,6 +148,15 @@ extension $PrefControllerNpSubjectAccessor on PrefController { Stream get secondarySeedColorChange => secondarySeedColor.distinct().skip(1); Color? get secondarySeedColorValue => _secondarySeedColorController.value; +// _isDontShowVideoPreviewHintController + ValueStream get isDontShowVideoPreviewHint => + _isDontShowVideoPreviewHintController.stream; + Stream get isDontShowVideoPreviewHintNew => + isDontShowVideoPreviewHint.skip(1); + Stream get isDontShowVideoPreviewHintChange => + isDontShowVideoPreviewHint.distinct().skip(1); + bool get isDontShowVideoPreviewHintValue => + _isDontShowVideoPreviewHintController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/controller/pref_controller/util.dart b/app/lib/controller/pref_controller/util.dart index eb727c50..f68d5fc0 100644 --- a/app/lib/controller/pref_controller/util.dart +++ b/app/lib/controller/pref_controller/util.dart @@ -81,4 +81,11 @@ extension on Pref { return provider.setString(PrefKey.protectedPageAuthPassword, value); } } + + bool? isDontShowVideoPreviewHint() => + provider.getBool(PrefKey.dontShowVideoPreviewHint); + bool isDontShowVideoPreviewHintOr(bool def) => + isDontShowVideoPreviewHint() ?? def; + Future setDontShowVideoPreviewHint(bool value) => + provider.setBool(PrefKey.dontShowVideoPreviewHint, value); } diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index 411c0e45..9ee99885 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -112,6 +112,7 @@ enum PrefKey implements PrefKeyInterface { protectedPageAuthType, protectedPageAuthPin, protectedPageAuthPassword, + dontShowVideoPreviewHint, ; @override @@ -196,6 +197,8 @@ enum PrefKey implements PrefKeyInterface { return "protectedPageAuthPin"; case PrefKey.protectedPageAuthPassword: return "protectedPageAuthPassword"; + case PrefKey.dontShowVideoPreviewHint: + return "dontShowVideoPreviewHint"; } } } diff --git a/app/lib/help_utils.dart b/app/lib/help_utils.dart index 30bf8a8a..a1de2926 100644 --- a/app/lib/help_utils.dart +++ b/app/lib/help_utils.dart @@ -13,3 +13,4 @@ const enhanceRetouchUrl = "https://bit.ly/3Ds2cea"; const editPhotosUrl = "https://bit.ly/3v82oKA"; const collectionTypesUrl = "https://bit.ly/3OwSiNq"; const contributorsUrl = "https://bit.ly/3QhlQQs"; +const videoPreviewUrl = "https://bit.ly/4c7cazP"; diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 0dcb2f6b..9ea6a2ae 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1485,6 +1485,8 @@ "description": "There's no HTTPS server in the account list" }, "trustedCertManagerFailedToRemoveCertError": "Failed to remove certificate", + "missingVideoThumbnailHelpDialogTitle": "Having trouble with video thumbnails?", + "dontShowAgain": "Don't show again", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index ab5dcf2d..823f20c0 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -247,6 +247,8 @@ "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -280,7 +282,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "de": [ @@ -332,7 +336,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "el": [ @@ -467,7 +473,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "es": [ @@ -493,7 +501,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "fi": [ @@ -519,7 +529,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "fr": [ @@ -545,7 +557,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "it": [ @@ -576,7 +590,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "nl": [ @@ -944,6 +960,8 @@ "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -981,7 +999,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "pt": [ @@ -1027,7 +1047,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "ru": [ @@ -1053,7 +1075,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "tr": [ @@ -1062,7 +1086,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "zh": [ @@ -1119,7 +1145,9 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ], "zh_Hant": [ @@ -1270,6 +1298,8 @@ "trustedCertManagerAlreadyTrustedError", "trustedCertManagerSelectServer", "trustedCertManagerNoHttpsServerError", - "trustedCertManagerFailedToRemoveCertError" + "trustedCertManagerFailedToRemoveCertError", + "missingVideoThumbnailHelpDialogTitle", + "dontShowAgain" ] } diff --git a/app/lib/session_storage.dart b/app/lib/session_storage.dart index 38252a75..cd26829d 100644 --- a/app/lib/session_storage.dart +++ b/app/lib/session_storage.dart @@ -22,5 +22,7 @@ class SessionStorage { DateTime lastSuspendTime = clock.now(); + bool hasShownVideoPreviewHint = false; + static final _inst = SessionStorage._(); } diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 1b668823..ea3242cc 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -50,6 +50,8 @@ class _Bloc extends Bloc<_Event, _State> on<_UpdateDateTimeGroup>(_onUpdateDateTimeGroup); on<_UpdateMemories>(_onUpdateMemories); + on<_TripMissingVideoPreview>(_onTripMissingVideoPreview); + on<_SetError>(_onSetError); _subscriptions @@ -486,6 +488,14 @@ class _Bloc extends Bloc<_Event, _State> )); } + void _onTripMissingVideoPreview( + _TripMissingVideoPreview ev, Emitter<_State> emit) { + // _log.info(ev); + if (!state.hasMissingVideoPreview) { + emit(state.copyWith(hasMissingVideoPreview: true)); + } + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/home_photos/state_event.dart b/app/lib/widget/home_photos/state_event.dart index a954cb44..d6579b33 100644 --- a/app/lib/widget/home_photos/state_event.dart +++ b/app/lib/widget/home_photos/state_event.dart @@ -25,6 +25,7 @@ class _State { this.minimapItems, required this.minimapYRatio, this.scrollDate, + required this.hasMissingVideoPreview, this.error, }); @@ -45,6 +46,7 @@ class _State { isScrolling: false, filesSummary: const DbFilesSummary(items: {}), minimapYRatio: 1, + hasMissingVideoPreview: false, ); @override @@ -76,6 +78,8 @@ class _State { final double minimapYRatio; final Date? scrollDate; + final bool hasMissingVideoPreview; + final ExceptionEvent? error; } @@ -315,6 +319,14 @@ class _UpdateMemories implements _Event { String toString() => _$toString(); } +@toString +class _TripMissingVideoPreview implements _Event { + const _TripMissingVideoPreview(); + + @override + String toString() => _$toString(); +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/home_photos/type.dart b/app/lib/widget/home_photos/type.dart index 6379e12b..6f829aec 100644 --- a/app/lib/widget/home_photos/type.dart +++ b/app/lib/widget/home_photos/type.dart @@ -72,6 +72,9 @@ class _VideoItem extends _FileItem { account: account, previewUrl: _previewUrl, isFavorite: file.fdIsFavorite, + onError: () { + context.addEvent(const _TripMissingVideoPreview()); + }, ); } diff --git a/app/lib/widget/home_photos/view.dart b/app/lib/widget/home_photos/view.dart index 26f60827..5aa74473 100644 --- a/app/lib/widget/home_photos/view.dart +++ b/app/lib/widget/home_photos/view.dart @@ -311,3 +311,30 @@ class _ScrollLabel extends StatelessWidget { ); } } + +class _VideoPreviewHintDialog extends StatelessWidget { + const _VideoPreviewHintDialog(); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(L10n.global().missingVideoThumbnailHelpDialogTitle), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + launch(help_util.videoPreviewUrl); + }, + child: Text(L10n.global().learnMoreButtonLabel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().setDontShowVideoPreviewHint(true); + }, + child: Text(L10n.global().dontShowAgain), + ), + ], + ); + } +} diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index 9b8508d3..5741c85f 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -33,13 +33,16 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/flutter_util.dart' as flutter_util; +import 'package:nc_photos/help_utils.dart' as help_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; +import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/stream_extension.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme/dimension.dart'; +import 'package:nc_photos/url_launcher_util.dart'; import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/collection_picker.dart'; import 'package:nc_photos/widget/file_sharer_dialog.dart'; @@ -133,6 +136,23 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { }, child: MultiBlocListener( listeners: [ + _BlocListenerT( + selector: (state) => state.hasMissingVideoPreview, + listener: (context, hasMissingVideoPreview) { + if (hasMissingVideoPreview) { + if (!context + .read() + .isDontShowVideoPreviewHintValue && + !SessionStorage().hasShownVideoPreviewHint) { + SessionStorage().hasShownVideoPreviewHint = true; + showDialog( + context: context, + builder: (context) => const _VideoPreviewHintDialog(), + ); + } + } + }, + ), _BlocListenerT( selector: (state) => state.error, listener: (context, error) { diff --git a/app/lib/widget/home_photos2.g.dart b/app/lib/widget/home_photos2.g.dart index d0a034bb..862ed28f 100644 --- a/app/lib/widget/home_photos2.g.dart +++ b/app/lib/widget/home_photos2.g.dart @@ -35,6 +35,7 @@ abstract class $_StateCopyWithWorker { List<_MinimapItem>? minimapItems, double? minimapYRatio, Date? scrollDate, + bool? hasMissingVideoPreview, ExceptionEvent? error}); } @@ -64,6 +65,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic minimapItems = copyWithNull, dynamic minimapYRatio, dynamic scrollDate = copyWithNull, + dynamic hasMissingVideoPreview, dynamic error = copyWithNull}) { return _State( files: files as List? ?? that.files, @@ -102,6 +104,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { minimapYRatio: minimapYRatio as double? ?? that.minimapYRatio, scrollDate: scrollDate == copyWithNull ? that.scrollDate : scrollDate as Date?, + hasMissingVideoPreview: + hasMissingVideoPreview as bool? ?? that.hasMissingVideoPreview, error: error == copyWithNull ? that.error : error as ExceptionEvent?); } @@ -180,7 +184,7 @@ extension _$_ContentListBodyNpLog on _ContentListBody { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, filesSummary: $filesSummary, visibleDates: {length: ${visibleDates.length}}, queriedDates: {length: ${queriedDates.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, syncProgress: $syncProgress, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, viewWidth: ${viewWidth == null ? null : "${viewWidth!.toStringAsFixed(3)}"}, viewHeight: ${viewHeight == null ? null : "${viewHeight!.toStringAsFixed(3)}"}, itemPerRow: $itemPerRow, itemSize: ${itemSize == null ? null : "${itemSize!.toStringAsFixed(3)}"}, isScrolling: $isScrolling, minimapItems: ${minimapItems == null ? null : "[length: ${minimapItems!.length}]"}, minimapYRatio: ${minimapYRatio.toStringAsFixed(3)}, scrollDate: $scrollDate, error: $error}"; + return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, filesSummary: $filesSummary, visibleDates: {length: ${visibleDates.length}}, queriedDates: {length: ${queriedDates.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, syncProgress: $syncProgress, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, viewWidth: ${viewWidth == null ? null : "${viewWidth!.toStringAsFixed(3)}"}, viewHeight: ${viewHeight == null ? null : "${viewHeight!.toStringAsFixed(3)}"}, itemPerRow: $itemPerRow, itemSize: ${itemSize == null ? null : "${itemSize!.toStringAsFixed(3)}"}, isScrolling: $isScrolling, minimapItems: ${minimapItems == null ? null : "[length: ${minimapItems!.length}]"}, minimapYRatio: ${minimapYRatio.toStringAsFixed(3)}, scrollDate: $scrollDate, hasMissingVideoPreview: $hasMissingVideoPreview, error: $error}"; } } @@ -360,6 +364,13 @@ extension _$_UpdateMemoriesToString on _UpdateMemories { } } +extension _$_TripMissingVideoPreviewToString on _TripMissingVideoPreview { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_TripMissingVideoPreview {}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/photo_list_item.dart b/app/lib/widget/photo_list_item.dart index ff34e937..d9df1796 100644 --- a/app/lib/widget/photo_list_item.dart +++ b/app/lib/widget/photo_list_item.dart @@ -278,6 +278,7 @@ class PhotoListVideo extends StatelessWidget { required this.account, required this.previewUrl, this.isFavorite = false, + this.onError, }); @override @@ -293,24 +294,29 @@ class PhotoListVideo extends StatelessWidget { child: NetworkRectThumbnail( account: account, imageUrl: previewUrl, - errorBuilder: (_) => Padding( - padding: const EdgeInsets.all(12), - child: Icon( - Icons.image_not_supported, - color: Theme.of(context).listPlaceholderForegroundColor, - ), - ), + errorBuilder: (context) { + onError?.call(); + return Padding( + padding: const EdgeInsets.all(12), + child: Icon( + Icons.image_not_supported, + color: Theme.of(context).listPlaceholderForegroundColor, + ), + ); + }, ), ), - Container( - alignment: AlignmentDirectional.topEnd, - padding: const EdgeInsets.all(6), + Positioned.directional( + textDirection: Directionality.of(context), + top: 6, + end: 6, child: const Icon(Icons.play_circle_outlined, size: 17), ), if (isFavorite) - Container( - alignment: AlignmentDirectional.bottomStart, - padding: const EdgeInsets.all(6), + Positioned.directional( + textDirection: Directionality.of(context), + bottom: 6, + start: 6, child: const Icon(Icons.star, size: 15), ), ], @@ -322,6 +328,7 @@ class PhotoListVideo extends StatelessWidget { final Account account; final String previewUrl; final bool isFavorite; + final VoidCallback? onError; } class PhotoListLabel extends StatelessWidget {