From 35a98c41ff43f28556a1424ae84c02919b61ab18 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 31 Aug 2023 10:01:47 +0530 Subject: [PATCH 01/15] Enable french language --- lib/l10n/l10n.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 0b2e15a2d..d4e31a2ec 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -13,6 +13,7 @@ const List appSupportedLocales = [ Locale('en'), Locale('es'), Locale('de'), + Locale('fr'), Locale('it'), Locale("nl"), Locale("zh", "CN"), From 78bf13ba1a47198463fd2c5b4d0278787b2c2cb8 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:51:32 +0530 Subject: [PATCH 02/15] Refactor download util Signed-off-by: Neeraj Gupta <254676+ua741@users.noreply.github.com> --- lib/utils/file_download_util.dart | 76 +++++++++++++++++-------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/lib/utils/file_download_util.dart b/lib/utils/file_download_util.dart index 0913a7acd..14410e474 100644 --- a/lib/utils/file_download_util.dart +++ b/lib/utils/file_download_util.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'dart:typed_data'; import "package:computer/computer.dart"; import 'package:dio/dio.dart'; +import "package:flutter/foundation.dart"; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/network/network.dart'; @@ -16,45 +16,48 @@ final _logger = Logger("file_download_util"); Future downloadAndDecrypt( EnteFile file, { ProgressCallback? progressCallback, -}) { +}) async { final String logPrefix = 'File-${file.uploadedFileID}:'; - _logger.info('$logPrefix starting download'); - final encryptedFilePath = Configuration.instance.getTempDirectory() + - file.generatedID.toString() + - ".encrypted"; + _logger + .info('$logPrefix starting download ${formatBytes(file.fileSize ?? 0)}'); + + final String tempDir = Configuration.instance.getTempDirectory(); + final String encryptedFilePath = "$tempDir${file.generatedID}.encrypted"; final encryptedFile = File(encryptedFilePath); + final startTime = DateTime.now().millisecondsSinceEpoch; - return NetworkClient.instance - .getDio() - .download( - file.downloadUrl, - encryptedFilePath, - options: Options( - headers: {"X-Auth-Token": Configuration.instance.getToken()}, - ), - onReceiveProgress: progressCallback, - ) - .then((response) async { - if (response.statusCode != 200) { - _logger.warning('$logPrefix download failed ${response.toString()}'); - return null; - } else if (!encryptedFile.existsSync()) { - _logger.warning('$logPrefix incomplete download, file not found'); + + try { + final response = await NetworkClient.instance.getDio().download( + file.downloadUrl, + encryptedFilePath, + options: Options( + headers: {"X-Auth-Token": Configuration.instance.getToken()}, + ), + onReceiveProgress: (a, b) { + if (kDebugMode) { + _logger.fine( + "$logPrefix download progress: ${formatBytes(a)} / ${formatBytes(b)}", + ); + } + progressCallback?.call(a, b); + }, + ); + if (response.statusCode != 200 || !encryptedFile.existsSync()) { + _logger.warning('$logPrefix download failed ${response.toString()}'); return null; } - final int sizeInBytes = ((file.fileSize ?? 0) > 0) - ? file.fileSize! - : await encryptedFile.length(); - final double speedInKBps = sizeInBytes / - 1024.0 / - ((DateTime.now().millisecondsSinceEpoch - startTime) / 1000); + + final int sizeInBytes = file.fileSize ?? await encryptedFile.length(); + final double elapsedSeconds = + (DateTime.now().millisecondsSinceEpoch - startTime) / 1000; + final double speedInKBps = sizeInBytes / 1024.0 / elapsedSeconds; + _logger.info( - "$logPrefix download completed: ${formatBytes(sizeInBytes)}, avg speed: ${speedInKBps.toStringAsFixed(2)} KB/s", + '$logPrefix download completed: ${formatBytes(sizeInBytes)}, avg speed: ${speedInKBps.toStringAsFixed(2)} KB/s', ); - final decryptedFilePath = Configuration.instance.getTempDirectory() + - file.generatedID.toString() + - ".decrypted"; + final String decryptedFilePath = "$tempDir${file.generatedID}.decrypted"; try { await CryptoUtil.decryptFile( encryptedFilePath, @@ -62,14 +65,17 @@ Future downloadAndDecrypt( CryptoUtil.base642bin(file.fileDecryptionHeader!), getFileKey(file), ); + _logger.info('$logPrefix decryption completed'); } catch (e, s) { - _logger.severe("failed to decrypt file", e, s); + _logger.severe("Critical: $logPrefix failed to decrypt", e, s); return null; } - _logger.info('$logPrefix decryption completed'); await encryptedFile.delete(); return File(decryptedFilePath); - }); + } catch (e, s) { + _logger.severe("$logPrefix failed to download or decrypt", e, s); + return null; + } } Uint8List getFileKey(EnteFile file) { From c319e8cd5539184358384b28f80b4827d7fef875 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:53:57 +0530 Subject: [PATCH 03/15] Fix OOM: Use fileStream while caching Signed-off-by: Neeraj Gupta <254676+ua741@users.noreply.github.com> --- lib/utils/file_util.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils/file_util.dart b/lib/utils/file_util.dart index c9a7548fb..6b1bb1de5 100644 --- a/lib/utils/file_util.dart +++ b/lib/utils/file_util.dart @@ -237,9 +237,9 @@ Future<_LivePhoto?> _downloadLivePhoto( final videoFile = File(decodePath); await videoFile.create(recursive: true); await videoFile.writeAsBytes(data); - videoFileCache = await VideoCacheManager.instance.putFile( + videoFileCache = await VideoCacheManager.instance.putFileStream( file.downloadUrl, - await videoFile.readAsBytes(), + videoFile.openRead(), eTag: file.downloadUrl, maxAge: const Duration(days: 365), fileExtension: fileExtension, @@ -287,9 +287,9 @@ Future _downloadAndCache( } await decryptedFile.delete(); } - final cachedFile = await cacheManager.putFile( + final cachedFile = await cacheManager.putFileStream( file.downloadUrl, - await outputFile.readAsBytes(), + outputFile.openRead(), eTag: file.downloadUrl, maxAge: const Duration(days: 365), fileExtension: fileExtension, From df79553bbab48ff4a251c64895837a1a7fe68e11 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 31 Aug 2023 18:06:51 +0530 Subject: [PATCH 04/15] Fix: dispose progress notifier --- lib/ui/viewer/file/video_widget.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ui/viewer/file/video_widget.dart b/lib/ui/viewer/file/video_widget.dart index a273ba8ed..d8fbe7d21 100644 --- a/lib/ui/viewer/file/video_widget.dart +++ b/lib/ui/viewer/file/video_widget.dart @@ -112,6 +112,7 @@ class _VideoWidgetState extends State { void dispose() { _videoPlayerController?.dispose(); _chewieController?.dispose(); + _progressNotifier.dispose(); if (_wakeLockEnabledHere) { unawaited( WakelockPlus.enabled.then((isEnabled) { From c0d9d66410fca6693ee589340912bf53dbc44c8a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:42:35 +0530 Subject: [PATCH 05/15] Fix progress callback --- lib/ui/viewer/file/video_widget.dart | 11 +++++-- lib/utils/file_util.dart | 43 +++++++++++++++++++--------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/ui/viewer/file/video_widget.dart b/lib/ui/viewer/file/video_widget.dart index d8fbe7d21..45dc9d3a6 100644 --- a/lib/ui/viewer/file/video_widget.dart +++ b/lib/ui/viewer/file/video_widget.dart @@ -92,6 +92,9 @@ class _VideoWidgetState extends State { getFileFromServer( widget.file, progressCallback: (count, total) { + if(!mounted) { + return; + } _progressNotifier.value = count / (widget.file.fileSize ?? total); if (_progressNotifier.value == 1) { if (mounted) { @@ -100,11 +103,15 @@ class _VideoWidgetState extends State { } }, ).then((file) { - if (file != null) { + if (file != null && mounted) { _setVideoPlayerController(file: file); } }).onError((error, stackTrace) { - showErrorDialog(context, "Error", S.of(context).failedToDownloadVideo); + if(mounted) { + showErrorDialog(context, "Error", S + .of(context) + .failedToDownloadVideo); + } }); } diff --git a/lib/utils/file_util.dart b/lib/utils/file_util.dart index 6b1bb1de5..a0e3fee24 100644 --- a/lib/utils/file_util.dart +++ b/lib/utils/file_util.dart @@ -110,12 +110,13 @@ void preloadThumbnail(EnteFile file) { final Map> fileDownloadsInProgress = >{}; +Map progressCallbacks = {}; Future getFileFromServer( - EnteFile file, { - ProgressCallback? progressCallback, - bool liveVideo = false, // only needed in case of live photos -}) async { + EnteFile file, { + ProgressCallback? progressCallback, + bool liveVideo = false, // only needed in case of live photos + }) async { final cacheManager = (file.fileType == FileType.video || liveVideo) ? VideoCacheManager.instance : DefaultCacheManager(); @@ -124,27 +125,41 @@ Future getFileFromServer( return fileFromCache.file; } final downloadID = file.uploadedFileID.toString() + liveVideo.toString(); + + if (progressCallback != null) { + progressCallbacks[downloadID] = progressCallback; + } + if (!fileDownloadsInProgress.containsKey(downloadID)) { + final completer = Completer(); + fileDownloadsInProgress[downloadID] = completer.future; + + Future downloadFuture; if (file.fileType == FileType.livePhoto) { - fileDownloadsInProgress[downloadID] = _getLivePhotoFromServer( + downloadFuture = _getLivePhotoFromServer( file, - progressCallback: progressCallback, + progressCallback: (count, total) { + progressCallbacks[downloadID]?.call(count, total); + }, needLiveVideo: liveVideo, ); - fileDownloadsInProgress[downloadID]!.whenComplete(() { - fileDownloadsInProgress.remove(downloadID); - }); } else { - fileDownloadsInProgress[downloadID] = _downloadAndCache( + downloadFuture = _downloadAndCache( file, cacheManager, - progressCallback: progressCallback, + progressCallback: (count, total) { + progressCallbacks[downloadID]?.call(count, total); + }, ); - fileDownloadsInProgress[downloadID]!.whenComplete(() { - fileDownloadsInProgress.remove(downloadID); - }); } + + downloadFuture.then((downloadedFile) { + completer.complete(downloadedFile); + fileDownloadsInProgress.remove(downloadID); + progressCallbacks.remove(downloadID); + }); } + return fileDownloadsInProgress[downloadID]; } From 5d16a39383d1e050e7e7890eefb000055e35a4e9 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:23:40 +0530 Subject: [PATCH 06/15] Remove unused imports Signed-off-by: Neeraj Gupta <254676+ua741@users.noreply.github.com> --- lib/ui/home/preserve_footer_widget.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/ui/home/preserve_footer_widget.dart b/lib/ui/home/preserve_footer_widget.dart index c2367a2d2..a9a3019c8 100644 --- a/lib/ui/home/preserve_footer_widget.dart +++ b/lib/ui/home/preserve_footer_widget.dart @@ -2,10 +2,8 @@ import 'dart:async'; import "dart:io"; import 'package:flutter/material.dart'; -import "package:flutter/services.dart"; import "package:logging/logging.dart"; import 'package:photo_manager/photo_manager.dart'; -import "package:photos/core/configuration.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/services/local_sync_service.dart'; import 'package:photos/ui/common/gradient_button.dart'; From c5f31929720570e603533a6e03abb70c9ec4b6d5 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:28:42 +0530 Subject: [PATCH 07/15] Remove callback --- lib/ui/viewer/file/video_widget.dart | 5 +-- lib/utils/file_util.dart | 47 ++++++++++++++++++---------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/ui/viewer/file/video_widget.dart b/lib/ui/viewer/file/video_widget.dart index 45dc9d3a6..806e4dc63 100644 --- a/lib/ui/viewer/file/video_widget.dart +++ b/lib/ui/viewer/file/video_widget.dart @@ -109,17 +109,18 @@ class _VideoWidgetState extends State { }).onError((error, stackTrace) { if(mounted) { showErrorDialog(context, "Error", S - .of(context) - .failedToDownloadVideo); + .of(context).failedToDownloadVideo,); } }); } @override void dispose() { + removeCallBack(widget.file); _videoPlayerController?.dispose(); _chewieController?.dispose(); _progressNotifier.dispose(); + if (_wakeLockEnabledHere) { unawaited( WakelockPlus.enabled.then((isEnabled) { diff --git a/lib/utils/file_util.dart b/lib/utils/file_util.dart index a0e3fee24..123a4327d 100644 --- a/lib/utils/file_util.dart +++ b/lib/utils/file_util.dart @@ -14,6 +14,7 @@ import 'package:photos/core/cache/thumbnail_in_memory_cache.dart'; import 'package:photos/core/cache/video_cache_manager.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; +import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/utils/file_download_util.dart'; @@ -108,15 +109,27 @@ void preloadThumbnail(EnteFile file) { } } -final Map> fileDownloadsInProgress = +final Map> _fileDownloadsInProgress = >{}; -Map progressCallbacks = {}; +Map _progressCallbacks = {}; + +void removeCallBack(EnteFile file) { + if (!file.isUploaded) { + return; + } + String id = file.uploadedFileID.toString() + false.toString(); + _progressCallbacks.remove(id); + if (file.isLivePhoto) { + id = file.uploadedFileID.toString() + true.toString(); + _progressCallbacks.remove(id); + } +} Future getFileFromServer( - EnteFile file, { - ProgressCallback? progressCallback, - bool liveVideo = false, // only needed in case of live photos - }) async { + EnteFile file, { + ProgressCallback? progressCallback, + bool liveVideo = false, // only needed in case of live photos +}) async { final cacheManager = (file.fileType == FileType.video || liveVideo) ? VideoCacheManager.instance : DefaultCacheManager(); @@ -127,19 +140,19 @@ Future getFileFromServer( final downloadID = file.uploadedFileID.toString() + liveVideo.toString(); if (progressCallback != null) { - progressCallbacks[downloadID] = progressCallback; + _progressCallbacks[downloadID] = progressCallback; } - if (!fileDownloadsInProgress.containsKey(downloadID)) { + if (!_fileDownloadsInProgress.containsKey(downloadID)) { final completer = Completer(); - fileDownloadsInProgress[downloadID] = completer.future; + _fileDownloadsInProgress[downloadID] = completer.future; Future downloadFuture; if (file.fileType == FileType.livePhoto) { downloadFuture = _getLivePhotoFromServer( file, progressCallback: (count, total) { - progressCallbacks[downloadID]?.call(count, total); + _progressCallbacks[downloadID]?.call(count, total); }, needLiveVideo: liveVideo, ); @@ -148,21 +161,21 @@ Future getFileFromServer( file, cacheManager, progressCallback: (count, total) { - progressCallbacks[downloadID]?.call(count, total); + _progressCallbacks[downloadID]?.call(count, total); }, ); } - downloadFuture.then((downloadedFile) { completer.complete(downloadedFile); - fileDownloadsInProgress.remove(downloadID); - progressCallbacks.remove(downloadID); + _fileDownloadsInProgress.remove(downloadID); + _progressCallbacks.remove(downloadID); }); } - - return fileDownloadsInProgress[downloadID]; + return _fileDownloadsInProgress[downloadID]; } + + Future isFileCached(EnteFile file, {bool liveVideo = false}) async { final cacheManager = (file.fileType == FileType.video || liveVideo) ? VideoCacheManager.instance @@ -278,7 +291,7 @@ Future<_LivePhoto?> _downloadLivePhoto( Future _downloadAndCache( EnteFile file, BaseCacheManager cacheManager, { - ProgressCallback? progressCallback, + required ProgressCallback progressCallback, }) async { return downloadAndDecrypt(file, progressCallback: progressCallback) .then((decryptedFile) async { From 521f534cf68d95c4fe670de764e4f0a5d67d665b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:31:11 +0530 Subject: [PATCH 08/15] Mark download dialog dismissible Signed-off-by: Neeraj Gupta <254676+ua741@users.noreply.github.com> --- lib/ui/viewer/file/file_app_bar.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ui/viewer/file/file_app_bar.dart b/lib/ui/viewer/file/file_app_bar.dart index ed8bd5dd2..ac666198b 100644 --- a/lib/ui/viewer/file/file_app_bar.dart +++ b/lib/ui/viewer/file/file_app_bar.dart @@ -11,6 +11,7 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; @@ -307,7 +308,11 @@ class FileAppBarState extends State { } Future _download(EnteFile file) async { - final dialog = createProgressDialog(context, "Downloading..."); + final dialog = createProgressDialog( + context, + context.l10n.downloading, + isDismissible: true, + ); await dialog.show(); try { final FileType type = file.fileType; From ff9fe8f3dd732606bf3fb5f38420bbcdb842f5b9 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:54:54 +0530 Subject: [PATCH 09/15] Add util for fake progress --- lib/utils/fake_progress.dart | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 lib/utils/fake_progress.dart diff --git a/lib/utils/fake_progress.dart b/lib/utils/fake_progress.dart new file mode 100644 index 000000000..678aef76b --- /dev/null +++ b/lib/utils/fake_progress.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import "package:flutter/foundation.dart"; + +typedef FakeProgressCallback = void Function(int count); + +class FakePeriodicProgress { + final FakeProgressCallback? callback; + final Duration duration; + late Timer _timer; + bool _shouldRun = true; + int runCount = 0; + + FakePeriodicProgress({ + required this.callback, + required this.duration, + }); + + void start() { + assert(_shouldRun, "Cannot start a stopped FakePeriodicProgress"); + Future.delayed(duration, _invokePeriodically); + } + + void stop() { + if (_shouldRun) { + _shouldRun = false; + _timer.cancel(); + } + } + + void _invokePeriodically() { + if (_shouldRun) { + try { + runCount++; + callback + ?.call(runCount); // replace 0,0 with actual progress if available + } catch (e) { + debugPrint("Error in FakePeriodicProgress callback: $e"); + stop(); + } + _timer = Timer(duration, _invokePeriodically); + } + } +} From 65bd6c9a18774d7a8ef8e02ebf3b14520774b748 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:12:52 +0530 Subject: [PATCH 10/15] Fire fake progress event every 5sec during decryption --- lib/utils/file_download_util.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/utils/file_download_util.dart b/lib/utils/file_download_util.dart index 14410e474..527018f67 100644 --- a/lib/utils/file_download_util.dart +++ b/lib/utils/file_download_util.dart @@ -7,9 +7,11 @@ import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/network/network.dart'; import 'package:photos/models/file/file.dart'; +import "package:photos/models/file/file_type.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/data_util.dart"; +import "package:photos/utils/fake_progress.dart"; final _logger = Logger("file_download_util"); @@ -58,15 +60,29 @@ Future downloadAndDecrypt( ); final String decryptedFilePath = "$tempDir${file.generatedID}.decrypted"; + // As decryption can take time, emit fake progress for large files during + // decryption + final FakePeriodicProgress? fakeProgress = file.fileType == FileType.video + ? FakePeriodicProgress( + callback: (count) { + progressCallback?.call(sizeInBytes, sizeInBytes); + }, + duration: const Duration(milliseconds: 5000), + ) + : null; try { + // Start the periodic callback after initial 5 seconds + fakeProgress?.start(); await CryptoUtil.decryptFile( encryptedFilePath, decryptedFilePath, CryptoUtil.base642bin(file.fileDecryptionHeader!), getFileKey(file), ); + fakeProgress?.stop(); _logger.info('$logPrefix decryption completed'); } catch (e, s) { + fakeProgress?.stop(); _logger.severe("Critical: $logPrefix failed to decrypt", e, s); return null; } From 58c671e91565a4dea7612402275fe7b19a90560e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:37:42 +0530 Subject: [PATCH 11/15] Fix bug in fake timer --- lib/utils/fake_progress.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/fake_progress.dart b/lib/utils/fake_progress.dart index 678aef76b..adcc3ee58 100644 --- a/lib/utils/fake_progress.dart +++ b/lib/utils/fake_progress.dart @@ -32,11 +32,11 @@ class FakePeriodicProgress { if (_shouldRun) { try { runCount++; - callback - ?.call(runCount); // replace 0,0 with actual progress if available + callback?.call(runCount); } catch (e) { debugPrint("Error in FakePeriodicProgress callback: $e"); stop(); + return; } _timer = Timer(duration, _invokePeriodically); } From 7545feb48b4044c60f19f8a5f4f413984ad2e0bb Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:00:23 +0530 Subject: [PATCH 12/15] [l10n]Search: use month name based on locale --- lib/data/months.dart | 31 ++++++++++++++----------- lib/services/search_service.dart | 28 +++++++++++++++------- lib/ui/viewer/search/search_widget.dart | 16 +++++++++---- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/lib/data/months.dart b/lib/data/months.dart index 530a869c1..4000b9a30 100644 --- a/lib/data/months.dart +++ b/lib/data/months.dart @@ -1,17 +1,20 @@ -List allMonths = [ - MonthData('January', 1), - MonthData('February', 2), - MonthData('March', 3), - MonthData('April', 4), - MonthData('May', 5), - MonthData('June', 6), - MonthData('July', 7), - MonthData('August', 8), - MonthData('September', 9), - MonthData('October', 10), - MonthData('November', 11), - MonthData('December', 12), -]; +import "package:flutter/widgets.dart"; +import "package:intl/intl.dart"; + +final Map> _cache = {}; + +List getMonthData(BuildContext context) { + final locale = Localizations.localeOf(context).toString(); + + if (!_cache.containsKey(locale)) { + final dateSymbols = DateFormat('MMMM', locale).dateSymbols; + _cache[locale] = List.generate( + 12, + (index) => MonthData(dateSymbols.MONTHS[index], index + 1), + ); + } + return _cache[locale]!; +} class MonthData { final String name; diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 4331eca05..4da6935be 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -1,3 +1,4 @@ +import "package:flutter/cupertino.dart"; import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/data/holidays.dart'; @@ -93,7 +94,8 @@ class SearchService { final List searchResults = []; for (var yearData in YearsData.instance.yearsData) { if (yearData.year.startsWith(yearFromQuery)) { - final List filesInYear = await _getFilesInYear(yearData.duration); + final List filesInYear = + await _getFilesInYear(yearData.duration); if (filesInYear.isNotEmpty) { searchResults.add( GenericSearchResult( @@ -314,9 +316,12 @@ class SearchService { return searchResults; } - Future> getMonthSearchResults(String query) async { + Future> getMonthSearchResults( + BuildContext context, + String query, + ) async { final List searchResults = []; - for (var month in _getMatchingMonths(query)) { + for (var month in _getMatchingMonths(context, query)) { final matchedFiles = await FilesDB.instance.getFilesCreatedWithinDurations( _getDurationsOfMonthInEveryYear(month.monthNumber), @@ -337,10 +342,11 @@ class SearchService { } Future> getDateResults( + BuildContext context, String query, ) async { final List searchResults = []; - final potentialDates = _getPossibleEventDate(query); + final potentialDates = _getPossibleEventDate(context, query); for (var potentialDate in potentialDates) { final int day = potentialDate.item1; @@ -365,8 +371,8 @@ class SearchService { return searchResults; } - List _getMatchingMonths(String query) { - return allMonths + List _getMatchingMonths(BuildContext context, String query) { + return getMonthData(context) .where( (monthData) => monthData.name.toLowerCase().startsWith(query.toLowerCase()), @@ -414,7 +420,10 @@ class SearchService { return durationsOfMonthInEveryYear; } - List> _getPossibleEventDate(String query) { + List> _getPossibleEventDate( + BuildContext context, + String query, + ) { final List> possibleEvents = []; if (query.trim().isEmpty) { return possibleEvents; @@ -434,8 +443,9 @@ class SearchService { if (day == null || day < 1 || day > 31) { return possibleEvents; } - final List potentialMonth = - resultCount > 1 ? _getMatchingMonths(result[1]) : allMonths; + final List potentialMonth = resultCount > 1 + ? _getMatchingMonths(context, result[1]) + : getMonthData(context); final int? parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null; final List matchingYears = []; if (parsedYear != null) { diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index a704bd6a1..f798aa1c6 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -141,7 +141,7 @@ class _SearchWidgetState extends State { onChanged: (value) async { _query = value; final List allResults = - await getSearchResultsForQuery(value); + await getSearchResultsForQuery(context, value); /*checking if _query == value to make sure that the results are from the current query and not from the previous query (race condition).*/ if (mounted && _query == value) { @@ -174,12 +174,15 @@ class _SearchWidgetState extends State { super.dispose(); } - Future> getSearchResultsForQuery(String query) async { + Future> getSearchResultsForQuery( + BuildContext context, + String query, + ) async { final Completer> completer = Completer(); _debouncer.run( () { - return _getSearchResultsFromService(query, completer); + return _getSearchResultsFromService(context, query, completer); }, ); @@ -187,6 +190,7 @@ class _SearchWidgetState extends State { } Future _getSearchResultsFromService( + BuildContext context, String query, Completer completer, ) async { @@ -224,10 +228,12 @@ class _SearchWidgetState extends State { await _searchService.getCollectionSearchResults(query); allResults.addAll(collectionResults); - final monthResults = await _searchService.getMonthSearchResults(query); + final monthResults = + await _searchService.getMonthSearchResults(context, query); allResults.addAll(monthResults); - final possibleEvents = await _searchService.getDateResults(query); + final possibleEvents = + await _searchService.getDateResults(context, query); allResults.addAll(possibleEvents); } catch (e, s) { _logger.severe("error during search", e, s); From c722fae674cdfa6985e1c59e10cf0b78f4e4ba69 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:16:51 +0530 Subject: [PATCH 13/15] Fix date check Signed-off-by: Neeraj Gupta <254676+ua741@users.noreply.github.com> --- lib/services/search_service.dart | 2 +- lib/utils/date_time_util.dart | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 4da6935be..7b4235fbe 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -397,7 +397,7 @@ class SearchService { final int startYear = year ?? searchStartYear; final int endYear = year ?? currentYear; for (var yr = startYear; yr <= endYear; yr++) { - if (isValidDate(day: day, month: month, year: yr)) { + if (isValidGregorianDate(day: day, month: month, year: yr)) { durationsOfHolidayInEveryYear.add([ DateTime(yr, month, day).microsecondsSinceEpoch, DateTime(yr, month, day + 1).microsecondsSinceEpoch, diff --git a/lib/utils/date_time_util.dart b/lib/utils/date_time_util.dart index ce510398b..942896859 100644 --- a/lib/utils/date_time_util.dart +++ b/lib/utils/date_time_util.dart @@ -2,8 +2,6 @@ import "package:flutter/cupertino.dart"; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; -const Set monthWith31Days = {1, 3, 5, 7, 8, 10, 12}; -const Set monthWith30Days = {4, 6, 9, 11}; Map _months = { 1: "Jan", 2: "Feb", @@ -123,22 +121,22 @@ String secondsToHHMMSS(int value) { return result; } -bool isValidDate({ +bool isValidGregorianDate({ required int day, required int month, required int year, }) { - if (day < 0 || day > 31 || month < 0 || month > 12 || year < 0) { + if (day <= 0 || day > 31 || month <= 0 || month > 12 || year < 0) { return false; } - if (monthWith30Days.contains(month) && day > 30) { + if ((month == 4 || month == 6 || month == 9 || month == 11) && day > 30) { return false; } if (month == 2) { if (day > 29) { return false; } - if (day == 29 && year % 4 != 0) { + if (day == 29 && (year % 4 != 0 || (year % 100 == 0 && year % 400 != 0))) { return false; } } From fa7c486d7c2c0129df9f67aa8be7e267fca84cf3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:47:43 +0530 Subject: [PATCH 14/15] Localize holidays & update list --- lib/data/holidays.dart | 112 +++++++++++++++++++++++- lib/services/search_service.dart | 7 +- lib/ui/viewer/search/search_widget.dart | 2 +- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/lib/data/holidays.dart b/lib/data/holidays.dart index c8ed8f8b0..4601cd92c 100644 --- a/lib/data/holidays.dart +++ b/lib/data/holidays.dart @@ -1,12 +1,26 @@ +import "package:flutter/cupertino.dart"; + class HolidayData { final String name; final int month; final int day; - const HolidayData(this.name, {required this.month, required this.day}); + HolidayData(this.name, {required this.month, required this.day}); +} + +// Based on the locale, this return holidays that have fixed date as per the +// Gregorian calendar. For example, Christmas is always on December 25th. +List getHolidays(BuildContext context) { + final locale = Localizations.localeOf(context); + if (localeToHolidays.containsKey(locale.toLanguageTag())) { + return localeToHolidays[locale.toLanguageTag()]!; + } else if (localeToHolidays.containsKey(locale.languageCode)) { + return localeToHolidays[locale.languageCode]!; + } + return _defaultHolidays; } -const List allHolidays = [ +List _defaultHolidays = [ HolidayData('New Year', month: 1, day: 1), HolidayData('Epiphany', month: 1, day: 6), HolidayData('Pongal', month: 1, day: 14), @@ -18,9 +32,101 @@ const List allHolidays = [ HolidayData('May Day', month: 5, day: 1), HolidayData('Midsummer\'s Eve', month: 6, day: 24), HolidayData('Midsummer Day', month: 6, day: 25), - HolidayData('Christmas Eve', month: 12, day: 24), HolidayData('Halloween', month: 10, day: 31), + HolidayData('Christmas Eve', month: 12, day: 24), HolidayData('Christmas', month: 12, day: 25), HolidayData('Boxing Day', month: 12, day: 26), HolidayData('New Year\'s Eve', month: 12, day: 31), ]; +Map> localeToHolidays = { + 'it': [ + HolidayData('Capodanno', month: 1, day: 1), + // New Year's Day + HolidayData('Epifania', month: 1, day: 6), + // Epiphany + HolidayData('San Valentino', month: 2, day: 14), + // Valentine's Day + HolidayData('Festa della Liberazione', month: 4, day: 25), + // Liberation Day + HolidayData('Primo Maggio', month: 5, day: 1), + // Labor Day + HolidayData('Festa della Repubblica', month: 6, day: 2), + // Republic Day + HolidayData('Ferragosto', month: 8, day: 15), + // Assumption of Mary + HolidayData('Halloween', month: 10, day: 31), + // Halloween + HolidayData('Ognissanti', month: 11, day: 1), + // All Saints' Day + HolidayData('Immacolata Concezione', month: 12, day: 8), + // Immaculate Conception + HolidayData('Natale', month: 12, day: 25), + // Christmas Day + HolidayData('Vigilia di Capodanno', month: 12, day: 31), + // New Year's Eve + ], + 'fr': [ + HolidayData('Jour de l\'An', month: 1, day: 1), // New Year's Day + HolidayData('Fête du Travail', month: 5, day: 1), // Labour Day + HolidayData('Fête Nationale', month: 7, day: 14), // Bastille Day + HolidayData('Assomption', month: 8, day: 15), // Assumption of Mary + HolidayData('Halloween', month: 10, day: 31), // Halloween + HolidayData('Toussaint', month: 11, day: 1), // All Saints' Day + HolidayData('Jour de l\'Armistice', month: 11, day: 11), // Armistice Day + HolidayData('Noël', month: 12, day: 25), // Christmas + HolidayData('Lendemain de Noël', month: 12, day: 26), // Boxing Day + HolidayData('Saint-Sylvestre', month: 12, day: 31), // New Year's Eve + ], + 'de': [ + HolidayData('Neujahrstag', month: 1, day: 1), + // New Year + HolidayData('Valentinstag', month: 2, day: 14), + // Valentine's Day + HolidayData('Tag der Arbeit', month: 5, day: 1), + // Labor Day + HolidayData('Tag der Deutschen Einheit', month: 10, day: 3), + // German Unity Day + HolidayData('Halloween', month: 10, day: 31), + // Halloween + HolidayData('Erster Weihnachtstag', month: 12, day: 25), + // First Christmas Day + HolidayData('Zweiter Weihnachtstag', month: 12, day: 26), + // Second Christmas Day (Boxing Day) + HolidayData('Silvester', month: 12, day: 31), + // New Year's Eve + ], + 'nl': [ + HolidayData('Nieuwjaarsdag', month: 1, day: 1), // New Year's Day + HolidayData('Valentijnsdag', month: 2, day: 14), // Valentine's Day + HolidayData('Koningsdag', month: 4, day: 27), // King's Day + HolidayData('Bevrijdingsdag', month: 5, day: 5), // Liberation Day + HolidayData('Hemelvaartsdag', month: 5, day: 26), // Ascension Day + HolidayData('Halloween', month: 10, day: 31), // Halloween + HolidayData('Sinterklaas', month: 12, day: 5), // Sinterklaas + HolidayData('Eerste Kerstdag', month: 12, day: 25), // First Christmas Day + HolidayData('Tweede Kerstdag', month: 12, day: 26), // Second Christmas Day + HolidayData('Oudejaarsdag', month: 12, day: 31), // New Year's Eve + ], + 'es': [ + HolidayData('Año Nuevo', month: 1, day: 1), + // New Year's Day + HolidayData('San Valentín', month: 2, day: 14), + // Valentine's Day + HolidayData('Día del Trabajador', month: 5, day: 1), + // Labor Day + HolidayData('Día de la Hispanidad', month: 10, day: 12), + // Hispanic Day + HolidayData('Halloween', month: 10, day: 31), + // Halloween + HolidayData('Día de Todos los Santos', month: 11, day: 1), + // All Saints' Day + HolidayData('Día de la Constitución', month: 12, day: 6), + // Constitution Day + HolidayData('La Inmaculada Concepción', month: 12, day: 8), + // Immaculate Conception + HolidayData('Navidad', month: 12, day: 25), + // Christmas Day + HolidayData('Nochevieja', month: 12, day: 31), + // New Year's Eve + ], +}; diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 7b4235fbe..30a71cbf9 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -111,11 +111,16 @@ class SearchService { } Future> getHolidaySearchResults( + BuildContext context, String query, ) async { final List searchResults = []; + if (query.isEmpty) { + return searchResults; + } + final holidays = getHolidays(context); - for (var holiday in allHolidays) { + for (var holiday in holidays) { if (holiday.name.toLowerCase().contains(query.toLowerCase())) { final matchedFiles = await FilesDB.instance.getFilesCreatedWithinDurations( diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index f798aa1c6..c37e453d2 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -206,7 +206,7 @@ class _SearchWidgetState extends State { } final holidayResults = - await _searchService.getHolidaySearchResults(query); + await _searchService.getHolidaySearchResults(context, query); allResults.addAll(holidayResults); final fileTypeSearchResults = From cb07b6bbc12d6cfa94344798601593374eef5ed8 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Sep 2023 14:01:31 +0530 Subject: [PATCH 15/15] Bump version: 0.7.90+490 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 96d40ea53..9bccb5e3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.89+489 +version: 0.7.90+490 environment: sdk: ">=3.0.0 <4.0.0"