fix(mobile): handle asset trash, restore and delete ws events (#4482)
* server: add ASSET_RESTORE ws event * mobile: handle ASSET_TRASH, ASSET_RESTORE and ASSET_DELETE ws events --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
634169235a
commit
a78e08bac1
7 changed files with 88 additions and 27 deletions
|
@ -226,6 +226,7 @@
|
|||
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
|
||||
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
|
||||
"profile_drawer_app_logs": "Logs",
|
||||
"profile_drawer_trash": "Trash",
|
||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||
"profile_drawer_settings": "Settings",
|
||||
"profile_drawer_sign_out": "Sign Out",
|
||||
|
|
|
@ -106,7 +106,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
title: Text(
|
||||
"Trash",
|
||||
"profile_drawer_trash",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
|
|||
import 'package:immich_mobile/modules/trash/services/trash.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
@ -42,12 +41,6 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
await _db.exifInfos.deleteAll(dbIds);
|
||||
await _db.assets.deleteAll(dbIds);
|
||||
});
|
||||
|
||||
// Refresh assets in background
|
||||
Future.delayed(
|
||||
const Duration(seconds: 4),
|
||||
() async => await _ref.read(assetProvider.notifier).getAllAsset(),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
}
|
||||
|
@ -68,12 +61,6 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updatedAssets);
|
||||
});
|
||||
|
||||
// Refresh assets in background
|
||||
Future.delayed(
|
||||
const Duration(seconds: 4),
|
||||
() async => await _ref.read(assetProvider.notifier).getAllAsset(),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
|
@ -106,12 +93,6 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updatedAssets);
|
||||
});
|
||||
|
||||
// Refresh assets in background
|
||||
Future.delayed(
|
||||
const Duration(seconds: 4),
|
||||
() async => await _ref.read(assetProvider.notifier).getAllAsset(),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
}
|
||||
|
|
|
@ -7,26 +7,43 @@ import 'package:immich_mobile/shared/models/asset.dart';
|
|||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart';
|
||||
|
||||
enum PendingAction {
|
||||
assetDelete,
|
||||
}
|
||||
|
||||
class PendingChange {
|
||||
final PendingAction action;
|
||||
final dynamic value;
|
||||
|
||||
const PendingChange(this.action, this.value);
|
||||
}
|
||||
|
||||
class WebsocketState {
|
||||
final Socket? socket;
|
||||
final bool isConnected;
|
||||
final List<PendingChange> pendingChanges;
|
||||
|
||||
WebsocketState({
|
||||
this.socket,
|
||||
required this.isConnected,
|
||||
required this.pendingChanges,
|
||||
});
|
||||
|
||||
WebsocketState copyWith({
|
||||
Socket? socket,
|
||||
bool? isConnected,
|
||||
List<PendingChange>? pendingChanges,
|
||||
}) {
|
||||
return WebsocketState(
|
||||
socket: socket ?? this.socket,
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
pendingChanges: pendingChanges ?? this.pendingChanges,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -49,10 +66,17 @@ class WebsocketState {
|
|||
|
||||
class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
WebsocketNotifier(this.ref)
|
||||
: super(WebsocketState(socket: null, isConnected: false));
|
||||
: super(
|
||||
WebsocketState(socket: null, isConnected: false, pendingChanges: []),
|
||||
) {
|
||||
debounce = Debounce(
|
||||
const Duration(milliseconds: 500),
|
||||
);
|
||||
}
|
||||
|
||||
final log = Logger('WebsocketNotifier');
|
||||
final Ref ref;
|
||||
late final Debounce debounce;
|
||||
|
||||
connect() {
|
||||
var authenticationState = ref.read(authenticationProvider);
|
||||
|
@ -79,21 +103,36 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
|
||||
socket.onConnect((_) {
|
||||
debugPrint("Established Websocket Connection");
|
||||
state = WebsocketState(isConnected: true, socket: socket);
|
||||
state = WebsocketState(
|
||||
isConnected: true,
|
||||
socket: socket,
|
||||
pendingChanges: state.pendingChanges,
|
||||
);
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
debugPrint("Disconnect to Websocket Connection");
|
||||
state = WebsocketState(isConnected: false, socket: null);
|
||||
state = WebsocketState(
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
pendingChanges: state.pendingChanges,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('error', (errorMessage) {
|
||||
log.severe("Websocket Error - $errorMessage");
|
||||
state = WebsocketState(isConnected: false, socket: null);
|
||||
state = WebsocketState(
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
pendingChanges: state.pendingChanges,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||
socket.on('on_asset_trash', _handleServerUpdates);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
} catch (e) {
|
||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||
}
|
||||
|
@ -106,7 +145,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
var socket = state.socket?.disconnect();
|
||||
|
||||
if (socket?.disconnected == true) {
|
||||
state = WebsocketState(isConnected: false, socket: null);
|
||||
state = WebsocketState(
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
pendingChanges: state.pendingChanges,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,6 +163,29 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
|
||||
}
|
||||
|
||||
addPendingChange(PendingAction action, dynamic value) {
|
||||
state = state.copyWith(
|
||||
pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
|
||||
);
|
||||
}
|
||||
|
||||
handlePendingChanges() {
|
||||
final deleteChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetDelete)
|
||||
.toList();
|
||||
if (deleteChanges.isNotEmpty) {
|
||||
List<String> remoteIds = deleteChanges
|
||||
.map((a) => jsonDecode(a.value.toString()).toString())
|
||||
.toList();
|
||||
ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.where((c) => c.action != PendingAction.assetDelete)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_handleOnUploadSuccess(dynamic data) {
|
||||
final jsonString = jsonDecode(data.toString());
|
||||
final dto = AssetResponseDto.fromJson(jsonString);
|
||||
|
@ -133,6 +199,16 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||
ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||
ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||
}
|
||||
|
||||
// Refresh updated assets
|
||||
_handleServerUpdates(dynamic data) {
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
_handleOnAssetDelete(dynamic data) {
|
||||
addPendingChange(PendingAction.assetDelete, data);
|
||||
debounce(handlePendingChanges);
|
||||
}
|
||||
}
|
||||
|
||||
final websocketProvider =
|
||||
|
|
|
@ -153,7 +153,7 @@ class SyncService {
|
|||
if (toUpsert == null || toDelete == null) return null;
|
||||
try {
|
||||
if (toDelete.isNotEmpty) {
|
||||
await _handleRemoteAssetRemoval(toDelete);
|
||||
await handleRemoteAssetRemoval(toDelete);
|
||||
}
|
||||
if (toUpsert.isNotEmpty) {
|
||||
final (_, updated) = await _linkWithExistingFromDb(toUpsert);
|
||||
|
@ -171,7 +171,7 @@ class SyncService {
|
|||
}
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> _handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
return _db.writeTxn(() async {
|
||||
await _db.assets.remote(idsToDelete).filter().localIdIsNull().deleteAll();
|
||||
final onlyLocal = await _db.assets.remote(idsToDelete).findAll();
|
||||
|
|
|
@ -431,6 +431,7 @@ export class AssetService {
|
|||
const ids = assets.map((a) => a.id);
|
||||
await this.assetRepository.restoreAll(ids);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -450,6 +451,7 @@ export class AssetService {
|
|||
await this.access.requirePermission(authUser, Permission.ASSET_RESTORE, ids);
|
||||
await this.assetRepository.restoreAll(ids);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
|
||||
}
|
||||
|
||||
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
|
||||
|
|
|
@ -4,6 +4,7 @@ export enum CommunicationEvent {
|
|||
UPLOAD_SUCCESS = 'on_upload_success',
|
||||
ASSET_DELETE = 'on_asset_delete',
|
||||
ASSET_TRASH = 'on_asset_trash',
|
||||
ASSET_RESTORE = 'on_asset_restore',
|
||||
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
||||
SERVER_VERSION = 'on_server_version',
|
||||
CONFIG_UPDATE = 'on_config_update',
|
||||
|
|
Loading…
Reference in a new issue