From f0d23fe873821161cccfb1866616e0287cfe69bd Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Mon, 17 Mar 2025 17:38:13 +0530 Subject: [PATCH] [mob] fix(memories): iOS tint, hide when disabled, sync on change, store generatedId --- mobile/ios/Podfile.lock | 2 +- .../ios/SlideshowWidget/SlideshowWidget.swift | 47 +- mobile/lib/services/home_widget_service.dart | 463 +++++++----------- .../lib/services/smart_memories_service.dart | 12 + .../ui/home/memories/full_screen_memory.dart | 12 +- .../ui/settings/gallery_settings_screen.dart | 12 +- 6 files changed, 252 insertions(+), 296 deletions(-) diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index ef64b452cc..7e2eb2fc57 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -453,7 +453,7 @@ SPEC CHECKSUMS: cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1 device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 - ffmpeg-kit-ios-full-gpl: 78f81da9b8c14f62f5013dd90f0c9c80bd720140 + ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf diff --git a/mobile/ios/SlideshowWidget/SlideshowWidget.swift b/mobile/ios/SlideshowWidget/SlideshowWidget.swift index 053bff96d8..6a7a8f5392 100644 --- a/mobile/ios/SlideshowWidget/SlideshowWidget.swift +++ b/mobile/ios/SlideshowWidget/SlideshowWidget.swift @@ -39,8 +39,8 @@ struct Provider: TimelineProvider { if totalSet != nil && totalSet! > 0 { totalSet = totalSet! > 5 ? 5 : totalSet! - for offset in 0 ..< totalSet! { - let randomInt = Int.random(in: 0 ..< totalSet!) + for offset in 0.. some View { + if #available(iOS 18.0, *) { + self.widgetAccentedRenderingMode(isAccentedRenderingMode ? .accented : .fullColor) + } else { + self + } + } + + @ViewBuilder + func backwardWidgetFullColorRenderingMode() -> some View { + backwardWidgetAccentedRenderingMode(false) + } +} diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index 24342ba185..e9a8fd8c07 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -1,23 +1,19 @@ -import "dart:io"; -import "dart:math"; - +import "package:crypto/crypto.dart"; import "package:figma_squircle/figma_squircle.dart"; import "package:flutter/material.dart"; import "package:flutter/scheduler.dart"; import "package:fluttertoast/fluttertoast.dart"; import 'package:home_widget/home_widget.dart' as hw; -import "package:image/image.dart" as img; import "package:logging/logging.dart"; -import "package:path_provider/path_provider.dart"; -import "package:path_provider_foundation/path_provider_foundation.dart"; import "package:photos/core/configuration.dart"; import "package:photos/core/constants.dart"; import "package:photos/db/files_db.dart"; import "package:photos/models/file/file.dart"; -import "package:photos/models/file/file_type.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/favorites_service.dart"; -import "package:photos/utils/file_util.dart"; +import "package:photos/services/memories_service.dart"; +import "package:photos/services/smart_memories_service.dart"; +import "package:photos/ui/viewer/file/detail_page.dart"; +import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/preload_util.dart"; import "package:photos/utils/thumbnail_util.dart"; @@ -36,88 +32,44 @@ class HomeWidgetService { } await hw.HomeWidget.setAppGroupId(iOSGroupID); + final homeWidgetCount = await HomeWidgetService.instance.countHomeWidgets(); if (homeWidgetCount == 0) { _logger.warning("no home widget active"); return; } - final isLoggedIn = Configuration.instance.isLoggedIn(); + final isLoggedIn = Configuration.instance.isLoggedIn(); if (!isLoggedIn) { await clearHomeWidget(); _logger.warning("user not logged in"); return; } - if (syncIt) { - final value = await hw.HomeWidget.getWidgetData("totalSet"); - if (value == null) { - _logger.warning("no home widget active"); - return; - } - - await hw.HomeWidget.updateWidget( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ); + final memoriesEnabled = MemoriesService.instance.showMemories; + if (!memoriesEnabled) { + _logger.warning("memories not enabled"); + await clearHomeWidget(); return; } - await lockAndLoadMemories(); + await _lockAndLoadMemories(); } - Future<(Size, Size)?> _renderFile(EnteFile randomFile, String key) async { - final fullImage = await getFile(randomFile); - if (fullImage == null) { - _logger.warning("Can't fetch file"); - return null; - } - - final image = await decodeImageFromList(await fullImage.readAsBytes()); - final width = image.width.toDouble(); - final height = image.height.toDouble(); - final ogSize = Size(width, height); - - // final size = min(min(width, height), 1024.0); - // final aspectRatio = width / height; - - // late final double cacheWidth; - // late final double cacheHeight; - // if (aspectRatio > 1) { - // cacheWidth = size; - // cacheHeight = (size / aspectRatio); - // } else if (aspectRatio < 1) { - // cacheHeight = size; - // cacheWidth = (size * aspectRatio); - // } else { - // cacheWidth = size; - // cacheHeight = size; - // } - - // final cacheSize = Size(cacheWidth, cacheHeight); - - // if (Platform.isIOS) { - // await captureFile2(randomFile, cacheSize, ogSize, key); - // return (ogSize, cacheSize); - // } - - // final result = await captureFile(randomFile, cacheSize, ogSize, key); - + Future _renderFile( + EnteFile randomFile, + String key, + String title, + ) async { const size = 512.0; - final result = await captureFile3(randomFile, ogSize, key); - if (result == null) { - _logger.warning("Can't capture file ${randomFile.displayName}"); + + final result = await _captureFile(randomFile, key, title); + if (!result) { + _logger.warning("can't capture file ${randomFile.displayName}"); return null; } - await hw.HomeWidget.saveWidgetData( - key, - result.path, - ); - - return (ogSize, const Size(size, size)); + return const Size(size, size); } Future countHomeWidgets() async { @@ -125,243 +77,139 @@ class HomeWidgetService { } Future clearHomeWidget() async { - _logger.info("Clearing SlideshowWidget"); + final total = await _getTotal(); + if (total == 0 || total == null) return; - await hw.HomeWidget.saveWidgetData("totalSet", 0); - await hw.HomeWidget.updateWidget( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ); + _logger.info("Clearing SlideshowWidget"); + await _setFilesHash(null); + await _setTotal(0); + await _updateWidget(); _logger.info(">>> SlideshowWidget cleared"); } - Future captureFile2( - EnteFile file, - Size size, - Size ogSize, - String key, - ) async { - final ogFile = await getFile(file); - - if (ogFile == null) { - return null; - } - - final minSize = min(size.width, size.height); - final Image img = Image.file( - ogFile, - fit: BoxFit.cover, - cacheWidth: size.width.toInt(), - cacheHeight: size.height.toInt(), - ); - - await PreloadImage.loadImage(img.image); - - final platformBrightness = - SchedulerBinding.instance.platformDispatcher.platformBrightness; - - final widget = ClipSmoothRect( - radius: SmoothBorderRadius(cornerRadius: 32, cornerSmoothing: 1), - child: Container( - width: minSize, - height: minSize, - decoration: BoxDecoration( - color: platformBrightness == Brightness.light - ? const Color.fromRGBO(251, 251, 251, 1) - : const Color.fromRGBO(27, 27, 27, 1), - image: DecorationImage(image: img.image, fit: BoxFit.cover), - ), - ), - ); - await hw.HomeWidget.renderFlutterWidget( - widget, - logicalSize: Size(minSize, minSize), - key: key, - ); - return; + Future _getFilesHash() async { + return await hw.HomeWidget.getWidgetData("filesHash"); } - Future captureFile( - EnteFile file, - Size size, - Size ogSize, - String key, - ) async { - final ogFile = await getFile(file); - - if (ogFile == null) { - return null; - } - - try { - final dir = await imagePath(); - final String path = '$dir/$key.png'; - - final File file = File(path); - file.createSync(recursive: true); - final image = img.decodeImage(ogFile.readAsBytesSync()); - final resizedImage = img.copyResize( - image!, - width: size.width.toInt(), - height: size.height.toInt(), - ); - await file.writeAsBytes(img.encodePng(resizedImage)); - return file; - } catch (_, __) { - _logger.severe("Failed to save the capture", _, __); - } - - return null; + Future _setFilesHash(String? fileHash) async { + await hw.HomeWidget.saveWidgetData("filesHash", fileHash); } - Future captureFile3( + Future _captureFile( EnteFile ogFile, - Size ogSize, String key, + String title, ) async { try { - final dir = await imagePath(); - final String path = '$dir/$key.png'; - - final File file = File(path); - file.createSync(recursive: true); final thumbnail = await getThumbnail(ogFile); - if (thumbnail == null) return null; - await file.writeAsBytes(thumbnail); - return file; + const double minSize = 512; + final Image img = Image.memory( + thumbnail!, + fit: BoxFit.cover, + cacheWidth: minSize.toInt(), + cacheHeight: minSize.toInt(), + ); + + await PreloadImage.loadImage(img.image); + + final platformBrightness = + SchedulerBinding.instance.platformDispatcher.platformBrightness; + + final widget = ClipSmoothRect( + radius: SmoothBorderRadius(cornerRadius: 32, cornerSmoothing: 1), + child: Container( + width: minSize, + height: minSize, + decoration: BoxDecoration( + color: platformBrightness == Brightness.light + ? const Color.fromRGBO(251, 251, 251, 1) + : const Color.fromRGBO(27, 27, 27, 1), + image: DecorationImage(image: img.image, fit: BoxFit.cover), + ), + ), + ); + await hw.HomeWidget.renderFlutterWidget( + widget, + logicalSize: const Size(minSize, minSize), + key: key, + ); + await hw.HomeWidget.saveWidgetData( + key + "_data", + { + "title": title, + "subText": SmartMemoriesService.getDateFormatted( + creationTime: ogFile.creationTime!, + ), + "generatedId": ogFile.generatedID, + }, + ); } catch (_, __) { _logger.severe("Failed to save the capture", _, __); + return false; } - - return null; + return true; } Future onLaunchFromWidget(Uri? uri, BuildContext context) async { - if (uri == null) return; - - // final res = previousGeneratedId != null - // ? await FilesDB.instance.getFile( - // previousGeneratedId, - // ) - // : null; - - // final page = DetailPage( - // DetailPageConfiguration(List.unmodifiable([res]), 0, "collection"), - // ); - // routeToPage(context, page, forceCustomPageRoute: true).ignore(); - } - - Future> getFiles({required bool fetchMemory}) async { - if (fetchMemory) { - final memories = await memoriesCacheService.getMemories(); - if (memories.isEmpty) { - return []; - } - - // flatten the list to list of ente files - final files = memories - .map((e) => e.memories.map((e) => e.file)) - .expand((element) => element) - .where((element) => element.fileType == FileType.image) - .toList(); - - return files; - } - - final collectionID = - await FavoritesService.instance.getFavoriteCollectionID(); - if (collectionID == null) { - await clearHomeWidget(); - _logger.warning("Favorite collection not found"); - throw "Favorite collection not found"; - } - - final res = await FilesDB.instance.getFilesInCollection( - collectionID, - galleryLoadStartTime, - galleryLoadEndTime, - limit: 100, - ); - - return res.files - .where((element) => element.fileType == FileType.image) - .toList(); - } - - Future imagePath() async { - String? directory; - // coverage:ignore-start - if (Platform.isIOS) { - final PathProviderFoundation provider = PathProviderFoundation(); - - directory = await provider.getContainerPath( - appGroupIdentifier: iOSGroupID, - ); - } else { - // coverage:ignore-end - directory = (await getApplicationSupportDirectory()).path; - } - - if (directory == null) { - throw "Directory is null"; - } - - final String path = '$directory/home_widget'; - - return path; - } - - Future lockAndLoadMemories() async { - final files = await getFiles(fetchMemory: true); - - int index = 0; - - for (final file in files) { - final value = await _renderFile(file, "slideshow_$index").catchError( - (e, sT) { - _logger.severe("Error rendering widget", e, sT); - return null; - }, - ); - - if (value != null) { - await hw.HomeWidget.saveWidgetData("totalSet", index); - if (index == 1 || index % 10 == 0) { - await Fluttertoast.showToast( - msg: "[i] SlideshowWidget updated", - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - timeInSecForIosWeb: 1, - backgroundColor: Colors.black, - textColor: Colors.white, - fontSize: 16.0, - ); - await hw.HomeWidget.updateWidget( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ); - } - index++; - } - } - - if (index == 0) { + if (uri == null) { + _logger.warning("onLaunchFromWidget: uri is null"); return; } - _logger.info(">>> SlideshowWidget params doing"); + final generatedId = int.tryParse(uri.queryParameters["generatedId"] ?? ""); + _logger.info("onLaunchFromWidget: $uri, $generatedId"); + final res = generatedId != null + ? await FilesDB.instance.getFile(generatedId) + : null; + + if (res == null) { + return; + } + + final page = DetailPage( + DetailPageConfiguration(List.unmodifiable([res]), 0, "collection"), + ); + routeToPage(context, page, forceCustomPageRoute: true).ignore(); + } + + Future>> _getMemories() async { + // if (fetchMemory) { + final memories = await memoriesCacheService.getMemories(); + if (memories.isEmpty) { + return {}; + } + + // flatten the list to list of ente files + final files = memories.asMap().map( + (k, v) => MapEntry( + v.title, + v.memories.map((e) => e.file), + ), + ); + + return files; + } + + String _getFilesKey(Map> files) { + // 1: file1_file2_file3, 2: file4_file5_file6 -> md5 hash + final key = files.entries + .map( + (entry) => + entry.key + ": " + entry.value.map((e) => e.cacheKey()).join("_"), + ) + .join(", "); + final hash = md5.convert(key.codeUnits).toString(); + return hash; + } + + Future _updateWidget() async { await hw.HomeWidget.updateWidget( name: 'SlideshowWidgetProvider', androidName: 'SlideshowWidgetProvider', qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', iOSName: 'SlideshowWidget', ); - _logger.info(">>> SlideshowWidget params done"); if (flagService.internalUser) { await Fluttertoast.showToast( msg: "[i] SlideshowWidget updated", @@ -373,5 +221,64 @@ class HomeWidgetService { fontSize: 16.0, ); } + _logger.info(">>> Home Widget updated"); + } + + Future _getTotal() async { + return await hw.HomeWidget.getWidgetData("totalSet"); + } + + Future _setTotal(int total) async { + await hw.HomeWidget.saveWidgetData("totalSet", total); + } + + Future _lockAndLoadMemories() async { + final files = await _getMemories(); + + if (files.isEmpty) { + _logger.warning("No files found, clearing everything"); + await clearHomeWidget(); + return; + } + + final keyHash = _getFilesKey(files); + + final value = await _getFilesHash(); + if (value == keyHash) { + _logger.info("No changes detected in memories"); + await _updateWidget(); + _logger.info(">>> Refreshing memory from same set"); + return; + } + await _setFilesHash(keyHash); + + int index = 0; + + for (final i in files.entries) { + for (final file in i.value) { + final value = + await _renderFile(file, "slideshow_$index", i.key).catchError( + (e, sT) { + _logger.severe("Error rendering widget", e, sT); + return null; + }, + ); + + if (value != null) { + await _setTotal(index); + if (index == 1) { + await _updateWidget(); + } + index++; + } + } + } + + if (index == 0) { + return; + } + + await _updateWidget(); + _logger.info(">>> Switching to next memory set"); } } diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index e6f519ae8d..930828d8d0 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -4,6 +4,7 @@ import "dart:math" show Random, max, min; import "package:computer/computer.dart"; import "package:flutter/foundation.dart" show kDebugMode; +import "package:flutter/material.dart"; import "package:intl/intl.dart"; import "package:logging/logging.dart"; import "package:ml_linalg/vector.dart"; @@ -1551,6 +1552,17 @@ class SmartMemoriesService { return memoryResults; } + static String getDateFormatted({ + required int creationTime, + BuildContext? context, + }) { + return DateFormat.yMMMd( + context != null ? Localizations.localeOf(context).languageCode : "en", + ).format( + DateTime.fromMicrosecondsSinceEpoch(creationTime), + ); + } + /// TODO: lau: replace this by just taking next 7 days static int _getWeekNumber(DateTime date) { // Get day of year (1-366) diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index bb92278911..74ef37a247 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -3,10 +3,10 @@ import "dart:io"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; -import "package:intl/intl.dart"; import "package:photos/core/configuration.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/services/memories_service.dart"; +import "package:photos/services/smart_memories_service.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/actions/file/file_actions.dart"; import "package:photos/ui/viewer/file/file_widget.dart"; @@ -192,12 +192,10 @@ class _FullScreenMemoryState extends State { children: [ child!, Text( - DateFormat.yMMMd( - Localizations.localeOf(context).languageCode, - ).format( - DateTime.fromMicrosecondsSinceEpoch( - inheritedData.memories[value].file.creationTime!, - ), + SmartMemoriesService.getDateFormatted( + creationTime: + inheritedData.memories[value].file.creationTime!, + context: context, ), style: Theme.of(context).textTheme.titleMedium!.copyWith( fontSize: 14, diff --git a/mobile/lib/ui/settings/gallery_settings_screen.dart b/mobile/lib/ui/settings/gallery_settings_screen.dart index 701e9dd1df..cc0bccea50 100644 --- a/mobile/lib/ui/settings/gallery_settings_screen.dart +++ b/mobile/lib/ui/settings/gallery_settings_screen.dart @@ -5,6 +5,7 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/hide_shared_items_from_home_gallery_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; +import "package:photos/services/home_widget_service.dart"; import "package:photos/services/memories_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/icon_button_widget.dart"; @@ -103,10 +104,15 @@ class _GallerySettingsScreenState extends State { trailingWidget: ToggleSwitchWidget( value: () => MemoriesService.instance.showMemories, onChanged: () async { + final showMemories = + MemoriesService.instance.showMemories; unawaited( - MemoriesService.instance.setShowMemories( - !MemoriesService.instance.showMemories, - ), + MemoriesService.instance + .setShowMemories(!showMemories), + ); + + unawaited( + HomeWidgetService.instance.initHomeWidget(false), ); }, ),