chore(mobile): remove unused code (#31280)

* chore: remove unused methods

* Remove dead test group

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Adam Gastineau <adam@agg.im>
This commit is contained in:
authored and GitHub committed 2026-09-08 13:56:46 +00:00
1 parent 1dedbe4c39
commit 8d90cbeafc
65 files changed
+7 -1195

No files matched your search

-2
View File
@@ -45,7 +45,6 @@ import 'package:immich_mobile/infrastructure/repositories/partner.repository.dar
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
@@ -101,7 +100,6 @@ import 'package:sqlite_async/sqlite_async.dart';
RemoteAlbumRepository,
RemoteAssetRepository,
RemoteExifRepository,
StackRepository,
StoreRepository,
SyncMigrationRepository,
SyncStreamRepository,
@@ -22,16 +22,4 @@ class LocalAlbumService {
Future<int> getCount() {
return _repository.getCount();
}
Future<void> unlinkRemoteAlbum(String id) async {
return _repository.unlinkRemoteAlbum(id);
}
Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
return _repository.linkRemoteAlbum(localAlbumId, remoteAlbumId);
}
Future<List<LocalAlbum>> getBackupAlbums() {
return _repository.getBackupAlbums();
}
}
@@ -4,7 +4,6 @@ import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' hide AssetVisibility, SearchFilter;
class SearchService {
final _log = Logger("SearchService");
@@ -12,27 +11,6 @@ class SearchService {
SearchService(this._searchApiRepository);
Future<List<String>?> getSearchSuggestions(
SearchSuggestionType type, {
String? country,
String? state,
String? make,
String? model,
}) async {
try {
return await _searchApiRepository.getSearchSuggestions(
type,
country: country,
state: state,
make: make,
model: model,
);
} catch (e) {
_log.warning("Failed to get search suggestions", e);
}
return [];
}
Future<SearchResult?> search(SearchFilter filter, int page) async {
try {
final response = await _searchApiRepository.search(filter, page);
@@ -12,8 +12,4 @@ class SettingsService {
const SettingsService({required this._storeService});
T get<T>(Setting<T> setting) => _storeService.get(setting.storeKey, setting.defaultValue);
Future<void> set<T>(Setting<T> setting, T value) => _storeService.put(setting.storeKey, value);
Stream<T> watch<T>(Setting<T> setting) => _storeService.watch(setting.storeKey).map((v) => v ?? setting.defaultValue);
}
@@ -194,15 +194,6 @@ class TimelineService {
BaseAsset getRandomAsset() => _buffer.elementAt(math.Random().nextInt(_buffer.length));
BaseAsset getAsset(int index) {
if (!hasRange(index, 1)) {
throw RangeError(
'TimelineService::getAsset Index $index not in buffer range [$_bufferOffset, ${_bufferOffset + _buffer.length})',
);
}
return _buffer.elementAt(index - _bufferOffset);
}
/// Gets an asset at the given index, automatically loading the buffer if needed.
/// This is an async version that can handle out-of-range indices by loading the appropriate buffer.
Future<BaseAsset?> getAssetAsync(int index) async {
@@ -30,9 +30,4 @@ class EventStream {
}) {
return where<T>().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
/// Closes the stream controller
Future<void> dispose() {
return _controller.close();
}
}
@@ -1,10 +0,0 @@
import 'dart:ui';
import 'package:flutter/painting.dart';
extension CodecImageInfoExtension on Codec {
Future<ImageInfo> getImageInfo({double scale = 1.0}) async {
final frame = await getNextFrame();
return ImageInfo(image: frame.image, scale: scale);
}
}
@@ -1,45 +0,0 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
extension ListExtension<E> on List<E> {
List<E> uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) {
compare ??= (E a, E b) => a == b ? 0 : 1;
int i = 1;
int j = 1;
for (; i < length; i++) {
if (compare(this[i - 1], this[i]) != 0) {
if (i != j) {
this[j] = this[i];
}
j++;
} else if (onDuplicate != null) {
onDuplicate(this[i - 1], this[i]);
}
}
length = length == 0 ? 0 : j;
return this;
}
ListSlice<E> nestedSlice(int start, int end) {
if (this is ListSlice) {
final ListSlice<E> self = this as ListSlice<E>;
return ListSlice<E>(self.source, self.start + start, self.start + end);
}
return ListSlice<E>(this, start, end);
}
}
extension IntListExtension on Iterable<int> {
Int64List toInt64List() {
final list = Int64List(length);
list.setAll(0, this);
return list;
}
}
extension SortedByProperty<T> on Iterable<T> {
Iterable<T> sortedByField(Comparable Function(T e) key) {
return sorted((a, b) => key(a).compareTo(key(b)));
}
}
@@ -1,20 +0,0 @@
import 'package:maplibre_gl/maplibre_gl.dart';
extension WithinBounds on LatLngBounds {
/// Checks whether [point] is inside bounds
bool contains(LatLng point) {
final sw = point;
final ne = point;
return containsBounds(LatLngBounds(southwest: sw, northeast: ne));
}
/// Checks whether [bounds] is contained inside bounds
bool containsBounds(LatLngBounds bounds) {
final sw = bounds.southwest;
final ne = bounds.northeast;
return (sw.latitude >= southwest.latitude) &&
(ne.latitude <= northeast.latitude) &&
(sw.longitude >= southwest.longitude) &&
(ne.longitude <= northeast.longitude);
}
}
@@ -1,71 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
extension MapMarkers on MapLibreMapController {
static var _completer = Completer()..complete();
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
return addSource(
MapUtils.defaultSourceId,
GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())),
);
}
Future<void> reloadAllLayersForMarkers(List<MapMarker> markers) async {
// Wait for previous reload to complete
if (!_completer.isCompleted) {
return _completer.future;
}
_completer = Completer();
// !! Make sure to remove layers before sources else the native
// maplibre library would crash when removing the source saying that
// the source is still in use
final existingLayers = await getLayerIds();
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
await removeLayer(MapUtils.defaultHeatMapLayerId);
}
final existingSources = await getSourceIds();
if (existingSources.contains(MapUtils.defaultSourceId)) {
await removeSource(MapUtils.defaultSourceId);
}
await addGeoJSONSourceForMarkers(markers);
if (Platform.isAndroid) {
await addCircleLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
const CircleLayerProperties(
circleRadius: 10,
circleColor: "rgba(150,86,34,0.7)",
circleBlur: 1.0,
circleOpacity: 0.7,
circleStrokeWidth: 0.1,
circleStrokeColor: "rgba(203,46,19,0.5)",
circleStrokeOpacity: 0.7,
),
);
}
if (Platform.isIOS) {
await addHeatmapLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultHeatMapLayerProperties,
);
}
_completer.complete();
}
Future<Symbol?> addMarkerAtLatLng(LatLng centre) async {
// no marker is displayed if asset-path is incorrect
try {
@@ -78,14 +16,4 @@ extension MapMarkers on MapLibreMapController {
// no-op
}
}
Future<LatLngBounds> getBoundsFromPoint(Point<double> point, double distance) async {
final southWestPx = Point(point.x - distance, point.y + distance);
final northEastPx = Point(point.x + distance, point.y - distance);
final southWest = await toLatLng(southWestPx);
final northEast = await toLatLng(northEastPx);
return LatLngBounds(southwest: southWest, northeast: northEast);
}
}
@@ -1,5 +0,0 @@
import 'package:http/http.dart';
extension LoggerExtension on Response {
String toLoggerString() => "Status: $statusCode $reasonPhrase\n\n$body";
}
@@ -3,10 +3,6 @@ import 'dart:convert';
import 'package:diacritic/diacritic.dart' as diacritic;
extension StringExtension on String {
String capitalize() {
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
}
String? get nullIfEmpty => isEmpty ? null : this;
String removeDiacritics() => diacritic.removeDiacritics(this);
@@ -218,29 +218,4 @@ class LocalAssetRepository extends DatabaseAccessor<Drift> with $LocalAssetRepos
return RemovalCandidatesResult(assets: assets, totalBytes: totalBytes);
}
Future<List<LocalAsset>> getEmptyCloudIdAssets() {
final query = _db.localAssetEntity.select()..where((row) => row.iCloudId.isNull());
return query.map((row) => row.toDto()).get();
}
Future<void> reconcileHashesFromCloudId() async {
await _db.customUpdate(
'''
UPDATE local_asset_entity
SET checksum = remote_asset_entity.checksum
FROM remote_asset_cloud_id_entity
INNER JOIN remote_asset_entity
ON remote_asset_cloud_id_entity.asset_id = remote_asset_entity.id
WHERE local_asset_entity.i_cloud_id = remote_asset_cloud_id_entity.cloud_id
AND local_asset_entity.checksum IS NULL
AND remote_asset_cloud_id_entity.adjustment_time IS local_asset_entity.adjustment_time
AND remote_asset_cloud_id_entity.latitude IS local_asset_entity.latitude
AND remote_asset_cloud_id_entity.longitude IS local_asset_entity.longitude
AND remote_asset_cloud_id_entity.created_at IS local_asset_entity.created_at
''',
updates: {_db.localAssetEntity},
updateKind: UpdateKind.update,
);
}
}
@@ -52,18 +52,6 @@ class LogRepository {
return true;
}
Future<void> deleteByLogger(String logger) async {
await _db.logMessageEntity.deleteWhere((row) => row.logger.equals(logger));
}
Stream<List<LogMessage>> watchMessages(String logger) {
final query = _db.logMessageEntity.select()
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..where((row) => row.logger.equals(logger));
return query.watch().map((rows) => rows.map((row) => row.toDto()).toList());
}
Future<void> truncate({int limit = kLogTruncateLimit}) async {
final totalCount = await _db.managers.logMessageEntity.count();
if (totalCount > limit) {
@@ -12,7 +12,6 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.drift.dart';
import 'package:immich_mobile/utils/option.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@DriftAccessor()
class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRepositoryMixin {
@@ -20,21 +19,6 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
Drift get _db => attachedDatabase;
/// For testing purposes
Future<List<RemoteAsset>> getSome(String userId) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(10);
return query.map((row) => row.toDto()).get();
}
SingleOrNullSelectable<RemoteAsset?> _assetSelectable(String id) {
final query =
_db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
@@ -119,18 +103,6 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
}).get();
}
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(visibility: Value(visibility)),
where: (e) => e.id.equals(id),
);
}
});
}
Future<void> trash(List<String> ids) {
return _db.batch((batch) async {
for (final id in ids) {
@@ -267,37 +239,4 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
}
});
}
// TODO(shenlong): remove after action migration
Future<void> updateLocation(List<String> ids, LatLng location) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteExifEntity,
RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)),
where: (e) => e.assetId.equals(id),
);
}
});
}
Future<void> updateDateTime(List<String> ids, DateTime dateTime, {String? timeZone}) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteExifEntity,
RemoteExifEntityCompanion(
dateTimeOriginal: Value(dateTime),
timeZone: timeZone == null ? const Value.absent() : Value(timeZone),
),
where: (e) => e.assetId.equals(id),
);
batch.update(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(createdAt: Value(dateTime)),
where: (e) => e.id.equals(id),
);
}
});
}
}
@@ -1,26 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/data/db/main/database.dart';
import 'package:immich_mobile/data/db/main/table/remote/stack.drift.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/infrastructure/repositories/stack.repository.drift.dart';
@DriftAccessor()
class StackRepository extends DatabaseAccessor<Drift> with $StackRepositoryMixin {
StackRepository(super.attachedDatabase);
Drift get _db => attachedDatabase;
Future<List<Stack>> getAll(String userId) {
final query = _db.stackEntity.select()..where((e) => e.ownerId.equals(userId));
return query.map((stack) {
return stack.toDto();
}).get();
}
}
extension on StackEntityData {
Stack toDto() {
return Stack(id: id, createdAt: createdAt, updatedAt: updatedAt, ownerId: ownerId, primaryAssetId: primaryAssetId);
}
}
@@ -900,40 +900,6 @@ class SyncStreamRepository extends DatabaseAccessor<Drift> with $SyncStreamRepos
rethrow;
}
}
Future<void> pruneAssets() async {
try {
await _db.transaction(() async {
final authQuery = _db.authUserEntity.selectOnly()
..addColumns([_db.authUserEntity.id])
..limit(1);
final currentUserId = await authQuery.map((row) => row.read(_db.authUserEntity.id)).getSingleOrNull();
if (currentUserId == null) {
_logger.warning('No authenticated user found during pruneAssets. Skipping asset pruning.');
return;
}
final partnerQuery = _db.partnerEntity.selectOnly()
..addColumns([_db.partnerEntity.sharedById])
..where(_db.partnerEntity.sharedWithId.equals(currentUserId));
final partnerIds = await partnerQuery.map((row) => row.read(_db.partnerEntity.sharedById)).get();
final validUsers = {currentUserId, ...partnerIds.nonNulls};
// Asset is not owned by the current user or any of their partners and is not part of any (shared) album
// Likely a stale asset that was previously shared but has been removed
await _db.remoteAssetEntity.deleteWhere((asset) {
return asset.ownerId.isNotIn(validUsers) &
asset.id.isNotInQuery(
_db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]),
);
});
});
} catch (error, stack) {
_logger.severe('Error: pruneAssets', error, stack);
// We do not rethrow here as this is a client-only cleanup and should not affect the sync process
}
}
}
extension on AssetTypeEnum {
@@ -23,9 +23,4 @@ class UserApiRepository extends ApiRepository {
final res = await checkNull(_api.createProfileImage(MultipartFile.fromBytes('file', data, filename: name)));
return res.profileImagePath;
}
Future<List<UserDto>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(UserConverter.fromSimpleUserDto).toList();
}
}
@@ -1,3 +0,0 @@
abstract interface class IDatabaseRepository {
Future<T> transaction<T>(Future<T> Function() callback);
}
@@ -83,10 +83,6 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
unawaited(filterAlbums());
}
Future<void> onRefresh() async {
await ref.read(remoteAlbumProvider.notifier).refresh();
}
void toggleViewMode() {
setState(() {
isGrid = !isGrid;
@@ -11,10 +11,6 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAs
return ref.watch(assetServiceProvider).getStack(asset);
}
void setStack(List<RemoteAsset> stack) {
state = AsyncData(stack);
}
}
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
@@ -10,7 +10,6 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:immich_mobile/utils/option.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
part 'map.state.freezed.dart';
@@ -98,17 +97,6 @@ class MapStateNotifier extends Notifier<MapState> {
EventStream.shared.emit(const MapMarkerReloadEvent());
}
Option<DateTime> parseDateOption(String s) {
try {
if (s.trim().isEmpty) {
return const Option.none();
}
return Option.some(DateTime.parse(s));
} catch (_) {
return const Option.none();
}
}
@override
MapState build() {
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
@@ -1,15 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
class AlbumTitleNotifier extends StateNotifier<String> {
AlbumTitleNotifier() : super("");
void setAlbumTitle(String title) {
state = title;
}
void clearAlbumTitle() {
state = "";
}
}
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>((ref) => AlbumTitleNotifier());
@@ -31,10 +31,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
AppLifeCycleNotifier(this._ref) : super(AppLifeCycleEnum.active);
AppLifeCycleEnum getAppState() {
return state;
}
Future<void> handleAppResume() async {
state = AppLifeCycleEnum.resumed;
_log.info("App resumed");
@@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
final scrollToTopNotifierProvider = ScrollNotifier();
class ScrollNotifier with ChangeNotifier {
void scrollToTop() {
notifyListeners();
}
}
@@ -1,14 +0,0 @@
import 'package:flutter/material.dart';
final scrollToDateNotifierProvider = ScrollToDateNotifier(null);
class ScrollToDateNotifier extends ValueNotifier<DateTime?> {
ScrollToDateNotifier(super.value);
void scrollToDate(DateTime date) {
value = date;
// Manually notify listeners to trigger the scroll, even if the value hasn't changed
notifyListeners();
}
}
@@ -14,8 +14,4 @@ class ShowControls extends StateNotifier<bool> {
set show(bool value) {
state = value;
}
void toggle() {
state = !state;
}
}
@@ -87,11 +87,6 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
_persistExcludedAlbumIds(newKeepAlbumIds);
}
void setExcludedAlbumIds(Set<String> albumIds) {
state = state.copyWith(keepAlbumIds: albumIds, assetsToDelete: []);
_persistExcludedAlbumIds(albumIds);
}
void _persistExcludedAlbumIds(Set<String> albumIds) {
unawaited(_settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList()));
}
@@ -10,7 +10,6 @@ final hapticFeedbackProvider = StateNotifierProvider<HapticNotifier, void>((ref)
});
class HapticNotifier extends StateNotifier<void> {
void build() {}
final Ref _ref;
HapticNotifier(this._ref) : super(null);
@@ -21,12 +20,6 @@ class HapticNotifier extends StateNotifier<void> {
}
}
void lightImpact() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) {
unawaited(HapticFeedback.lightImpact());
}
}
void mediumImpact() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) {
unawaited(HapticFeedback.mediumImpact());
@@ -38,10 +31,4 @@ class HapticNotifier extends StateNotifier<void> {
unawaited(HapticFeedback.heavyImpact());
}
}
void vibrate() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) {
unawaited(HapticFeedback.vibrate());
}
}
}
@@ -2,8 +2,6 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
@@ -13,7 +11,6 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/action.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:logging/logging.dart';
@@ -67,16 +64,6 @@ class ActionNotifier extends Notifier<void> {
};
}
Future<ActionResult> troubleshoot(ActionSource source, BuildContext context) async {
final assets = _getAssets(source);
if (assets.length > 1) {
return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets');
}
unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
return ActionResult(count: assets.length, success: true);
}
Future<ActionResult> emptyTrash(String userId) async {
try {
final count = await _service.emptyTrash(userId);
@@ -10,13 +10,6 @@ class SettingsNotifier extends Notifier<SettingsService> {
SettingsService build() => SettingsService(storeService: ref.read(storeServiceProvider));
T get<T>(Setting<T> setting) => state.get(setting);
Future<void> set<T>(Setting<T> setting, T value) async {
await state.set(setting, value);
ref.invalidateSelf();
}
Stream<T> watch<T>(Setting<T> setting) => state.watch(setting);
}
final settingsProvider = NotifierProvider<SettingsNotifier, SettingsService>(SettingsNotifier.new);
@@ -9,17 +9,6 @@ class TagNotifier extends AsyncNotifier<Set<Tag>> {
Future<Set<Tag>> build() async {
return ref.watch(tagServiceProvider).getAllTags();
}
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
return ref.read(tagServiceProvider).bulkTagAssets(assetIds, tagIds);
}
Future<List<Tag>> upsertTags(List<String> tags) async {
final upsertedTags = await ref.read(tagServiceProvider).upsertTags(tags);
state = AsyncValue.data({...?state.valueOrNull, ...upsertedTags});
return upsertedTags;
}
}
final tagProvider = AsyncNotifierProvider<TagNotifier, Set<Tag>>(TagNotifier.new);
@@ -31,28 +31,4 @@ class MapStateNotifier extends Notifier<MapState> {
unawaited(ref.read(settingsProvider).write(.mapThemeMode, mode));
state = state.copyWith(themeMode: mode);
}
void switchFavoriteOnly(bool isFavoriteOnly) {
unawaited(ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly));
state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true);
}
void setRefetchMarkers(bool shouldRefetch) {
state = state.copyWith(shouldRefetchMarkers: shouldRefetch);
}
void switchIncludeArchived(bool isIncludeArchived) {
unawaited(ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived));
state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true);
}
void switchWithPartners(bool isWithPartners) {
unawaited(ref.read(settingsProvider).write(.mapWithPartners, isWithPartners));
state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true);
}
void setRelativeTime(int relativeTime) {
unawaited(ref.read(settingsProvider).write(.mapRelativeDate, relativeTime));
state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true);
}
}
@@ -23,22 +23,11 @@ class NotificationPermissionNotifier extends StateNotifier<PermissionStatus> {
return permission;
}
/// Whether the user has the permission or not
/// Note: In Android, this is always true
Future<bool> hasNotificationPermission() {
return Permission.notification.isGranted;
}
Future<PermissionStatus> getNotificationPermission() async {
final status = await Permission.notification.status;
state = status;
return status;
}
/// Either the permission was granted already or else ask for the permission
Future<bool> hasOrAskForNotificationPermission() {
return requestNotificationPermission().then((p) => p.isGranted);
}
}
final notificationPermissionProvider = StateNotifierProvider<NotificationPermissionNotifier, PermissionStatus>(
+1 -17
View File
@@ -1,26 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/generated/translations.g.dart';
part 'sync_status.provider.freezed.dart';
enum SyncStatus {
idle,
syncing,
success,
error;
String localized() {
final t = StaticTranslations.instance;
return switch (this) {
SyncStatus.idle => t.idle,
SyncStatus.syncing => t.running,
SyncStatus.success => t.success,
SyncStatus.error => t.error,
};
}
}
enum SyncStatus { idle, syncing, success, error }
@freezed
abstract class SyncStatusState with _$SyncStatusState {
@@ -75,25 +75,6 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
state = const MultiSelectState(selectedAssets: {}, lockedSelectionAssets: {}, forceEnable: false);
}
/// Bucket bulk operations
Future<void> selectBucket(int offset, int bucketCount) async {
final assets = await _timelineService.loadAssets(offset, bucketCount);
final selectedAssets = state.selectedAssets.toSet();
selectedAssets.addAll(assets);
state = state.copyWith(selectedAssets: selectedAssets);
}
Future<void> deselectBucket(int offset, int bucketCount) async {
final assets = await _timelineService.loadAssets(offset, bucketCount);
final selectedAssets = state.selectedAssets.toSet();
selectedAssets.removeAll(assets);
state = state.copyWith(selectedAssets: selectedAssets);
}
Future<void> toggleBucketSelection(int offset, int bucketCount) async {
final assets = await _timelineService.loadAssets(offset, bucketCount);
toggleBucketSelectionByAssets(assets);
@@ -119,10 +100,6 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
state = state.copyWith(selectedAssets: selectedAssets);
}
void setLockedSelectionAssets(Set<BaseAsset> assets) {
state = state.copyWith(lockedSelectionAssets: assets);
}
}
final bucketSelectionProvider = Provider.family<bool, List<BaseAsset>>((ref, bucketAssets) {
@@ -17,13 +17,6 @@ class ViewIntentFilePathNotifier extends Notifier<String?> {
}
state = null;
}
void clearIfMatch(String path) {
if (state != path) {
return;
}
state = null;
}
}
final viewIntentFilePathProvider = NotifierProvider<ViewIntentFilePathNotifier, String?>(
@@ -34,11 +34,6 @@ class ActivityApiRepository extends ApiRepository {
return checkNull(_api.deleteActivity(id));
}
Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
final response = await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));
return ActivityStats(comments: response.comments);
}
static Activity _toActivity(ActivityResponseDto dto) => Activity(
id: dto.id,
createdAt: dto.createdAt,
@@ -1,5 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction;
import 'package:immich_mobile/domain/models/stack.model.dart';
@@ -43,11 +42,6 @@ class AssetApiRepository extends ApiRepository {
return response?.count ?? 0;
}
// TODO(shenlong): remove after action migration
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) async {
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
}
Future<StackResponse> stack(List<String> ids) async {
final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
@@ -58,10 +52,6 @@ class AssetApiRepository extends ApiRepository {
return _stacksApi.deleteStacks(BulkIdsDto(ids: ids));
}
Future<Response> downloadAsset(String id, {required bool edited}) {
return _api.downloadAssetWithHttpInfo(id, edited: edited);
}
api.AssetVisibility _mapVisibility(AssetVisibility visibility) => switch (visibility) {
AssetVisibility.timeline => api.AssetVisibility.timeline,
AssetVisibility.hidden => api.AssetVisibility.hidden,
@@ -110,20 +100,6 @@ class AssetApiRepository extends ApiRepository {
),
);
}
Future<void> updateLocation(List<String> ids, LatLng location) async {
return _api.updateAssets(
AssetBulkUpdateDto(
ids: ids,
latitude: Optional.present(location.latitude),
longitude: Optional.present(location.longitude),
),
);
}
Future<void> updateDateTime(List<String> ids, String dateTime) async {
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime)));
}
}
extension on StackResponseDto {
@@ -77,11 +77,6 @@ class AssetMediaRepository {
return restoredIds;
}
Future<AssetEntity?> get(String id) async {
final entity = await AssetEntity.fromId(id);
return entity;
}
Future<String?> getOriginalFilename(String id) async {
final entity = await AssetEntity.fromId(id);
if (entity == null) {
@@ -10,7 +10,6 @@ import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
// ignore: dispose-provided-instances
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
class DownloadRepository {
@@ -60,18 +59,6 @@ class DownloadRepository {
.listen((record) => onLivePhotoRecordComplete?.call(record));
}
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
return _downloader.enqueueAll(tasks);
}
Future<void> deleteAllTrackingRecords() {
return _downloader.database.deleteAllRecords();
}
Future<bool> cancel(String id) {
return _downloader.cancelTaskWithId(id);
}
Future<List<TaskRecord>> getLiveVideoTasks() {
return _downloader.database.allRecordsWithStatus(TaskStatus.complete, group: kDownloadGroupLivePhoto);
}
@@ -1,8 +1,6 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository());
@@ -10,20 +8,6 @@ final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository(
class FileMediaRepository {
const FileMediaRepository();
Future<LocalAsset?> saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async {
final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath);
return LocalAsset(
id: entity.id,
name: title,
type: AssetType.image,
createdAt: entity.createDateTime,
updatedAt: entity.modifiedDateTime,
playbackStyle: AssetPlaybackStyle.image,
isEdited: false,
);
}
Future<AssetEntity?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
return entity;
@@ -27,8 +27,4 @@ class NetworkRepository {
}
return _networkInfo.getWifiName();
}
Future<String?> getWifiIp() {
return _networkInfo.getWifiIP();
}
}
@@ -14,13 +14,6 @@ class PartnerApiRepository extends ApiRepository {
PartnerApiRepository(this._api);
Future<List<UserDto>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(direction == Direction.sharedByMe ? PartnerDirection.sharedBy : PartnerDirection.sharedWith),
);
return response.map(UserConverter.fromPartnerDto).toList();
}
Future<UserDto> create(String sharedWithId) async {
final dto = await checkNull(_api.createPartner(PartnerCreateDto(sharedWithId: sharedWithId)));
return UserConverter.fromPartnerDto(dto);
@@ -9,7 +9,6 @@ import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
@@ -37,10 +36,6 @@ class UploadRepository {
);
}
Future<void> enqueueBackground(UploadTask task) {
return FileDownloader().enqueue(task);
}
Future<List<bool>> enqueueBackgroundAll(List<UploadTask> tasks) {
return FileDownloader().enqueueAll(tasks);
}
@@ -49,10 +44,6 @@ class UploadRepository {
return FileDownloader().database.deleteAllRecords(group: group);
}
Future<bool> cancelAll(String group) {
return FileDownloader().cancelAll(group: group);
}
Future<int> reset(String group) {
return FileDownloader().reset(group: group);
}
@@ -66,28 +57,6 @@ class UploadRepository {
return FileDownloader().start();
}
Future<void> getUploadInfo() async {
final [enqueuedTasks, runningTasks, canceledTasks, waitingTasks, pausedTasks] = await Future.wait([
FileDownloader().database.allRecordsWithStatus(TaskStatus.enqueued, group: kBackupGroup),
FileDownloader().database.allRecordsWithStatus(TaskStatus.running, group: kBackupGroup),
FileDownloader().database.allRecordsWithStatus(TaskStatus.canceled, group: kBackupGroup),
FileDownloader().database.allRecordsWithStatus(TaskStatus.waitingToRetry, group: kBackupGroup),
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
]);
dPrint(
() =>
"""
Upload Info:
Enqueued: ${enqueuedTasks.length}
Running: ${runningTasks.length}
Canceled: ${canceledTasks.length}
Waiting: ${waitingTasks.length}
Paused: ${pausedTasks.length}
""",
);
}
Future<UploadResult> uploadFile({
required File file,
required String originalFileName,
-9
View File
@@ -110,15 +110,6 @@ class GCastService {
onReceiverName?.call(device.extras["fn"] ?? "Google Cast");
}
CastDestinationType getType() {
return CastDestinationType.googleCast;
}
Future<bool> initialize() async {
// there is nothing blocking us from using Google Cast that we can check for
return true;
}
Future<void> disconnect() async {
onReceiverName?.call("");
currentAssetId = null;
-4
View File
@@ -16,10 +16,6 @@ class ToastService {
snackbar.success(message, duration: toast?.timeout, action: toast?.action);
}
FutureOr<void> info(String message, {ToastOption? toast}) {
snackbar.info(message, duration: toast?.timeout, action: toast?.action);
}
FutureOr<void> error(String message, {ToastOption? toast}) {
snackbar.error(message, duration: toast?.timeout, action: toast?.action);
}
@@ -44,13 +44,6 @@ class ViewIntentService {
}
}
Future<void> cleanupManagedTempFileIfCurrent(String path) async {
if (_managedTempFilePath == path) {
_managedTempFilePath = null;
}
await cleanupTempFile(path);
}
Future<void> cleanupTempFile(String path) async {
if (!_isManagedTempFile(path)) {
return;
@@ -90,19 +83,6 @@ class ViewIntentService {
}
}
void markUploadActive(String path) {
_activeUploadPaths.add(path);
}
Future<void> markUploadInactive(String path) async {
if (!_activeUploadPaths.remove(path)) {
return;
}
if (_managedTempFilePath != path) {
await cleanupTempFile(path);
}
}
bool _isManagedTempFile(String path) {
return p.basename(path).startsWith('view_intent_') && p.basename(p.dirname(path)) == 'cache';
}
@@ -260,10 +260,6 @@ class ActionButtonBuilder {
ActionButtonType.restoreTrash,
};
static List<Widget> build(ActionButtonContext context) {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
}
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
final visibleButtons = defaultViewerKebabMenuOrder
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
-4
View File
@@ -2,9 +2,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implements Iterable<T> {
AssetFilter<T> where(bool Function(T asset) test) => AssetFilter(assets.where(test));
AssetFilter<T> whereNot(bool Function(T asset) test) => AssetFilter(assets.where((asset) => !test(asset)));
AssetFilter<T> type(AssetType type) => where((asset) => asset.type == type);
AssetFilter<T> favorite({bool isFavorite = true}) => where((asset) => asset.isFavorite == isFavorite);
AssetFilter<RemoteAsset> remote() => AssetFilter(assets.whereType<RemoteAsset>());
@@ -13,8 +11,6 @@ extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implem
remote().where((asset) => asset.visibility == visibility);
AssetFilter<RemoteAsset> notVisibility(AssetVisibility visibility) =>
remote().where((asset) => asset.visibility != visibility);
AssetFilter<RemoteAsset> archived({bool isArchived = true}) =>
remote().where((asset) => asset.isArchived == isArchived);
AssetFilter<RemoteAsset> locked({bool isLocked = true}) => remote().where((asset) => asset.isLocked == isLocked);
AssetFilter<RemoteAsset> stacked({bool isStacked = true}) => remote().where((asset) => asset.isStacked == isStacked);
AssetFilter<RemoteAsset> trashed({bool isTrashed = true}) => remote().where((asset) => asset.isTrashed == isTrashed);
-11
View File
@@ -26,17 +26,6 @@ class Debouncer {
_timer = Timer(interval, _callAndRest);
}
Future<void>? drain() {
final timer = _timer;
if (timer != null && timer.isActive) {
timer.cancel();
if (_lastAction != null) {
_callAndRest();
}
}
return _actionFuture;
}
@pragma('vm:prefer-inline')
void _callAndRest() {
_lastActionTime = DateTime.now();
-15
View File
@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@@ -49,20 +48,6 @@ class MapUtils {
heatmapOpacity: 0.7,
);
static Map<String, dynamic> _addFeature(MapMarker marker) => {
'type': 'Feature',
'id': marker.assetRemoteId,
'geometry': {
'type': 'Point',
'coordinates': [marker.latLng.longitude, marker.latLng.latitude],
},
};
static Map<String, dynamic> generateGeoJsonForMarkers(List<MapMarker> markers) => {
'type': 'FeatureCollection',
'features': markers.map(_addFeature).toList(),
};
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
required BuildContext context,
bool silent = false,
-10
View File
@@ -264,16 +264,6 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateString(StoreKey<String> legacyKey, SettingsKey<String> newKey) async {
final value = await readLegacyStoreString(legacyKey.id);
if (value == null) {
return;
}
_cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id);
}
void stage<T, U extends T>(StoreKey legacyKey, SettingsKey<T> newKey, U value) {
_cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id);
-9
View File
@@ -13,15 +13,6 @@ class SemVer {
return '$major.$minor.$patch${prerelease == null ? '' : '-rc.$prerelease'}';
}
SemVer copyWith({int? major, int? minor, int? patch, int? prerelease}) {
return SemVer(
major: major ?? this.major,
minor: minor ?? this.minor,
patch: patch ?? this.patch,
prerelease: prerelease ?? this.prerelease,
);
}
static final _pattern = RegExp(r'^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?(?:[-+].*)?$', caseSensitive: false);
factory SemVer.fromString(String version) {
@@ -24,14 +24,6 @@ class UploadSpeedCalculator {
/// The total file size being uploaded.
int _totalBytes = 0;
/// Resets the calculator for a new upload.
void reset() {
_speedSamples.clear();
_lastUpdateTime = null;
_lastBytes = 0;
_totalBytes = 0;
}
/// Updates the calculator with the current progress.
///
/// [currentBytes] is the number of bytes transferred so far.
@@ -160,16 +152,6 @@ class UploadSpeedManager {
return calculator.speedAsString;
}
/// Gets the current speed string for a specific task.
String getSpeedAsString(String taskId) {
return _calculators[taskId]?.speedAsString ?? '-- MB/s';
}
/// Gets the time remaining string for a specific task.
String getTimeRemainingAsString(String taskId) {
return _calculators[taskId]?.timeRemainingAsString ?? '--:--';
}
/// Removes a task from tracking.
void removeTask(String taskId) {
_calculators.remove(taskId);
@@ -62,12 +62,6 @@ class PhotoViewScaleStateController {
/// Check if is `zoomedIn` & `zoomedOut`
bool get isZooming => scaleState == PhotoViewScaleState.zoomedIn || scaleState == PhotoViewScaleState.zoomedOut;
/// Resets the state to the initial value;
void reset() {
prevScaleState = scaleState;
scaleState = PhotoViewScaleState.initial;
}
/// Closes streams and removes eventual listeners
void dispose() {
unawaited(_outputScaleStateCtrl.close());
@@ -323,14 +323,6 @@ class PhotoViewCoreState extends State<PhotoViewCore>
super.dispose();
}
void onTapUp(TapUpDetails details) {
widget.onTapUp?.call(context, details, controller.value);
}
void onTapDown(TapDownDetails details) {
widget.onTapDown?.call(context, details, controller.value);
}
void _updateScaleBoundaries() {
final prev = controller.scaleBoundaries;
if (prev == widget.scaleBoundaries) {
@@ -443,195 +443,4 @@ void main() {
expect(result.assets.first.id, videoAsset.id);
});
});
group('reconcileHashesFromCloudId', () {
late String userId;
setUp(() async {
final user = await ctx.newUser();
userId = user.id;
});
test('updates local asset checksum when all metadata matches', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final remoteCloudAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id);
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: remoteCloudAsset.cloudId,
createdAt: remoteCloudAsset.createdAt,
adjustmentTime: remoteCloudAsset.adjustmentTime,
latitude: remoteCloudAsset.latitude,
longitude: remoteCloudAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, remoteAsset.checksum);
});
test('does not update when local asset already has checksum', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final remoteCloudAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id);
final localAsset = await ctx.newLocalAsset(
checksum: 'existing',
iCloudId: remoteCloudAsset.cloudId,
createdAt: remoteCloudAsset.createdAt,
adjustmentTime: remoteCloudAsset.adjustmentTime,
latitude: remoteCloudAsset.latitude,
longitude: remoteCloudAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, 'existing');
});
test('does not update when adjustment_time does not match', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, adjustmentTime: DateTime(2024, 1, 12));
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId,
createdAt: cloudIdAsset.createdAt,
adjustmentTime: DateTime(2026, 1, 12),
latitude: cloudIdAsset.latitude,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('does not update when latitude does not match', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, latitude: const Option.none());
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId,
createdAt: cloudIdAsset.createdAt,
adjustmentTime: cloudIdAsset.adjustmentTime,
latitude: 40.7128,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('does not update when longitude does not match', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable(-74.006));
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId,
createdAt: cloudIdAsset.createdAt,
adjustmentTime: cloudIdAsset.adjustmentTime,
latitude: cloudIdAsset.latitude,
longitude: 0.0,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('does not update when createdAt does not match', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, createdAt: DateTime(2024, 1, 5));
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId,
createdAt: DateTime(2024, 6, 1),
adjustmentTime: cloudIdAsset.adjustmentTime,
latitude: cloudIdAsset.latitude,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('does not update when iCloudId is null', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id);
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: null,
createdAt: cloudIdAsset.createdAt,
adjustmentTime: cloudIdAsset.adjustmentTime,
latitude: cloudIdAsset.latitude,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('does not update when cloudId does not match iCloudId', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id);
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: 'different-cloud-id',
createdAt: cloudIdAsset.createdAt,
adjustmentTime: cloudIdAsset.adjustmentTime,
latitude: cloudIdAsset.latitude,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('handles partial null metadata fields matching correctly', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(
id: remoteAsset.id,
adjustmentTimeOption: const Option.none(),
);
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId,
createdAt: cloudIdAsset.createdAt,
adjustmentTimeOption: const Option.none(),
latitude: cloudIdAsset.latitude,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, remoteAsset.checksum);
});
test('does not update when one has null and other has value', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id);
final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId,
createdAt: cloudIdAsset.createdAt,
adjustmentTime: cloudIdAsset.adjustmentTime,
latitude: null,
longitude: cloudIdAsset.longitude,
);
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
test('handles no matching assets gracefully', () async {
final localAsset = await ctx.newLocalAsset(checksumOption: const Option.none(), iCloudId: 'cloud-no-match');
await sut.reconcileHashesFromCloudId();
final updated = await sut.getById(localAsset.id);
expect(updated?.checksum, isNull);
});
});
}
-1
View File
@@ -13,7 +13,6 @@ void _stubPartnerApi(MockPartnerApiRepository api) {
final partner = UserDto(id: id, email: '$id@example.com', name: 'name $id', profileChangedAt: TestUtils.now());
registerFallbackValue(Direction.sharedByMe);
when(() => api.getAll(any())).thenAnswer((_) async => const <UserDto>[]);
when(() => api.create(any())).thenAnswer((_) async => partner);
when(() => api.update(any(), inTimeline: any(named: 'inTimeline'))).thenAnswer((_) async => partner);
when(() => api.delete(any())).thenAnswer((_) async {});
@@ -1,5 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
void main() {
@@ -61,45 +60,7 @@ void main() {
test('diacritic removal is order-independent', () {
const raw = 'Árvíztűrő';
expect(
raw.toLowerCase().removeDiacritics(),
raw.removeDiacritics().toLowerCase(),
);
});
});
group('Test uniqueConsecutive', () {
test('empty', () {
final a = [];
expect(a.uniqueConsecutive(), []);
});
test('singleElement', () {
final a = [5];
expect(a.uniqueConsecutive(), [5]);
});
test('noDuplicates', () {
final a = [1, 2, 3];
expect(a.uniqueConsecutive(), orderedEquals([1, 2, 3]));
});
test('unsortedDuplicates', () {
final a = [1, 2, 1, 3];
expect(a.uniqueConsecutive(), orderedEquals([1, 2, 1, 3]));
});
test('sortedDuplicates', () {
final a = [6, 6, 2, 3, 3, 3, 4, 5, 1, 1];
expect(a.uniqueConsecutive(), orderedEquals([6, 2, 3, 4, 5, 1]));
});
test('withKey', () {
final a = ["a", "bb", "cc", "ddd"];
expect(
a.uniqueConsecutive(compare: (s1, s2) => s1.length.compareTo(s2.length)),
orderedEquals(["a", "bb", "ddd"]),
);
expect(raw.toLowerCase().removeDiacritics(), raw.removeDiacritics().toLowerCase());
});
});
}
@@ -180,7 +180,7 @@ void main() {
await lifeCycle.handleAppPause();
await releaseResume();
expect(lifeCycle.getAppState(), AppLifeCycleEnum.paused);
expect(lifeCycle.state, AppLifeCycleEnum.paused);
expect(serverVersionCount, 1);
expect(websocket.disconnectCount, 2);
expect(websocket.connectCount, 0);
@@ -194,7 +194,7 @@ void main() {
unawaited(lifeCycle.handleAppResume());
await websocket.connectCalled.future;
expect(lifeCycle.getAppState(), AppLifeCycleEnum.resumed);
expect(lifeCycle.state, AppLifeCycleEnum.resumed);
expect(serverVersionCount, 2);
expect(websocket.disconnectCount, 1);
expect(websocket.connectCount, 1);
@@ -211,7 +211,7 @@ void main() {
unawaited(lifeCycle.handleAppResume());
await Future<void>.delayed(Duration.zero);
expect(lifeCycle.getAppState(), AppLifeCycleEnum.resumed);
expect(lifeCycle.state, AppLifeCycleEnum.resumed);
expect(serverVersionCount, 2);
expect(websocket.disconnectCount, 2);
expect(websocket.connectCount, 1);
@@ -72,18 +72,6 @@ void main() {
expect(await secondFile.exists(), isFalse);
});
test('cleanupTempFile defers deletion while an upload is active', () async {
final tempFile = File('${cacheDir.path}/view_intent_in_flight.jpg')..writeAsStringSync('bytes');
service.markUploadActive(tempFile.path);
await service.cleanupTempFile(tempFile.path);
expect(await tempFile.exists(), isTrue, reason: 'active uploads block cleanup');
await service.markUploadInactive(tempFile.path);
expect(await tempFile.exists(), isFalse);
});
test('cleanupTempFile ignores non-managed paths', () async {
final nonManagedFile = File('${tempRoot.path}/plain_file.jpg')..writeAsStringSync('content');
@@ -103,15 +91,4 @@ void main() {
expect(await secondFile.exists(), isFalse);
expect(await unrelatedFile.exists(), isTrue);
});
test('cleanupStaleTempFiles skips paths with active uploads', () async {
final stale = File('${cacheDir.path}/view_intent_stale.jpg')..writeAsStringSync('stale');
final active = File('${cacheDir.path}/view_intent_active.jpg')..writeAsStringSync('active');
service.markUploadActive(active.path);
await service.cleanupStaleTempFiles();
expect(await stale.exists(), isFalse);
expect(await active.exists(), isTrue);
});
}
+1 -6
View File
@@ -85,7 +85,6 @@ class RepositoryMocks {
}
void _stubLocalAssetRepository() {
when(localAsset.reconcileHashesFromCloudId).thenAnswer((_) async => {});
when(localAsset.updateHashes).thenAnswer((_) async => {});
when(localAsset.deleteAssets).thenAnswer((_) async => {});
}
@@ -256,11 +255,7 @@ extension type const LocalAlbumRepositoryStub(MockLocalAlbumRepository repo) imp
() => repo.getAssetsToHash(any());
}
extension type const LocalAssetRepositoryStub(MockLocalAssetRepository repo)
implements Stub<MockLocalAssetRepository> {
Future<void> Function() get reconcileHashesFromCloudId =>
() => repo.reconcileHashesFromCloudId();
extension type const LocalAssetRepositoryStub(MockLocalAssetRepository repo) implements Stub<MockLocalAssetRepository> {
Future<void> Function() get updateHashes =>
() => repo.updateHashes(any());
+1 -39
View File
@@ -81,15 +81,6 @@ void main() {
expect(favorites.toList(), [asset1]);
});
test('type keeps only assets of the given type', () {
final image = RemoteAssetFactory.create();
final video = RemoteAssetFactory.create(ownerId: image.ownerId).copyWith(type: .video);
final videos = AssetFilter([image, video]).type(.video);
expect(videos.toList(), [video]);
});
test('visibility keeps only assets with the given visibility', () {
final locked = RemoteAssetFactory.create(visibility: AssetVisibility.locked);
final onTimeline = RemoteAssetFactory.create(ownerId: locked.ownerId);
@@ -99,15 +90,6 @@ void main() {
expect(lockedPhotos.toList(), [locked]);
});
test('archived keeps only archived assets', () {
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId);
final archivedPhotos = AssetFilter([archived, onTimeline]).archived();
expect(archivedPhotos.toList(), [archived]);
});
test('stacked keeps only assets belonging to a stack', () {
final stacked = RemoteAssetFactory.create(stackId: 'stack');
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
@@ -119,17 +101,6 @@ void main() {
});
group('inversion', () {
test('notArchived keeps every non-archived visibility', () {
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
final hidden = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.hidden);
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
final visiblePhotos = AssetFilter([archived, onTimeline, hidden, locked]).archived(isArchived: false);
expect(visiblePhotos.toSet(), {onTimeline, hidden, locked});
});
test('notVisibility keeps every asset not at the target visibility', () {
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
@@ -149,15 +120,6 @@ void main() {
expect(loosePhotos.toList(), [loose]);
});
test('whereNot inverts an arbitrary predicate', () {
final favorite = RemoteAssetFactory.create(isFavorite: true);
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
final nonFavorites = AssetFilter([favorite, regular]).whereNot((asset) => asset.isFavorite);
expect(nonFavorites.toList(), [regular]);
});
test('notFavorites keeps only non-favorite assets', () {
final favorite = RemoteAssetFactory.create(isFavorite: true);
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
@@ -182,7 +144,7 @@ void main() {
archived,
stacked,
localPhoto,
]).owned(asset.ownerId).archived(isArchived: false).stacked(isStacked: false);
]).owned(asset.ownerId).notVisibility(AssetVisibility.archive).stacked(isStacked: false);
expect(result.toList(), [asset]);
});
@@ -1034,100 +1034,4 @@ void main() {
}
});
});
group('ActionButtonBuilder', () {
test('should return buttons that should show', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widgets = ActionButtonBuilder.build(context);
expect(widgets, isNotEmpty);
expect(widgets.length, greaterThan(0));
});
test('should include album-specific buttons when album is present', () {
final remoteAsset = createRemoteAsset();
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widgets = ActionButtonBuilder.build(context);
expect(widgets, isNotEmpty);
});
test('should only include local buttons for local assets', () {
final localAsset = createLocalAsset();
final context = ActionButtonContext(
asset: localAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widgets = ActionButtonBuilder.build(context);
expect(widgets, isNotEmpty);
});
test('should respect archived state', () {
final remoteAsset = createRemoteAsset();
final archivedContext = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final archivedWidgets = ActionButtonBuilder.build(archivedContext);
final nonArchivedContext = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final nonArchivedWidgets = ActionButtonBuilder.build(nonArchivedContext);
expect(archivedWidgets, isNotEmpty);
expect(nonArchivedWidgets, isNotEmpty);
});
});
}