Skip to content

Commit

Permalink
Implemented delete asset on device and on database (#22)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alextran1502 authored Feb 13, 2022
1 parent 051c958 commit 897d49f
Show file tree
Hide file tree
Showing 22 changed files with 511 additions and 10,610 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,18 @@ You can use docker compose for development, there are several services that comp

Navigate to `server` directory and run

```
````
cp .env.example .env
```
Then populate the value in there.
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned the user that run the `docker-compose` command below.
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
To start, run
```bash
docker-compose -f ./server/docker-compose.yml up
```
````

To force rebuild node modules after installing new packages

Expand Down
38 changes: 31 additions & 7 deletions mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Expand All @@ -10,7 +9,6 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.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:photo_view/photo_view.dart';

// ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget {
Expand All @@ -35,6 +33,7 @@ class ImageViewerPage extends HookConsumerWidget {

useEffect(() {
getAssetExif();
return null;
}, []);

return Scaffold(
Expand All @@ -60,12 +59,34 @@ class ImageViewerPage extends HookConsumerWidget {
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
errorWidget: (context, url, error) => const Icon(Icons.error),
imageBuilder: (context, imageProvider) {
return PhotoView(imageProvider: imageProvider);
},
errorWidget: (context, url, error) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Wrap(
spacing: 32,
runSpacing: 32,
alignment: WrapAlignment.center,
children: [
const Text(
"Failed To Render Image - Possibly Corrupted Data",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SingleChildScrollView(
child: Text(
error.toString(),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
),
),
],
),
),
// imageBuilder: (context, imageProvider) {
// return PhotoView(imageProvider: imageProvider);
// },
placeholder: (context, url) {
return CachedNetworkImage(
cacheKey: thumbnailUrl,
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
Expand All @@ -74,7 +95,10 @@ class ImageViewerPage extends HookConsumerWidget {
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
errorWidget: (context, url, error) => Icon(
Icons.error,
color: Colors.grey[300],
),
);
},
),
Expand Down
52 changes: 52 additions & 0 deletions mobile/lib/modules/home/models/delete_asset_response.model.dart
Original file line number Diff line number Diff line change
@@ -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;
}
113 changes: 43 additions & 70 deletions mobile/lib/modules/home/providers/asset.provider.dart
Original file line number Diff line number Diff line change
@@ -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];
}
if (allAssets != null) {
allAssets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
state = allAssets;
}
}

// 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];
}
clearAllAsset() {
state = [];
}

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];
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);
}
}

isFetching = false;
}
}

// Get newer asset from the current time
getNewAsset() async {
if (state.isNotEmpty) {
var latestGroup = state.first;
final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
print(result);

// 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);
// Delete asset on server
List<DeleteAssetResponse>? deleteAssetResult = await _assetService.deleteAssets(deleteAssets);
if (deleteAssetResult == null) {
return;
}

if (newAssets.isEmpty) {
return;
for (var asset in deleteAssetResult) {
if (asset.status == 'success') {
state = state.where((immichAsset) => immichAsset.id != asset.id).toList();
}

// 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)];
}
});
}
}

clearAllAsset() {
state = [];
}
}

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)));
});
40 changes: 37 additions & 3 deletions mobile/lib/modules/home/services/asset.service.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
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';

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());
Expand Down Expand Up @@ -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()}");
Expand Down
16 changes: 13 additions & 3 deletions mobile/lib/modules/home/ui/delete_diaglog.dart
Original file line number Diff line number Diff line change
@@ -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)),
Expand All @@ -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]),
Expand Down
1 change: 0 additions & 1 deletion mobile/lib/modules/home/ui/immich_sliver_appbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 897d49f

Please sign in to comment.