import 'dart:async'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; mixin TrashMixin on SourceBase { static const Duration binKeepDuration = Duration(days: 30); Future loadTrashDetails() async { final saved = await metadataDb.loadAllTrashDetails(); final idMap = entryById; saved.forEach((details) => idMap[details.id]?.trashDetails = details); } Future> deleteExpiredTrash() async { final expiredEntries = trashedEntries.where((entry) => entry.isExpiredTrash).toSet(); if (expiredEntries.isEmpty) return {}; final processed = {}; final completer = Completer>(); mediaEditService.delete(entries: expiredEntries).listen( processed.add, onError: completer.completeError, onDone: () async { final successOps = processed.where((e) => e.success).toSet(); final deletedOps = successOps.where((e) => !e.skipped).toSet(); final deletedUris = deletedOps.map((event) => event.uri).toSet(); completer.complete(deletedUris); }, ); return await completer.future; } Future> recoverUntrackedTrashItems() async { final newEntries = {}; final knownPaths = allEntries.map((v) => v.trashDetails?.path).whereNotNull().toSet(); final untrackedPaths = await storageService.getUntrackedTrashPaths(knownPaths); if (untrackedPaths.isNotEmpty) { debugPrint('Recovering ${untrackedPaths.length} untracked bin items'); final recoveryPath = pContext.join(androidFileUtils.picturesPath, AndroidFileUtils.recoveryDir); await Future.forEach(untrackedPaths, (untrackedPath) async { TrashDetails _buildTrashDetails(int id) => TrashDetails( id: id, path: untrackedPath, dateMillis: DateTime.now().millisecondsSinceEpoch, ); final uri = Uri.file(untrackedPath).toString(); final entry = allEntries.firstWhereOrNull((v) => v.uri == uri); if (entry != null) { // there is already a matching entry // but missing trash details, and possibly not marked as trash final id = entry.id; entry.contentId = null; entry.trashed = true; entry.trashDetails = _buildTrashDetails(id); // persist await metadataDb.updateEntry(id, entry); await metadataDb.updateTrash(id, entry.trashDetails); } else { // there is no matching entry final sourceEntry = await mediaFetchService.getEntry(uri, null); if (sourceEntry != null) { final id = metadataDb.nextId; sourceEntry.id = id; sourceEntry.path = pContext.join(recoveryPath, pContext.basename(untrackedPath)); sourceEntry.trashed = true; sourceEntry.trashDetails = _buildTrashDetails(id); newEntries.add(sourceEntry); } else { await reportService.recordError('Failed to recover untracked bin item at uri=$uri', null); } } }); } return newEntries; } }