Skip to content

Commit 1633af7

Browse files
authored
feat(mobile): show local assets (immich-app#905)
* introduce Asset as composition of AssetResponseDTO and AssetEntity * filter out duplicate assets (that are both local and remote, take only remote for now) * only allow remote images to be added to albums * introduce ImmichImage to render Asset using local or remote data * optimized deletion of local assets * local video file playback * allow multiple methods to wait on background service finished * skip local assets when adding to album from home screen * fix and optimize delete * show gray box placeholder for local assets * add comments * fix bug: duplicate assets in state after onNewAssetUploaded
1 parent 99da181 commit 1633af7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+823
-507
lines changed

mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
134134
}
135135

136136
private fun stopEngine(result: Result?) {
137+
clearBackgroundNotification()
138+
engine?.destroy()
139+
engine = null
137140
if (result != null) {
138141
Log.d(TAG, "stopEngine result=${result}")
139142
resolvableFuture.set(result)
140143
}
141-
engine?.destroy()
142-
engine = null
143-
clearBackgroundNotification()
144144
waitOnSetForegroundAsync()
145145
}
146146

mobile/lib/main.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ void main() async {
3535
await Future.wait([
3636
Hive.openBox(userInfoBox),
3737
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
38-
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
3938
Hive.openBox(hiveGithubReleaseInfoBox),
4039
Hive.openBox(userSettingInfoBox),
41-
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
40+
if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
41+
if (!Platform.isAndroid)
42+
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
43+
if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
4244
EasyLocalization.ensureInitialized(),
4345
]);
4446

@@ -86,8 +88,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
8688
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
8789

8890
if (isAuthenticated) {
91+
ref.read(backupProvider.notifier).resumeBackup();
8992
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
90-
ref.watch(backupProvider.notifier).resumeBackup();
9193
ref.watch(assetProvider.notifier).getAllAsset();
9294
ref.watch(serverInfoProvider.notifier).getServerVersion();
9395
}

mobile/lib/modules/album/models/asset_selection_page_result.model.dart

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import 'package:collection/collection.dart';
2-
3-
import 'package:openapi/api.dart';
2+
import 'package:immich_mobile/shared/models/asset.dart';
43

54
class AssetSelectionPageResult {
6-
final Set<AssetResponseDto> selectedNewAsset;
7-
final Set<AssetResponseDto> selectedAdditionalAsset;
5+
final Set<Asset> selectedNewAsset;
6+
final Set<Asset> selectedAdditionalAsset;
87
final bool isAlbumExist;
98

109
AssetSelectionPageResult({
@@ -14,8 +13,8 @@ class AssetSelectionPageResult {
1413
});
1514

1615
AssetSelectionPageResult copyWith({
17-
Set<AssetResponseDto>? selectedNewAsset,
18-
Set<AssetResponseDto>? selectedAdditionalAsset,
16+
Set<Asset>? selectedNewAsset,
17+
Set<Asset>? selectedAdditionalAsset,
1918
bool? isAlbumExist,
2019
}) {
2120
return AssetSelectionPageResult(

mobile/lib/modules/album/models/asset_selection_state.model.dart

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import 'package:collection/collection.dart';
2-
3-
import 'package:openapi/api.dart';
2+
import 'package:immich_mobile/shared/models/asset.dart';
43

54
class AssetSelectionState {
65
final Set<String> selectedMonths;
7-
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
8-
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
9-
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
6+
final Set<Asset> selectedNewAssetsForAlbum;
7+
final Set<Asset> selectedAdditionalAssetsForAlbum;
8+
final Set<Asset> selectedAssetsInAlbumViewer;
109
final bool isMultiselectEnable;
1110

1211
/// Indicate the asset selection page is navigated from existing album
@@ -22,9 +21,9 @@ class AssetSelectionState {
2221

2322
AssetSelectionState copyWith({
2423
Set<String>? selectedMonths,
25-
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
26-
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
27-
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
24+
Set<Asset>? selectedNewAssetsForAlbum,
25+
Set<Asset>? selectedAdditionalAssetsForAlbum,
26+
Set<Asset>? selectedAssetsInAlbumViewer,
2827
bool? isMultiselectEnable,
2928
bool? isAlbumExist,
3029
}) {

mobile/lib/modules/album/providers/album.provider.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:hooks_riverpod/hooks_riverpod.dart';
22
import 'package:immich_mobile/modules/album/services/album.service.dart';
33
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
4+
import 'package:immich_mobile/shared/models/asset.dart';
45
import 'package:openapi/api.dart';
56

67
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
@@ -13,7 +14,6 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
1314
}
1415

1516
getAllAlbums() async {
16-
1717
if (await _albumCacheService.isValid() && state.isEmpty) {
1818
state = await _albumCacheService.get();
1919
}
@@ -34,7 +34,7 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
3434

3535
Future<AlbumResponseDto?> createAlbum(
3636
String albumTitle,
37-
Set<AssetResponseDto> assets,
37+
Set<Asset> assets,
3838
) async {
3939
AlbumResponseDto? album =
4040
await _albumService.createAlbum(albumTitle, assets, []);

mobile/lib/modules/album/providers/asset_selection.provider.dart

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:hooks_riverpod/hooks_riverpod.dart';
22
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
3-
4-
import 'package:openapi/api.dart';
3+
import 'package:immich_mobile/shared/models/asset.dart';
54

65
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
76
AssetSelectionNotifier()
@@ -22,15 +21,15 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
2221

2322
void removeAssetsInMonth(
2423
String removedMonth,
25-
List<AssetResponseDto> assetsInMonth,
24+
List<Asset> assetsInMonth,
2625
) {
27-
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
26+
Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
2827
Set<String> currentMonthList = state.selectedMonths;
2928

3029
currentMonthList
3130
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
3231

33-
for (AssetResponseDto asset in assetsInMonth) {
32+
for (Asset asset in assetsInMonth) {
3433
currentAssetList.removeWhere((e) => e.id == asset.id);
3534
}
3635

@@ -40,7 +39,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
4039
);
4140
}
4241

43-
void addAdditionalAssets(List<AssetResponseDto> assets) {
42+
void addAdditionalAssets(List<Asset> assets) {
4443
state = state.copyWith(
4544
selectedAdditionalAssetsForAlbum: {
4645
...state.selectedAdditionalAssetsForAlbum,
@@ -49,7 +48,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
4948
);
5049
}
5150

52-
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
51+
void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
5352
state = state.copyWith(
5453
selectedMonths: {...state.selectedMonths, month},
5554
selectedNewAssetsForAlbum: {
@@ -59,7 +58,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
5958
);
6059
}
6160

62-
void addNewAssets(List<AssetResponseDto> assets) {
61+
void addNewAssets(List<Asset> assets) {
6362
state = state.copyWith(
6463
selectedNewAssetsForAlbum: {
6564
...state.selectedNewAssetsForAlbum,
@@ -68,20 +67,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
6867
);
6968
}
7069

71-
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
72-
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
70+
void removeSelectedNewAssets(List<Asset> assets) {
71+
Set<Asset> currentList = state.selectedNewAssetsForAlbum;
7372

74-
for (AssetResponseDto asset in assets) {
73+
for (Asset asset in assets) {
7574
currentList.removeWhere((e) => e.id == asset.id);
7675
}
7776

7877
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
7978
}
8079

81-
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
82-
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
80+
void removeSelectedAdditionalAssets(List<Asset> assets) {
81+
Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
8382

84-
for (AssetResponseDto asset in assets) {
83+
for (Asset asset in assets) {
8584
currentList.removeWhere((e) => e.id == asset.id);
8685
}
8786

@@ -109,7 +108,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
109108
);
110109
}
111110

112-
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
111+
void addAssetsInAlbumViewer(List<Asset> assets) {
113112
state = state.copyWith(
114113
selectedAssetsInAlbumViewer: {
115114
...state.selectedAssetsInAlbumViewer,
@@ -118,10 +117,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
118117
);
119118
}
120119

121-
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
122-
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
120+
void removeAssetsInAlbumViewer(List<Asset> assets) {
121+
Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
123122

124-
for (AssetResponseDto asset in assets) {
123+
for (Asset asset in assets) {
125124
currentList.removeWhere((e) => e.id == asset.id);
126125
}
127126

mobile/lib/modules/album/providers/shared_album.provider.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
22
import 'package:hooks_riverpod/hooks_riverpod.dart';
33
import 'package:immich_mobile/modules/album/services/album.service.dart';
44
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
5+
import 'package:immich_mobile/shared/models/asset.dart';
56
import 'package:openapi/api.dart';
67

78
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
8-
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
9+
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService)
10+
: super([]);
911

1012
final AlbumService _sharedAlbumService;
1113
final SharedAlbumCacheService _sharedAlbumCacheService;
@@ -16,7 +18,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
1618

1719
Future<AlbumResponseDto?> createSharedAlbum(
1820
String albumName,
19-
Set<AssetResponseDto> assets,
21+
Set<Asset> assets,
2022
List<String> sharedUserIds,
2123
) async {
2224
try {

mobile/lib/modules/album/services/album.service.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22

33
import 'package:flutter/foundation.dart';
44
import 'package:hooks_riverpod/hooks_riverpod.dart';
5+
import 'package:immich_mobile/shared/models/asset.dart';
56
import 'package:immich_mobile/shared/providers/api.provider.dart';
67
import 'package:immich_mobile/shared/services/api.service.dart';
78
import 'package:openapi/api.dart';
@@ -29,7 +30,7 @@ class AlbumService {
2930

3031
Future<AlbumResponseDto?> createAlbum(
3132
String albumName,
32-
Set<AssetResponseDto> assets,
33+
Iterable<Asset> assets,
3334
List<String> sharedUserIds,
3435
) async {
3536
try {
@@ -65,7 +66,7 @@ class AlbumService {
6566
}
6667

6768
Future<AlbumResponseDto?> createAlbumWithGeneratedName(
68-
Set<AssetResponseDto> assets,
69+
Iterable<Asset> assets,
6970
) async {
7071
return createAlbum(
7172
_getNextAlbumName(await getAlbums(isShared: false)), assets, []);
@@ -81,7 +82,7 @@ class AlbumService {
8182
}
8283

8384
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
84-
Set<AssetResponseDto> assets,
85+
Iterable<Asset> assets,
8586
String albumId,
8687
) async {
8788
try {

mobile/lib/modules/album/ui/album_viewer_thumbnail.dart

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import 'package:auto_route/auto_route.dart';
2-
import 'package:cached_network_image/cached_network_image.dart';
32
import 'package:flutter/material.dart';
4-
import 'package:hive_flutter/hive_flutter.dart';
53
import 'package:hooks_riverpod/hooks_riverpod.dart';
6-
import 'package:immich_mobile/constants/hive_box.dart';
74
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
85
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
96
import 'package:immich_mobile/routing/router.dart';
10-
import 'package:immich_mobile/utils/image_url_builder.dart';
11-
import 'package:openapi/api.dart';
7+
import 'package:immich_mobile/shared/models/asset.dart';
8+
import 'package:immich_mobile/shared/ui/immich_image.dart';
129

1310
class AlbumViewerThumbnail extends HookConsumerWidget {
14-
final AssetResponseDto asset;
15-
final List<AssetResponseDto> assetList;
11+
final Asset asset;
12+
final List<Asset> assetList;
1613
final bool showStorageIndicator;
1714

1815
const AlbumViewerThumbnail({
@@ -24,8 +21,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
2421

2522
@override
2623
Widget build(BuildContext context, WidgetRef ref) {
27-
var box = Hive.box(userInfoBox);
28-
var thumbnailRequestUrl = getThumbnailUrl(asset);
2924
var deviceId = ref.watch(authenticationProvider).deviceId;
3025
final selectedAssetsInAlbumViewer =
3126
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
@@ -120,27 +115,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
120115
_buildThumbnailImage() {
121116
return Container(
122117
decoration: BoxDecoration(border: drawBorderColor()),
123-
child: CachedNetworkImage(
124-
cacheKey: asset.id,
125-
width: 300,
126-
height: 300,
127-
memCacheHeight: 200,
128-
fit: BoxFit.cover,
129-
imageUrl: thumbnailRequestUrl,
130-
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
131-
fadeInDuration: const Duration(milliseconds: 250),
132-
progressIndicatorBuilder: (context, url, downloadProgress) =>
133-
Transform.scale(
134-
scale: 0.2,
135-
child: CircularProgressIndicator(value: downloadProgress.progress),
136-
),
137-
errorWidget: (context, url, error) {
138-
return Icon(
139-
Icons.image_not_supported_outlined,
140-
color: Theme.of(context).primaryColor,
141-
);
142-
},
143-
),
118+
child: ImmichImage(asset, width: 300, height: 300),
144119
);
145120
}
146121

@@ -167,7 +142,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
167142
children: [
168143
_buildThumbnailImage(),
169144
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
170-
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
145+
if (!asset.isImage) _buildVideoLabel(),
171146
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
172147
],
173148
),

mobile/lib/modules/album/ui/asset_grid_by_month.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'package:flutter/material.dart';
22
import 'package:hooks_riverpod/hooks_riverpod.dart';
33
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
4-
import 'package:openapi/api.dart';
4+
import 'package:immich_mobile/shared/models/asset.dart';
55

66
class AssetGridByMonth extends HookConsumerWidget {
7-
final List<AssetResponseDto> assetGroup;
7+
final List<Asset> assetGroup;
88
const AssetGridByMonth({Key? key, required this.assetGroup})
99
: super(key: key);
1010
@override

0 commit comments

Comments
 (0)