mirror of
https://github.com/immich-app/immich.git
synced 2025-06-04 14:14:23 -04:00
feat(mobile): Various minor performance improvements (#1176)
* Improve scroll performance by introducing repaint boundaries and moving more calculations to providers. * Add error handing for malformed dates. * Remove unused method * Use compute in different places to improve app performance during heavy tasks * Fix test * Refactor `List<RenderAssetGridElement>` to separate `RenderList` class and make `fromAssetGroups` a static method of this class. * Fix loading indicator bug * Use provider directly * `RenderList` refactoring * `AssetNotifier` refactoring * Move `combine` to static private method * Extract compute methods in cache services to static private methods. * Use `tryParse` instead of `parse` with try/catch for dates. * Fix bug in caching mechanism. * Fixed state not being used to trigger conditional rendering * styling * Corrected state Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
92972ac776
commit
7a1ae8691e
@ -57,6 +57,7 @@ void main() async {
|
|||||||
if (kReleaseMode && Platform.isAndroid) {
|
if (kReleaseMode && Platform.isAndroid) {
|
||||||
try {
|
try {
|
||||||
await FlutterDisplayMode.setHighRefreshRate();
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
|
debugPrint("Enabled high refresh mode");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error setting high refresh rate: $e");
|
debugPrint("Error setting high refresh rate: $e");
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,12 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Card(
|
child: Card(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(15),
|
||||||
|
topRight: Radius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
margin: const EdgeInsets.all(0),
|
margin: const EdgeInsets.all(0),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
@ -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);
|
|
||||||
});
|
|
@ -7,9 +7,18 @@ import 'package:immich_mobile/shared/services/json_cache.dart';
|
|||||||
class AssetCacheService extends JsonCache<List<Asset>> {
|
class AssetCacheService extends JsonCache<List<Asset>> {
|
||||||
AssetCacheService() : super("asset_cache");
|
AssetCacheService() : super("asset_cache");
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> _computeSerialize(
|
||||||
|
List<Asset> assets) async {
|
||||||
|
return assets.map((e) => e.toJson()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void put(List<Asset> data) {
|
void put(List<Asset> data) async {
|
||||||
putRawData(data.map((e) => e.toJson()).toList());
|
putRawData(await compute(_computeSerialize, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Asset>> _computeEncode(List<dynamic> data) async {
|
||||||
|
return data.map((e) => Asset.fromJson(e)).whereNotNull().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -17,8 +26,7 @@ class AssetCacheService extends JsonCache<List<Asset>> {
|
|||||||
try {
|
try {
|
||||||
final mapList = await readRawData() as List<dynamic>;
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
final responseData =
|
final responseData = await compute(_computeEncode, mapList);
|
||||||
mapList.map((e) => Asset.fromJson(e)).whereNotNull().toList();
|
|
||||||
|
|
||||||
return responseData;
|
return responseData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'dart:math';
|
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:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@ -33,85 +35,122 @@ class RenderAssetGridElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RenderAssetGridElement> assetsToRenderList(
|
class _AssetGroupsToRenderListComputeParameters {
|
||||||
List<Asset> assets,
|
final String monthFormat;
|
||||||
int assetsPerRow,
|
final String dayFormat;
|
||||||
) {
|
final String dayFormatYear;
|
||||||
List<RenderAssetGridElement> elements = [];
|
final Map<String, List<Asset>> groups;
|
||||||
|
final int perRow;
|
||||||
|
|
||||||
int cursor = 0;
|
_AssetGroupsToRenderListComputeParameters(this.monthFormat, this.dayFormat,
|
||||||
while (cursor < assets.length) {
|
this.dayFormatYear, this.groups, this.perRow);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RenderAssetGridElement> assetGroupsToRenderList(
|
class RenderList {
|
||||||
Map<String, List<Asset>> assetGroups,
|
final List<RenderAssetGridElement> elements;
|
||||||
int assetsPerRow,
|
|
||||||
) {
|
|
||||||
List<RenderAssetGridElement> elements = [];
|
|
||||||
DateTime? lastDate;
|
|
||||||
|
|
||||||
assetGroups.forEach((groupName, assets) {
|
RenderList(this.elements);
|
||||||
try {
|
|
||||||
final date = DateTime.parse(groupName);
|
static Future<RenderList> _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<RenderAssetGridElement> 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(
|
elements.add(
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElementType.dayTitle,
|
||||||
title: groupName,
|
title: dateText,
|
||||||
date: date,
|
date: date,
|
||||||
),
|
relatedAssetList: assets,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
elements.add(rowElement);
|
// Add rows
|
||||||
cursor += rowElements;
|
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;
|
return RenderList(elements);
|
||||||
} catch (e, stackTrace) {
|
}
|
||||||
log.severe(e, stackTrace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return elements;
|
static Future<RenderList> fromAssetGroups(
|
||||||
|
Map<String, List<Asset>> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
class DailyTitleText extends ConsumerWidget {
|
class DailyTitleText extends ConsumerWidget {
|
||||||
const DailyTitleText({
|
const DailyTitleText({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.isoDate,
|
required this.text,
|
||||||
required this.multiselectEnabled,
|
required this.multiselectEnabled,
|
||||||
required this.onSelect,
|
required this.onSelect,
|
||||||
required this.onDeselect,
|
required this.onDeselect,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String isoDate;
|
final String text;
|
||||||
final bool multiselectEnabled;
|
final bool multiselectEnabled;
|
||||||
final Function onSelect;
|
final Function onSelect;
|
||||||
final Function onDeselect;
|
final Function onDeselect;
|
||||||
@ -20,13 +20,7 @@ class DailyTitleText extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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() {
|
void handleTitleIconClick() {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@ -46,7 +40,7 @@ class DailyTitleText extends ConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
dateText,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
@ -24,22 +24,10 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<String> _selectedAssets = HashSet();
|
final Set<String> _selectedAssets = HashSet();
|
||||||
|
|
||||||
List<Asset> get _assets {
|
|
||||||
return widget.renderList
|
|
||||||
.map((e) {
|
|
||||||
if (e.type == RenderAssetGridElementType.assetRow) {
|
|
||||||
return e.assetRow!.assets;
|
|
||||||
} else {
|
|
||||||
return List<Asset>.empty();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flattened
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Asset> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return _selectedAssets
|
return _selectedAssets
|
||||||
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
|
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.toSet();
|
.toSet();
|
||||||
}
|
}
|
||||||
@ -95,9 +83,9 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
return ThumbnailImage(
|
return ThumbnailImage(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
assetList: _assets,
|
assetList: widget.allAssets,
|
||||||
multiselectEnabled: widget.selectionActive,
|
multiselectEnabled: widget.selectionActive,
|
||||||
isSelected: _selectedAssets.contains(asset.id),
|
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
|
||||||
onSelect: () => _selectAssets([asset]),
|
onSelect: () => _selectAssets([asset]),
|
||||||
onDeselect: () => _deselectAssets([asset]),
|
onDeselect: () => _deselectAssets([asset]),
|
||||||
useGrayBoxPlaceholder: true,
|
useGrayBoxPlaceholder: true,
|
||||||
@ -137,7 +125,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
List<Asset> assets,
|
List<Asset> assets,
|
||||||
) {
|
) {
|
||||||
return DailyTitleText(
|
return DailyTitleText(
|
||||||
isoDate: title,
|
text: title,
|
||||||
multiselectEnabled: widget.selectionActive,
|
multiselectEnabled: widget.selectionActive,
|
||||||
onSelect: () => _selectAssets(assets),
|
onSelect: () => _selectAssets(assets),
|
||||||
onDeselect: () => _deselectAssets(assets),
|
onDeselect: () => _deselectAssets(assets),
|
||||||
@ -146,14 +134,11 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMonthTitle(BuildContext context, String title) {
|
Widget _buildMonthTitle(BuildContext context, String title) {
|
||||||
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
|
||||||
.format(DateTime.parse(title));
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
key: Key("month-$title"),
|
key: Key("month-$title"),
|
||||||
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||||
child: Text(
|
child: Text(
|
||||||
monthTitleText,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -164,7 +149,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _itemBuilder(BuildContext c, int position) {
|
Widget _itemBuilder(BuildContext c, int position) {
|
||||||
final item = widget.renderList[position];
|
final item = widget.renderList.elements[position];
|
||||||
|
|
||||||
if (item.type == RenderAssetGridElementType.dayTitle) {
|
if (item.type == RenderAssetGridElementType.dayTitle) {
|
||||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||||
@ -178,7 +163,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text _labelBuilder(int pos) {
|
Text _labelBuilder(int pos) {
|
||||||
final date = widget.renderList[pos].date;
|
final date = widget.renderList.elements[pos].date;
|
||||||
return Text(
|
return Text(
|
||||||
DateFormat.yMMMd().format(date),
|
DateFormat.yMMMd().format(date),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -196,7 +181,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAssetGrid() {
|
Widget _buildAssetGrid() {
|
||||||
final useDragScrolling = _assets.length >= 20;
|
final useDragScrolling = widget.allAssets.length >= 20;
|
||||||
|
|
||||||
void dragScrolling(bool active) {
|
void dragScrolling(bool active) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -208,7 +193,8 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
itemBuilder: _itemBuilder,
|
itemBuilder: _itemBuilder,
|
||||||
itemPositionsListener: _itemPositionsListener,
|
itemPositionsListener: _itemPositionsListener,
|
||||||
itemScrollController: _itemScrollController,
|
itemScrollController: _itemScrollController,
|
||||||
itemCount: widget.renderList.length,
|
itemCount: widget.renderList.elements.length,
|
||||||
|
addRepaintBoundaries: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!useDragScrolling) {
|
if (!useDragScrolling) {
|
||||||
@ -250,16 +236,18 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImmichAssetGrid extends StatefulWidget {
|
class ImmichAssetGrid extends StatefulWidget {
|
||||||
final List<RenderAssetGridElement> renderList;
|
final RenderList renderList;
|
||||||
final int assetsPerRow;
|
final int assetsPerRow;
|
||||||
final double margin;
|
final double margin;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final ImmichAssetGridSelectionListener? listener;
|
final ImmichAssetGridSelectionListener? listener;
|
||||||
final bool selectionActive;
|
final bool selectionActive;
|
||||||
|
final List<Asset> allAssets;
|
||||||
|
|
||||||
const ImmichAssetGrid({
|
const ImmichAssetGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.renderList,
|
required this.renderList,
|
||||||
|
required this.allAssets,
|
||||||
required this.assetsPerRow,
|
required this.assetsPerRow,
|
||||||
required this.showStorageIndicator,
|
required this.showStorageIndicator,
|
||||||
this.listener,
|
this.listener,
|
||||||
|
@ -8,7 +8,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.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/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/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.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';
|
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||||
@ -32,7 +31,6 @@ class HomePage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
var renderList = ref.watch(renderListProvider);
|
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
|
|
||||||
@ -212,10 +210,12 @@ class HomePage extends HookConsumerWidget {
|
|||||||
top: selectionEnabledHook.value ? 0 : 60,
|
top: selectionEnabledHook.value ? 0 : 60,
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
),
|
),
|
||||||
child: ref.watch(assetProvider).isEmpty
|
child: ref.watch(assetProvider).renderList == null ||
|
||||||
|
ref.watch(assetProvider).allAssets.isEmpty
|
||||||
? buildLoadingIndicator()
|
? buildLoadingIndicator()
|
||||||
: ImmichAssetGrid(
|
: ImmichAssetGrid(
|
||||||
renderList: renderList,
|
renderList: ref.watch(assetProvider).renderList!,
|
||||||
|
allAssets: ref.watch(assetProvider).allAssets,
|
||||||
assetsPerRow: appSettingService
|
assetsPerRow: appSettingService
|
||||||
.getSetting(AppSettingsEnum.tilesPerRow),
|
.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
showStorageIndicator: appSettingService
|
showStorageIndicator: appSettingService
|
||||||
|
@ -70,11 +70,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final searchRenderListProvider = StateProvider((ref) {
|
final searchRenderListProvider = FutureProvider((ref) {
|
||||||
var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
|
var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
|
||||||
|
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
||||||
|
|
||||||
return assetGroupsToRenderList(assetGroups, assetsPerRow);
|
return RenderList.fromAssetGroups(assetGroups, assetsPerRow);
|
||||||
});
|
});
|
||||||
|
@ -111,6 +111,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
buildSearchResult() {
|
buildSearchResult() {
|
||||||
var searchResultPageState = ref.watch(searchResultPageProvider);
|
var searchResultPageState = ref.watch(searchResultPageProvider);
|
||||||
var searchResultRenderList = ref.watch(searchRenderListProvider);
|
var searchResultRenderList = ref.watch(searchRenderListProvider);
|
||||||
|
var allSearchAssets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
||||||
@ -126,10 +127,21 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchResultPageState.isSuccess) {
|
if (searchResultPageState.isSuccess) {
|
||||||
return ImmichAssetGrid(
|
return searchResultRenderList.when(
|
||||||
renderList: searchResultRenderList,
|
data: (result) {
|
||||||
assetsPerRow: assetsPerRow,
|
return ImmichAssetGrid(
|
||||||
showStorageIndicator: showStorageIndicator,
|
allAssets: allSearchAssets,
|
||||||
|
renderList: result,
|
||||||
|
assetsPerRow: assetsPerRow,
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (err, stack) {
|
||||||
|
return Text("$err");
|
||||||
|
},
|
||||||
|
loading: () {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class StorageIndicator extends HookConsumerWidget {
|
|||||||
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
|
appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
|
||||||
showStorageIndicator.value = value;
|
showStorageIndicator.value = value;
|
||||||
|
|
||||||
ref.invalidate(assetGroupByDateTimeProvider);
|
ref.invalidate(assetProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -23,7 +23,7 @@ class TilesPerRow extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sliderChangedEnd(double _) {
|
void sliderChangedEnd(double _) {
|
||||||
ref.invalidate(assetGroupByDateTimeProvider);
|
ref.invalidate(assetProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.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.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset_cache.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/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -14,18 +18,79 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<Asset>> {
|
class AssetsState {
|
||||||
|
final List<Asset> allAssets;
|
||||||
|
final RenderList? renderList;
|
||||||
|
|
||||||
|
AssetsState(this.allAssets, {this.renderList});
|
||||||
|
|
||||||
|
Future<AssetsState> withRenderDataStructure(int groupSize) async {
|
||||||
|
return AssetsState(
|
||||||
|
allAssets,
|
||||||
|
renderList:
|
||||||
|
await RenderList.fromAssetGroups(await _groupByDate(), groupSize),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetsState withAdditionalAssets(List<Asset> toAdd) {
|
||||||
|
return AssetsState([...allAssets, ...toAdd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_groupByDate() async {
|
||||||
|
sortCompare(List<Asset> assets) {
|
||||||
|
assets.sortByCompare<DateTime>(
|
||||||
|
(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<Asset> assets) {
|
||||||
|
return AssetsState(assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
static empty() {
|
||||||
|
return AssetsState([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CombineAssetsComputeParameters {
|
||||||
|
final Iterable<Asset> local;
|
||||||
|
final Iterable<Asset> remote;
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
|
_CombineAssetsComputeParameters(this.local, this.remote, this.deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final AssetCacheService _assetCacheService;
|
final AssetCacheService _assetCacheService;
|
||||||
|
final AppSettingsService _settingsService;
|
||||||
final log = Logger('AssetNotifier');
|
final log = Logger('AssetNotifier');
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
bool _getAllAssetInProgress = false;
|
bool _getAllAssetInProgress = false;
|
||||||
bool _deleteInProgress = false;
|
bool _deleteInProgress = false;
|
||||||
|
|
||||||
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
AssetNotifier(
|
||||||
|
this._assetService,
|
||||||
|
this._assetCacheService,
|
||||||
|
this._settingsService,
|
||||||
|
) : super(AssetsState.fromAssetList([]));
|
||||||
|
|
||||||
_cacheState() {
|
_updateAssetsState(List<Asset> newAssetList, {bool cache = true}) async {
|
||||||
_assetCacheService.put(state);
|
if (cache) {
|
||||||
|
_assetCacheService.put(newAssetList);
|
||||||
|
}
|
||||||
|
|
||||||
|
state =
|
||||||
|
await AssetsState.fromAssetList(newAssetList).withRenderDataStructure(
|
||||||
|
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllAsset() async {
|
getAllAsset() async {
|
||||||
@ -43,17 +108,19 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
|||||||
final remoteTask = _assetService.getRemoteAssets(
|
final remoteTask = _assetService.getRemoteAssets(
|
||||||
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
||||||
);
|
);
|
||||||
if (isCacheValid && state.isEmpty) {
|
if (isCacheValid && state.allAssets.isEmpty) {
|
||||||
state = await _assetCacheService.get();
|
await _updateAssetsState(await _assetCacheService.get(), cache: false);
|
||||||
log.info(
|
log.info(
|
||||||
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
|
"Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms",
|
||||||
);
|
);
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
int remoteBegin = state.indexWhere((a) => a.isRemote);
|
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
||||||
remoteBegin = remoteBegin == -1 ? state.length : remoteBegin;
|
remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin;
|
||||||
final List<Asset> currentLocal = state.slice(0, remoteBegin);
|
|
||||||
|
final List<Asset> currentLocal = state.allAssets.slice(0, remoteBegin);
|
||||||
|
|
||||||
final Pair<List<Asset>?, String?> remoteResult = await remoteTask;
|
final Pair<List<Asset>?, String?> remoteResult = await remoteTask;
|
||||||
List<Asset>? newRemote = remoteResult.first;
|
List<Asset>? newRemote = remoteResult.first;
|
||||||
List<Asset>? newLocal = await localTask;
|
List<Asset>? newLocal = await localTask;
|
||||||
@ -64,27 +131,32 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
|||||||
log.info("state is already up-to-date");
|
log.info("state is already up-to-date");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newRemote ??= state.slice(remoteBegin);
|
newRemote ??= state.allAssets.slice(remoteBegin);
|
||||||
newLocal ??= [];
|
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");
|
log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
|
||||||
stopwatch.reset();
|
|
||||||
_cacheState();
|
|
||||||
box.put(assetEtagKey, remoteResult.second);
|
box.put(assetEtagKey, remoteResult.second);
|
||||||
log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
|
||||||
} finally {
|
} finally {
|
||||||
_getAllAssetInProgress = false;
|
_getAllAssetInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Asset> _combineLocalAndRemoteAssets({
|
static Future<List<Asset>> _computeCombine(
|
||||||
required Iterable<Asset> local,
|
_CombineAssetsComputeParameters data,
|
||||||
required List<Asset> remote,
|
) async {
|
||||||
}) {
|
var local = data.local;
|
||||||
|
var remote = data.remote;
|
||||||
|
final deviceId = data.deviceId;
|
||||||
|
|
||||||
final List<Asset> assets = [];
|
final List<Asset> assets = [];
|
||||||
if (remote.isNotEmpty && local.isNotEmpty) {
|
if (remote.isNotEmpty && local.isNotEmpty) {
|
||||||
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
|
||||||
final Set<String> existingIds = remote
|
final Set<String> existingIds = remote
|
||||||
.where((e) => e.deviceId == deviceId)
|
.where((e) => e.deviceId == deviceId)
|
||||||
.map((e) => e.deviceAssetId)
|
.map((e) => e.deviceAssetId)
|
||||||
@ -97,31 +169,40 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
|||||||
return assets;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Asset>> _combineLocalAndRemoteAssets({
|
||||||
|
required Iterable<Asset> local,
|
||||||
|
required List<Asset> remote,
|
||||||
|
}) async {
|
||||||
|
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
return await compute(
|
||||||
|
_computeCombine,
|
||||||
|
_CombineAssetsComputeParameters(local, remote, deviceId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
clearAllAsset() {
|
clearAllAsset() {
|
||||||
state = [];
|
_updateAssetsState([]);
|
||||||
_cacheState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||||
final int i = state.indexWhere(
|
final int i = state.allAssets.indexWhere(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.isRemote ||
|
a.isRemote ||
|
||||||
(a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId),
|
(a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (i == -1 || state[i].deviceAssetId != newAsset.deviceAssetId) {
|
if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||||
state = [...state, Asset.remote(newAsset)];
|
_updateAssetsState([...state.allAssets, Asset.remote(newAsset)]);
|
||||||
} else {
|
} else {
|
||||||
// order is important to keep all local-only assets at the beginning!
|
// order is important to keep all local-only assets at the beginning!
|
||||||
state = [
|
_updateAssetsState([
|
||||||
...state.slice(0, i),
|
...state.allAssets.slice(0, i),
|
||||||
...state.slice(i + 1),
|
...state.allAssets.slice(i + 1),
|
||||||
Asset.remote(newAsset),
|
Asset.remote(newAsset),
|
||||||
];
|
]);
|
||||||
// TODO here is a place to unify local/remote assets by replacing the
|
// TODO here is a place to unify local/remote assets by replacing the
|
||||||
// local-only asset in the state with a local&remote asset
|
// local-only asset in the state with a local&remote asset
|
||||||
}
|
}
|
||||||
_cacheState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAssets(Set<Asset> deleteAssets) async {
|
deleteAssets(Set<Asset> deleteAssets) async {
|
||||||
@ -133,8 +214,9 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
|||||||
deleted.addAll(localDeleted);
|
deleted.addAll(localDeleted);
|
||||||
deleted.addAll(remoteDeleted);
|
deleted.addAll(remoteDeleted);
|
||||||
if (deleted.isNotEmpty) {
|
if (deleted.isNotEmpty) {
|
||||||
state = state.where((a) => !deleted.contains(a.id)).toList();
|
_updateAssetsState(
|
||||||
_cacheState();
|
state.allAssets.where((a) => !deleted.contains(a.id)).toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_deleteInProgress = false;
|
_deleteInProgress = false;
|
||||||
@ -180,23 +262,11 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<Asset>>((ref) {
|
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||||
return AssetNotifier(
|
return AssetNotifier(
|
||||||
ref.watch(assetServiceProvider),
|
ref.watch(assetServiceProvider),
|
||||||
ref.watch(assetCacheServiceProvider),
|
ref.watch(assetCacheServiceProvider),
|
||||||
);
|
ref.watch(appSettingsServiceProvider),
|
||||||
});
|
|
||||||
|
|
||||||
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<DateTime>(
|
|
||||||
(e) => e.createdAt,
|
|
||||||
(a, b) => b.compareTo(a),
|
|
||||||
);
|
|
||||||
return assets.groupListsBy(
|
|
||||||
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,7 +274,8 @@ final assetGroupByMonthYearProvider = StateProvider((ref) {
|
|||||||
// TODO: remove `where` once temporary workaround is no longer needed (to only
|
// 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
|
// allow remote assets to be added to album). Keep `toList()` as to NOT sort
|
||||||
// the original list/state
|
// 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<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => e.createdAt,
|
(e) => e.createdAt,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
abstract class JsonCache<T> {
|
abstract class JsonCache<T> {
|
||||||
@ -31,8 +32,13 @@ abstract class JsonCache<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<String> _computeEncodeJson(dynamic toEncode) async {
|
||||||
|
return json.encode(toEncode);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> putRawData(dynamic data) async {
|
Future<void> putRawData(dynamic data) async {
|
||||||
final jsonString = json.encode(data);
|
final jsonString = await compute(_computeEncodeJson, data);
|
||||||
|
|
||||||
final file = await _getCacheFile();
|
final file = await _getCacheFile();
|
||||||
|
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
@ -42,10 +48,15 @@ abstract class JsonCache<T> {
|
|||||||
await file.writeAsString(jsonString);
|
await file.writeAsString(jsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic readRawData() async {
|
static Future<dynamic> _computeDecodeJson(String jsonString) async {
|
||||||
|
return json.decode(jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> readRawData() async {
|
||||||
final file = await _getCacheFile();
|
final file = await _getCacheFile();
|
||||||
final data = await file.readAsString();
|
final data = await file.readAsString();
|
||||||
return json.decode(data);
|
|
||||||
|
return await compute(_computeDecodeJson, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void put(T data);
|
void put(T data);
|
||||||
|
@ -54,55 +54,9 @@ void main() {
|
|||||||
}).toList()
|
}).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', () {
|
group('Test grouped', () {
|
||||||
test('test grouped check months', () {
|
test('test grouped check months', () async {
|
||||||
final renderList = assetGroupsToRenderList(groups, 3);
|
final renderList = await RenderList.fromAssetGroups(groups, 3);
|
||||||
|
|
||||||
// Jan
|
// Jan
|
||||||
// Day 1
|
// Day 1
|
||||||
@ -115,17 +69,17 @@ void main() {
|
|||||||
// Oct
|
// Oct
|
||||||
// Day 1
|
// Day 1
|
||||||
// 15 Assets => 5 Rows
|
// 15 Assets => 5 Rows
|
||||||
expect(renderList.length, 18);
|
expect(renderList.elements.length, 18);
|
||||||
expect(renderList[0].type, RenderAssetGridElementType.monthTitle);
|
expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle);
|
||||||
expect(renderList[0].date.month, 1);
|
expect(renderList.elements[0].date.month, 1);
|
||||||
expect(renderList[7].type, RenderAssetGridElementType.monthTitle);
|
expect(renderList.elements[7].type, RenderAssetGridElementType.monthTitle);
|
||||||
expect(renderList[7].date.month, 2);
|
expect(renderList.elements[7].date.month, 2);
|
||||||
expect(renderList[11].type, RenderAssetGridElementType.monthTitle);
|
expect(renderList.elements[11].type, RenderAssetGridElementType.monthTitle);
|
||||||
expect(renderList[11].date.month, 10);
|
expect(renderList.elements[11].date.month, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test grouped check types', () {
|
test('test grouped check types', () async {
|
||||||
final renderList = assetGroupsToRenderList(groups, 5);
|
final renderList = await RenderList.fromAssetGroups(groups, 5);
|
||||||
|
|
||||||
// Jan
|
// Jan
|
||||||
// Day 1
|
// Day 1
|
||||||
@ -155,10 +109,10 @@ void main() {
|
|||||||
RenderAssetGridElementType.assetRow
|
RenderAssetGridElementType.assetRow
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(renderList.length, types.length);
|
expect(renderList.elements.length, types.length);
|
||||||
|
|
||||||
for (int i = 0; i < renderList.length; i++) {
|
for (int i = 0; i < renderList.elements.length; i++) {
|
||||||
expect(renderList[i].type, types[i]);
|
expect(renderList.elements[i].type, types[i]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user