Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented delete asset on device and on database #22

Merged
merged 15 commits into from
Feb 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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