diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 811dca7cff..5a84a8e110 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -57,6 +57,7 @@ void main() async { if (kReleaseMode && Platform.isAndroid) { try { await FlutterDisplayMode.setHighRefreshRate(); + debugPrint("Enabled high refresh mode"); } catch (e) { debugPrint("Error setting high refresh rate: $e"); } diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index ec66387e7a..f7fa863dde 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -83,6 +83,12 @@ class ExifBottomSheet extends HookConsumerWidget { return SingleChildScrollView( child: Card( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), + ), + ), margin: const EdgeInsets.all(0), child: Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart deleted file mode 100644 index f97fd537e0..0000000000 --- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; - -final renderListProvider = StateProvider((ref) { - var assetGroups = ref.watch(assetGroupByDateTimeProvider); - - var settings = ref.watch(appSettingsServiceProvider); - final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); - - return assetGroupsToRenderList(assetGroups, assetsPerRow); -}); diff --git a/mobile/lib/modules/home/services/asset_cache.service.dart b/mobile/lib/modules/home/services/asset_cache.service.dart index 1275bfb54c..3eb684f6ea 100644 --- a/mobile/lib/modules/home/services/asset_cache.service.dart +++ b/mobile/lib/modules/home/services/asset_cache.service.dart @@ -7,9 +7,18 @@ import 'package:immich_mobile/shared/services/json_cache.dart'; class AssetCacheService extends JsonCache> { AssetCacheService() : super("asset_cache"); + static Future>> _computeSerialize( + List assets) async { + return assets.map((e) => e.toJson()).toList(); + } + @override - void put(List data) { - putRawData(data.map((e) => e.toJson()).toList()); + void put(List data) async { + putRawData(await compute(_computeSerialize, data)); + } + + static Future> _computeEncode(List data) async { + return data.map((e) => Asset.fromJson(e)).whereNotNull().toList(); } @override @@ -17,8 +26,7 @@ class AssetCacheService extends JsonCache> { try { final mapList = await readRawData() as List; - final responseData = - mapList.map((e) => Asset.fromJson(e)).whereNotNull().toList(); + final responseData = await compute(_computeEncode, mapList); return responseData; } catch (e) { diff --git a/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart index c852f89d22..a2461eb385 100644 --- a/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart +++ b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart @@ -1,5 +1,7 @@ import 'dart:math'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:logging/logging.dart'; @@ -33,85 +35,122 @@ class RenderAssetGridElement { }); } -List assetsToRenderList( - List assets, - int assetsPerRow, -) { - List elements = []; +class _AssetGroupsToRenderListComputeParameters { + final String monthFormat; + final String dayFormat; + final String dayFormatYear; + final Map> groups; + final int perRow; - int cursor = 0; - while (cursor < assets.length) { - int rowElements = min(assets.length - cursor, assetsPerRow); - final date = assets[cursor].createdAt; - - final rowElement = RenderAssetGridElement( - RenderAssetGridElementType.assetRow, - date: date, - assetRow: RenderAssetGridRow( - assets.sublist(cursor, cursor + rowElements), - ), - ); - - elements.add(rowElement); - cursor += rowElements; - } - - return elements; + _AssetGroupsToRenderListComputeParameters(this.monthFormat, this.dayFormat, + this.dayFormatYear, this.groups, this.perRow); } -List assetGroupsToRenderList( - Map> assetGroups, - int assetsPerRow, -) { - List elements = []; - DateTime? lastDate; +class RenderList { + final List elements; - assetGroups.forEach((groupName, assets) { - try { - final date = DateTime.parse(groupName); + RenderList(this.elements); + + static Future _processAssetGroupData( + _AssetGroupsToRenderListComputeParameters data) async { + final monthFormat = DateFormat(data.monthFormat); + final dayFormatSameYear = DateFormat(data.dayFormat); + final dayFormatOtherYear = DateFormat(data.dayFormatYear); + final groups = data.groups; + final perRow = data.perRow; + + List elements = []; + DateTime? lastDate; + + groups.forEach((groupName, assets) { + try { + final date = DateTime.parse(groupName); + + if (lastDate == null || lastDate!.month != date.month) { + // Month title + + var monthTitleText = groupName; + + var groupDate = DateTime.tryParse(groupName); + if (groupDate != null) { + monthTitleText = monthFormat.format(groupDate); + } else { + log.severe("Failed to format date for day title: $groupName"); + } + + elements.add( + RenderAssetGridElement( + RenderAssetGridElementType.monthTitle, + title: monthTitleText, + date: date, + ), + ); + } + + // Add group title + var currentYear = DateTime.now().year; + var groupYear = DateTime.parse(groupName).year; + var formatDate = + currentYear == groupYear ? dayFormatSameYear : dayFormatOtherYear; + + var dateText = groupName; + + var groupDate = DateTime.tryParse(groupName); + if (groupDate != null) { + dateText = formatDate.format(groupDate); + } else { + log.severe("Failed to format date for day title: $groupName"); + } - if (lastDate == null || lastDate!.month != date.month) { elements.add( RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - title: groupName, + RenderAssetGridElementType.dayTitle, + title: dateText, date: date, - ), - ); - } - - // Add group title - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.dayTitle, - title: groupName, - date: date, - relatedAssetList: assets, - ), - ); - - // Add rows - int cursor = 0; - while (cursor < assets.length) { - int rowElements = min(assets.length - cursor, assetsPerRow); - - final rowElement = RenderAssetGridElement( - RenderAssetGridElementType.assetRow, - date: date, - assetRow: RenderAssetGridRow( - assets.sublist(cursor, cursor + rowElements), + relatedAssetList: assets, ), ); - elements.add(rowElement); - cursor += rowElements; + // Add rows + int cursor = 0; + while (cursor < assets.length) { + int rowElements = min(assets.length - cursor, perRow); + + final rowElement = RenderAssetGridElement( + RenderAssetGridElementType.assetRow, + date: date, + assetRow: RenderAssetGridRow( + assets.sublist(cursor, cursor + rowElements), + ), + ); + + elements.add(rowElement); + cursor += rowElements; + } + + lastDate = date; + } catch (e, stackTrace) { + log.severe(e, stackTrace); } + }); - lastDate = date; - } catch (e, stackTrace) { - log.severe(e, stackTrace); - } - }); + return RenderList(elements); + } - return elements; + static Future fromAssetGroups( + Map> assetGroups, + int assetsPerRow, + ) async { + // Compute only allows for one parameter. Therefore we pass all parameters in a map + return compute( + _processAssetGroupData, + _AssetGroupsToRenderListComputeParameters( + "monthly_title_text_date_format".tr(), + "daily_title_text_date".tr(), + "daily_title_text_date_year".tr(), + assetGroups, + assetsPerRow, + ), + ); + } } diff --git a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart index 78e033ce59..f61d06cac3 100644 --- a/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart +++ b/mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart @@ -5,14 +5,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class DailyTitleText extends ConsumerWidget { const DailyTitleText({ Key? key, - required this.isoDate, + required this.text, required this.multiselectEnabled, required this.onSelect, required this.onDeselect, required this.selected, }) : super(key: key); - final String isoDate; + final String text; final bool multiselectEnabled; final Function onSelect; final Function onDeselect; @@ -20,13 +20,7 @@ class DailyTitleText extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var currentYear = DateTime.now().year; - var groupYear = DateTime.parse(isoDate).year; - var formatDateTemplate = currentYear == groupYear - ? "daily_title_text_date".tr() - : "daily_title_text_date_year".tr(); - var dateText = - DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); + void handleTitleIconClick() { if (selected) { @@ -46,7 +40,7 @@ class DailyTitleText extends ConsumerWidget { child: Row( children: [ Text( - dateText, + text, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index f069be32cd..d0f82990b8 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -24,22 +24,10 @@ class ImmichAssetGridState extends State { bool _scrolling = false; final Set _selectedAssets = HashSet(); - List get _assets { - return widget.renderList - .map((e) { - if (e.type == RenderAssetGridElementType.assetRow) { - return e.assetRow!.assets; - } else { - return List.empty(); - } - }) - .flattened - .toList(); - } Set _getSelectedAssets() { return _selectedAssets - .map((e) => _assets.firstWhereOrNull((a) => a.id == e)) + .map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e)) .whereNotNull() .toSet(); } @@ -95,9 +83,9 @@ class ImmichAssetGridState extends State { } return ThumbnailImage( asset: asset, - assetList: _assets, + assetList: widget.allAssets, multiselectEnabled: widget.selectionActive, - isSelected: _selectedAssets.contains(asset.id), + isSelected: widget.selectionActive && _selectedAssets.contains(asset.id), onSelect: () => _selectAssets([asset]), onDeselect: () => _deselectAssets([asset]), useGrayBoxPlaceholder: true, @@ -137,7 +125,7 @@ class ImmichAssetGridState extends State { List assets, ) { return DailyTitleText( - isoDate: title, + text: title, multiselectEnabled: widget.selectionActive, onSelect: () => _selectAssets(assets), onDeselect: () => _deselectAssets(assets), @@ -146,14 +134,11 @@ class ImmichAssetGridState extends State { } Widget _buildMonthTitle(BuildContext context, String title) { - var monthTitleText = DateFormat("monthly_title_text_date_format".tr()) - .format(DateTime.parse(title)); - return Padding( key: Key("month-$title"), padding: const EdgeInsets.only(left: 12.0, top: 32), child: Text( - monthTitleText, + title, style: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, @@ -164,7 +149,7 @@ class ImmichAssetGridState extends State { } Widget _itemBuilder(BuildContext c, int position) { - final item = widget.renderList[position]; + final item = widget.renderList.elements[position]; if (item.type == RenderAssetGridElementType.dayTitle) { return _buildTitle(c, item.title!, item.relatedAssetList!); @@ -178,7 +163,7 @@ class ImmichAssetGridState extends State { } Text _labelBuilder(int pos) { - final date = widget.renderList[pos].date; + final date = widget.renderList.elements[pos].date; return Text( DateFormat.yMMMd().format(date), style: const TextStyle( @@ -196,7 +181,7 @@ class ImmichAssetGridState extends State { } Widget _buildAssetGrid() { - final useDragScrolling = _assets.length >= 20; + final useDragScrolling = widget.allAssets.length >= 20; void dragScrolling(bool active) { setState(() { @@ -208,7 +193,8 @@ class ImmichAssetGridState extends State { itemBuilder: _itemBuilder, itemPositionsListener: _itemPositionsListener, itemScrollController: _itemScrollController, - itemCount: widget.renderList.length, + itemCount: widget.renderList.elements.length, + addRepaintBoundaries: true, ); if (!useDragScrolling) { @@ -250,16 +236,18 @@ class ImmichAssetGridState extends State { } class ImmichAssetGrid extends StatefulWidget { - final List renderList; + final RenderList renderList; final int assetsPerRow; final double margin; final bool showStorageIndicator; final ImmichAssetGridSelectionListener? listener; final bool selectionActive; + final List allAssets; const ImmichAssetGrid({ super.key, required this.renderList, + required this.allAssets, required this.assetsPerRow, required this.showStorageIndicator, this.listener, diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 1fafc63436..022eb6d152 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -8,7 +8,6 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; -import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; @@ -32,7 +31,6 @@ class HomePage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appSettingService = ref.watch(appSettingsServiceProvider); - var renderList = ref.watch(renderListProvider); final multiselectEnabled = ref.watch(multiselectProvider.notifier); final selectionEnabledHook = useState(false); @@ -212,10 +210,12 @@ class HomePage extends HookConsumerWidget { top: selectionEnabledHook.value ? 0 : 60, bottom: 0.0, ), - child: ref.watch(assetProvider).isEmpty + child: ref.watch(assetProvider).renderList == null || + ref.watch(assetProvider).allAssets.isEmpty ? buildLoadingIndicator() : ImmichAssetGrid( - renderList: renderList, + renderList: ref.watch(assetProvider).renderList!, + allAssets: ref.watch(assetProvider).allAssets, assetsPerRow: appSettingService .getSetting(AppSettingsEnum.tilesPerRow), showStorageIndicator: appSettingService diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart index ae90e4ade4..ba7f296e27 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -70,11 +70,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) { ); }); -final searchRenderListProvider = StateProvider((ref) { +final searchRenderListProvider = FutureProvider((ref) { var assetGroups = ref.watch(searchResultGroupByDateTimeProvider); var settings = ref.watch(appSettingsServiceProvider); final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); - return assetGroupsToRenderList(assetGroups, assetsPerRow); + return RenderList.fromAssetGroups(assetGroups, assetsPerRow); }); diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 4d20f87668..83fd5107db 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -111,6 +111,7 @@ class SearchResultPage extends HookConsumerWidget { buildSearchResult() { var searchResultPageState = ref.watch(searchResultPageProvider); var searchResultRenderList = ref.watch(searchRenderListProvider); + var allSearchAssets = ref.watch(searchResultPageProvider).searchResult; var settings = ref.watch(appSettingsServiceProvider); final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); @@ -126,10 +127,21 @@ class SearchResultPage extends HookConsumerWidget { } if (searchResultPageState.isSuccess) { - return ImmichAssetGrid( - renderList: searchResultRenderList, - assetsPerRow: assetsPerRow, - showStorageIndicator: showStorageIndicator, + return searchResultRenderList.when( + data: (result) { + return ImmichAssetGrid( + allAssets: allSearchAssets, + renderList: result, + assetsPerRow: assetsPerRow, + showStorageIndicator: showStorageIndicator, + ); + }, + error: (err, stack) { + return Text("$err"); + }, + loading: () { + return const CircularProgressIndicator(); + }, ); } diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart index c59c4de1da..44301447d4 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart @@ -21,7 +21,7 @@ class StorageIndicator extends HookConsumerWidget { appSettingService.setSetting(AppSettingsEnum.storageIndicator, value); showStorageIndicator.value = value; - ref.invalidate(assetGroupByDateTimeProvider); + ref.invalidate(assetProvider); } useEffect( diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart index a99b3a11ec..dde68a4a3e 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart @@ -23,7 +23,7 @@ class TilesPerRow extends HookConsumerWidget { } void sliderChangedEnd(double _) { - ref.invalidate(assetGroupByDateTimeProvider); + ref.invalidate(assetProvider); } useEffect( diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 96c19147a2..5ae9251eb2 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -1,10 +1,14 @@ import 'dart:collection'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset_cache.service.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/services/device_info.service.dart'; import 'package:collection/collection.dart'; @@ -14,18 +18,79 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; -class AssetNotifier extends StateNotifier> { +class AssetsState { + final List allAssets; + final RenderList? renderList; + + AssetsState(this.allAssets, {this.renderList}); + + Future withRenderDataStructure(int groupSize) async { + return AssetsState( + allAssets, + renderList: + await RenderList.fromAssetGroups(await _groupByDate(), groupSize), + ); + } + + AssetsState withAdditionalAssets(List toAdd) { + return AssetsState([...allAssets, ...toAdd]); + } + + _groupByDate() async { + sortCompare(List assets) { + assets.sortByCompare( + (e) => e.createdAt, + (a, b) => b.compareTo(a), + ); + return assets.groupListsBy( + (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()), + ); + } + + return await compute(sortCompare, allAssets.toList()); + } + + static fromAssetList(List assets) { + return AssetsState(assets); + } + + static empty() { + return AssetsState([]); + } +} + +class _CombineAssetsComputeParameters { + final Iterable local; + final Iterable remote; + final String deviceId; + + _CombineAssetsComputeParameters(this.local, this.remote, this.deviceId); +} + +class AssetNotifier extends StateNotifier { final AssetService _assetService; final AssetCacheService _assetCacheService; + final AppSettingsService _settingsService; final log = Logger('AssetNotifier'); final DeviceInfoService _deviceInfoService = DeviceInfoService(); bool _getAllAssetInProgress = false; bool _deleteInProgress = false; - AssetNotifier(this._assetService, this._assetCacheService) : super([]); + AssetNotifier( + this._assetService, + this._assetCacheService, + this._settingsService, + ) : super(AssetsState.fromAssetList([])); - _cacheState() { - _assetCacheService.put(state); + _updateAssetsState(List newAssetList, {bool cache = true}) async { + if (cache) { + _assetCacheService.put(newAssetList); + } + + state = + await AssetsState.fromAssetList(newAssetList).withRenderDataStructure( + _settingsService.getSetting(AppSettingsEnum.tilesPerRow), + ); } getAllAsset() async { @@ -43,17 +108,19 @@ class AssetNotifier extends StateNotifier> { final remoteTask = _assetService.getRemoteAssets( etag: isCacheValid ? box.get(assetEtagKey) : null, ); - if (isCacheValid && state.isEmpty) { - state = await _assetCacheService.get(); + if (isCacheValid && state.allAssets.isEmpty) { + await _updateAssetsState(await _assetCacheService.get(), cache: false); log.info( - "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms", + "Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms", ); stopwatch.reset(); } - int remoteBegin = state.indexWhere((a) => a.isRemote); - remoteBegin = remoteBegin == -1 ? state.length : remoteBegin; - final List currentLocal = state.slice(0, remoteBegin); + int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote); + remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin; + + final List currentLocal = state.allAssets.slice(0, remoteBegin); + final Pair?, String?> remoteResult = await remoteTask; List? newRemote = remoteResult.first; List? newLocal = await localTask; @@ -64,27 +131,32 @@ class AssetNotifier extends StateNotifier> { log.info("state is already up-to-date"); return; } - newRemote ??= state.slice(remoteBegin); + newRemote ??= state.allAssets.slice(remoteBegin); newLocal ??= []; - state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote); + + final combinedAssets = await _combineLocalAndRemoteAssets( + local: newLocal, + remote: newRemote, + ); + await _updateAssetsState(combinedAssets); + log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); - stopwatch.reset(); - _cacheState(); box.put(assetEtagKey, remoteResult.second); - log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); } finally { _getAllAssetInProgress = false; } } - List _combineLocalAndRemoteAssets({ - required Iterable local, - required List remote, - }) { + static Future> _computeCombine( + _CombineAssetsComputeParameters data, + ) async { + var local = data.local; + var remote = data.remote; + final deviceId = data.deviceId; + final List assets = []; if (remote.isNotEmpty && local.isNotEmpty) { - final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); final Set existingIds = remote .where((e) => e.deviceId == deviceId) .map((e) => e.deviceAssetId) @@ -97,31 +169,40 @@ class AssetNotifier extends StateNotifier> { return assets; } + Future> _combineLocalAndRemoteAssets({ + required Iterable local, + required List remote, + }) async { + final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); + return await compute( + _computeCombine, + _CombineAssetsComputeParameters(local, remote, deviceId), + ); + } + clearAllAsset() { - state = []; - _cacheState(); + _updateAssetsState([]); } onNewAssetUploaded(AssetResponseDto newAsset) { - final int i = state.indexWhere( + final int i = state.allAssets.indexWhere( (a) => a.isRemote || (a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId), ); - if (i == -1 || state[i].deviceAssetId != newAsset.deviceAssetId) { - state = [...state, Asset.remote(newAsset)]; + if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) { + _updateAssetsState([...state.allAssets, Asset.remote(newAsset)]); } else { // order is important to keep all local-only assets at the beginning! - state = [ - ...state.slice(0, i), - ...state.slice(i + 1), + _updateAssetsState([ + ...state.allAssets.slice(0, i), + ...state.allAssets.slice(i + 1), Asset.remote(newAsset), - ]; + ]); // TODO here is a place to unify local/remote assets by replacing the // local-only asset in the state with a local&remote asset } - _cacheState(); } deleteAssets(Set deleteAssets) async { @@ -133,8 +214,9 @@ class AssetNotifier extends StateNotifier> { deleted.addAll(localDeleted); deleted.addAll(remoteDeleted); if (deleted.isNotEmpty) { - state = state.where((a) => !deleted.contains(a.id)).toList(); - _cacheState(); + _updateAssetsState( + state.allAssets.where((a) => !deleted.contains(a.id)).toList(), + ); } } finally { _deleteInProgress = false; @@ -180,23 +262,11 @@ class AssetNotifier extends StateNotifier> { } } -final assetProvider = StateNotifierProvider>((ref) { +final assetProvider = StateNotifierProvider((ref) { return AssetNotifier( ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider), - ); -}); - -final assetGroupByDateTimeProvider = StateProvider((ref) { - final assets = ref.watch(assetProvider).toList(); - // `toList()` ist needed to make a copy as to NOT sort the original list/state - - assets.sortByCompare( - (e) => e.createdAt, - (a, b) => b.compareTo(a), - ); - return assets.groupListsBy( - (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()), + ref.watch(appSettingsServiceProvider), ); }); @@ -204,7 +274,8 @@ final assetGroupByMonthYearProvider = StateProvider((ref) { // TODO: remove `where` once temporary workaround is no longer needed (to only // allow remote assets to be added to album). Keep `toList()` as to NOT sort // the original list/state - final assets = ref.watch(assetProvider).where((e) => e.isRemote).toList(); + final assets = + ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList(); assets.sortByCompare( (e) => e.createdAt, diff --git a/mobile/lib/shared/services/json_cache.dart b/mobile/lib/shared/services/json_cache.dart index 739d8931de..34d2dbafd4 100644 --- a/mobile/lib/shared/services/json_cache.dart +++ b/mobile/lib/shared/services/json_cache.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; abstract class JsonCache { @@ -31,8 +32,13 @@ abstract class JsonCache { } } + static Future _computeEncodeJson(dynamic toEncode) async { + return json.encode(toEncode); + } + Future putRawData(dynamic data) async { - final jsonString = json.encode(data); + final jsonString = await compute(_computeEncodeJson, data); + final file = await _getCacheFile(); if (!await file.exists()) { @@ -42,10 +48,15 @@ abstract class JsonCache { await file.writeAsString(jsonString); } - dynamic readRawData() async { + static Future _computeDecodeJson(String jsonString) async { + return json.decode(jsonString); + } + + Future readRawData() async { final file = await _getCacheFile(); final data = await file.readAsString(); - return json.decode(data); + + return await compute(_computeDecodeJson, data); } void put(T data); diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/asset_grid_data_structure_test.dart index 0742e22482..e363786dba 100644 --- a/mobile/test/asset_grid_data_structure_test.dart +++ b/mobile/test/asset_grid_data_structure_test.dart @@ -54,55 +54,9 @@ void main() { }).toList() }; - group('Asset only list', () { - test('items < itemsPerRow', () { - final assets = testAssets.sublist(0, 2); - final renderList = assetsToRenderList(assets, 3); - - expect(renderList.length, 1); - expect(renderList[0].assetRow!.assets.length, 2); - }); - - test('items = itemsPerRow', () { - final assets = testAssets.sublist(0, 3); - final renderList = assetsToRenderList(assets, 3); - - expect(renderList.length, 1); - expect(renderList[0].assetRow!.assets.length, 3); - }); - - test('items > itemsPerRow', () { - final assets = testAssets.sublist(0, 20); - final renderList = assetsToRenderList(assets, 3); - - expect(renderList.length, 7); - expect(renderList[6].assetRow!.assets.length, 2); - }); - - test('items > itemsPerRow partition 4', () { - final assets = testAssets.sublist(0, 21); - final renderList = assetsToRenderList(assets, 4); - - expect(renderList.length, 6); - expect(renderList[5].assetRow!.assets.length, 1); - }); - - test('items > itemsPerRow check ids', () { - final assets = testAssets.sublist(0, 21); - final renderList = assetsToRenderList(assets, 3); - - expect(renderList.length, 7); - expect(renderList[6].assetRow!.assets.length, 3); - expect(renderList[0].assetRow!.assets[0].id, '0'); - expect(renderList[1].assetRow!.assets[1].id, '4'); - expect(renderList[3].assetRow!.assets[2].id, '11'); - expect(renderList[6].assetRow!.assets[2].id, '20'); - }); - }); - group('Test grouped', () { - test('test grouped check months', () { - final renderList = assetGroupsToRenderList(groups, 3); + test('test grouped check months', () async { + final renderList = await RenderList.fromAssetGroups(groups, 3); // Jan // Day 1 @@ -115,17 +69,17 @@ void main() { // Oct // Day 1 // 15 Assets => 5 Rows - expect(renderList.length, 18); - expect(renderList[0].type, RenderAssetGridElementType.monthTitle); - expect(renderList[0].date.month, 1); - expect(renderList[7].type, RenderAssetGridElementType.monthTitle); - expect(renderList[7].date.month, 2); - expect(renderList[11].type, RenderAssetGridElementType.monthTitle); - expect(renderList[11].date.month, 10); + expect(renderList.elements.length, 18); + expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle); + expect(renderList.elements[0].date.month, 1); + expect(renderList.elements[7].type, RenderAssetGridElementType.monthTitle); + expect(renderList.elements[7].date.month, 2); + expect(renderList.elements[11].type, RenderAssetGridElementType.monthTitle); + expect(renderList.elements[11].date.month, 10); }); - test('test grouped check types', () { - final renderList = assetGroupsToRenderList(groups, 5); + test('test grouped check types', () async { + final renderList = await RenderList.fromAssetGroups(groups, 5); // Jan // Day 1 @@ -155,10 +109,10 @@ void main() { RenderAssetGridElementType.assetRow ]; - expect(renderList.length, types.length); + expect(renderList.elements.length, types.length); - for (int i = 0; i < renderList.length; i++) { - expect(renderList[i].type, types[i]); + for (int i = 0; i < renderList.elements.length; i++) { + expect(renderList.elements[i].type, types[i]); } }); });