mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
Implemented delete asset on device and on database (#22)
* refactor serving file function asset service * Remove PhotoViewer for now since it creates a problem in 2.10 * Added error message for wrong decode file and logo for failed to load file * Fixed error when read stream cannot be created and crash server * Added method to get all assets as a raw array * Implemented cleaner way of grouping image * Implemented operation to delete assets in the database * Implemented delete on database operation * Implemented delete on device operation * Fixed issue display wrong information when the auto backup is enabled after deleting all assets
This commit is contained in:
parent
051c958c8b
commit
897d49f734
22 changed files with 518 additions and 10617 deletions
|
|
@ -0,0 +1,52 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class DeleteAssetResponse {
|
||||
final String id;
|
||||
final String status;
|
||||
|
||||
DeleteAssetResponse({
|
||||
required this.id,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
DeleteAssetResponse copyWith({
|
||||
String? id,
|
||||
String? status,
|
||||
}) {
|
||||
return DeleteAssetResponse(
|
||||
id: id ?? this.id,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
factory DeleteAssetResponse.fromMap(Map<String, dynamic> map) {
|
||||
return DeleteAssetResponse(
|
||||
id: map['id'] ?? '',
|
||||
status: map['status'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory DeleteAssetResponse.fromJson(String source) => DeleteAssetResponse.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is DeleteAssetResponse && other.id == id && other.status == status;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ status.hashCode;
|
||||
}
|
||||
|
|
@ -1,99 +1,72 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
|
||||
class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
||||
final AssetService _assetService = AssetService();
|
||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||
|
||||
AssetNotifier() : super([]);
|
||||
|
||||
late String? nextPageKey = "";
|
||||
bool isFetching = false;
|
||||
getAllAsset() async {
|
||||
List<ImmichAsset>? allAssets = await _assetService.getAllAsset();
|
||||
|
||||
// Get All assets
|
||||
getAllAssets() async {
|
||||
GetAllAssetResponse? res = await _assetService.getAllAsset();
|
||||
nextPageKey = res?.nextPageKey;
|
||||
|
||||
if (res != null) {
|
||||
for (var assets in res.data) {
|
||||
state = [...state, assets];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get Asset From The Past
|
||||
getOlderAsset() async {
|
||||
if (nextPageKey != null && !isFetching) {
|
||||
isFetching = true;
|
||||
GetAllAssetResponse? res = await _assetService.getOlderAsset(nextPageKey);
|
||||
|
||||
if (res != null) {
|
||||
nextPageKey = res.nextPageKey;
|
||||
|
||||
List<ImmichAssetGroupByDate> previousState = state;
|
||||
List<ImmichAssetGroupByDate> currentState = [];
|
||||
|
||||
for (var assets in res.data) {
|
||||
currentState = [...currentState, assets];
|
||||
}
|
||||
|
||||
if (previousState.last.date == currentState.first.date) {
|
||||
previousState.last.assets = [...previousState.last.assets, ...currentState.first.assets];
|
||||
state = [...previousState, ...currentState.sublist(1)];
|
||||
} else {
|
||||
state = [...previousState, ...currentState];
|
||||
}
|
||||
}
|
||||
|
||||
isFetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get newer asset from the current time
|
||||
getNewAsset() async {
|
||||
if (state.isNotEmpty) {
|
||||
var latestGroup = state.first;
|
||||
|
||||
// Sort the last asset group and put the lastest asset in front.
|
||||
latestGroup.assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||
var latestAsset = latestGroup.assets.first;
|
||||
var formatDateTemplate = 'y-MM-dd';
|
||||
var latestAssetDateText = DateFormat(formatDateTemplate).format(DateTime.parse(latestAsset.createdAt));
|
||||
|
||||
List<ImmichAsset> newAssets = await _assetService.getNewAsset(latestAsset.createdAt);
|
||||
|
||||
if (newAssets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Grouping by data
|
||||
var groupByDateList = groupBy<ImmichAsset, String>(
|
||||
newAssets, (asset) => DateFormat(formatDateTemplate).format(DateTime.parse(asset.createdAt)));
|
||||
|
||||
groupByDateList.forEach((groupDateInFormattedText, assets) {
|
||||
if (groupDateInFormattedText != latestAssetDateText) {
|
||||
ImmichAssetGroupByDate newGroup = ImmichAssetGroupByDate(assets: assets, date: groupDateInFormattedText);
|
||||
state = [newGroup, ...state];
|
||||
} else {
|
||||
latestGroup.assets.insertAll(0, assets);
|
||||
|
||||
state = [latestGroup, ...state.sublist(1)];
|
||||
}
|
||||
});
|
||||
if (allAssets != null) {
|
||||
allAssets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||
state = allAssets;
|
||||
}
|
||||
}
|
||||
|
||||
clearAllAsset() {
|
||||
state = [];
|
||||
}
|
||||
|
||||
deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||
var deviceId = deviceInfo["deviceId"];
|
||||
List<String> deleteIdList = [];
|
||||
// Delete asset from device
|
||||
for (var asset in deleteAssets) {
|
||||
// Delete asset on device if present
|
||||
if (asset.deviceId == deviceId) {
|
||||
AssetEntity? localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||
|
||||
if (localAsset != null) {
|
||||
deleteIdList.add(localAsset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
|
||||
print(result);
|
||||
|
||||
// Delete asset on server
|
||||
List<DeleteAssetResponse>? deleteAssetResult = await _assetService.deleteAssets(deleteAssets);
|
||||
if (deleteAssetResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var asset in deleteAssetResult) {
|
||||
if (asset.status == 'success') {
|
||||
state = state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final currentLocalPageProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAssetGroupByDate>>((ref) {
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
|
||||
return AssetNotifier();
|
||||
});
|
||||
|
||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||
var assetGroup = ref.watch(assetProvider);
|
||||
|
||||
return assetGroup.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||
|
|
@ -9,7 +10,20 @@ import 'package:immich_mobile/shared/services/network.service.dart';
|
|||
class AssetService {
|
||||
final NetworkService _networkService = NetworkService();
|
||||
|
||||
Future<GetAllAssetResponse?> getAllAsset() async {
|
||||
Future<List<ImmichAsset>?> getAllAsset() async {
|
||||
var res = await _networkService.getRequest(url: "asset/");
|
||||
try {
|
||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
||||
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint("Error getAllAsset ${e.toString()}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<GetAllAssetResponse?> getAllAssetWithPagination() async {
|
||||
var res = await _networkService.getRequest(url: "asset/all");
|
||||
try {
|
||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
|
@ -69,7 +83,27 @@ class AssetService {
|
|||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
||||
ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData);
|
||||
print("result $result");
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint("Error getAllAsset ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DeleteAssetResponse>?> deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
||||
try {
|
||||
var payload = [];
|
||||
|
||||
for (var asset in deleteAssets) {
|
||||
payload.add(asset.id);
|
||||
}
|
||||
|
||||
var res = await _networkService.deleteRequest(url: "asset/", data: {"ids": payload});
|
||||
|
||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||
|
||||
List<DeleteAssetResponse> result = List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint("Error getAllAsset ${e.toString()}");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||
|
||||
class DeleteDialog extends StatelessWidget {
|
||||
class DeleteDialog extends ConsumerWidget {
|
||||
const DeleteDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final homePageState = ref.watch(homePageStateProvider);
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.grey[200],
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
|
|
@ -21,7 +26,12 @@ class DeleteDialog extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
ref.watch(assetProvider.notifier).deleteAssets(homePageState.selectedItems);
|
||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
"Delete",
|
||||
style: TextStyle(color: Colors.red[400]),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:badges/badges.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||
|
||||
class ProfileDrawer extends ConsumerWidget {
|
||||
const ProfileDrawer({Key? key}) : super(key: key);
|
||||
|
|
@ -57,6 +58,7 @@ class ProfileDrawer extends ConsumerWidget {
|
|||
bool res = await ref.read(authenticationProvider.notifier).logout();
|
||||
|
||||
if (res) {
|
||||
ref.watch(backupProvider.notifier).cancelBackup();
|
||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||
AutoRouter.of(context).popUntilRoot();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||
|
||||
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||
|
||||
Widget _buildSelectionIcon(ImmichAsset asset) {
|
||||
if (selectedAsset.contains(asset)) {
|
||||
|
|
@ -42,6 +44,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint("View ${asset.id}");
|
||||
if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
|
||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||
} else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
|
||||
|
|
@ -99,9 +102,10 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
debugPrint("Error Loading Thumbnail Widget $error");
|
||||
cacheKey.value += 1;
|
||||
return const Icon(Icons.error);
|
||||
return Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -116,6 +120,15 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||
)
|
||||
: Container(),
|
||||
),
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
|||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
|
|
@ -20,76 +19,51 @@ class HomePage extends HookConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ScrollController _scrollController = useScrollController();
|
||||
List<ImmichAssetGroupByDate> _assetGroup = ref.watch(assetProvider);
|
||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
||||
List<Widget> _imageGridGroup = [];
|
||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||
var homePageState = ref.watch(homePageStateProvider);
|
||||
|
||||
_scrollControllerCallback() {
|
||||
var endOfPage = _scrollController.position.maxScrollExtent;
|
||||
|
||||
if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) {
|
||||
ref.read(assetProvider.notifier).getOlderAsset();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
ref.read(assetProvider.notifier).getAllAssets();
|
||||
|
||||
_scrollController.addListener(_scrollControllerCallback);
|
||||
return () {
|
||||
_scrollController.removeListener(_scrollControllerCallback);
|
||||
};
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
onPopBackFromBackupPage() {
|
||||
ref.read(assetProvider.notifier).getNewAsset();
|
||||
// Remove and force getting new widget again if there is not many widget on screen.
|
||||
// Otherwise do nothing.
|
||||
|
||||
if (_imageGridGroup.isNotEmpty && _imageGridGroup.length < 20) {
|
||||
ref.read(assetProvider.notifier).getOlderAsset();
|
||||
} else if (_imageGridGroup.isEmpty) {
|
||||
ref.read(assetProvider.notifier).getAllAssets();
|
||||
}
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_assetGroup.isNotEmpty) {
|
||||
String lastGroupDate = _assetGroup[0].date;
|
||||
if (assetGroupByDateTime.isNotEmpty) {
|
||||
int? lastMonth;
|
||||
|
||||
for (var group in _assetGroup) {
|
||||
var dateTitle = group.date;
|
||||
var assetGroup = group.assets;
|
||||
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
|
||||
DateTime parseDateGroup = DateTime.parse(dateGroup);
|
||||
int currentMonth = parseDateGroup.month;
|
||||
|
||||
int? currentMonth = DateTime.tryParse(dateTitle)?.month;
|
||||
int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
|
||||
|
||||
// Add Monthly Title Group if started at the beginning of the month
|
||||
|
||||
if (currentMonth != null && previousMonth != null) {
|
||||
if ((currentMonth - previousMonth) != 0) {
|
||||
if (lastMonth != null) {
|
||||
if (currentMonth - lastMonth! != 0) {
|
||||
_imageGridGroup.add(
|
||||
MonthlyTitleText(isoDate: dateTitle),
|
||||
MonthlyTitleText(
|
||||
isoDate: dateGroup,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Daily Title Group
|
||||
_imageGridGroup.add(
|
||||
DailyTitleText(
|
||||
isoDate: dateTitle,
|
||||
assetGroup: assetGroup,
|
||||
isoDate: dateGroup,
|
||||
assetGroup: immichAssetList,
|
||||
),
|
||||
);
|
||||
|
||||
// Add Image Group
|
||||
_imageGridGroup.add(
|
||||
ImageGrid(assetGroup: assetGroup),
|
||||
ImageGrid(assetGroup: immichAssetList),
|
||||
);
|
||||
//
|
||||
lastGroupDate = dateTitle;
|
||||
}
|
||||
|
||||
lastMonth = currentMonth;
|
||||
});
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue