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:
65 files changed
+7
-1195
No files matched your search
@@ -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,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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user