chore(mobile): enable dead code CI (#31753)

* chore(mobile): enable dead code CI

* Restore accidentally removed test and set mise default
This commit is contained in:
Adam Gastineau
2026-09-24 06:57:06 -07:00
committed by GitHub
parent 647a445d9a
commit 2ccd0effc8
115 changed files with 181 additions and 1517 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
run: mise //mobile:codegen
- name: Run analyze
run: mise //mobile:analyze
run: mise //mobile:analyze --full
- name: Run format
run: mise //mobile:format
+13
View File
@@ -104,6 +104,19 @@ analyzer:
deprecated_member_use_from_same_package: error
dart_code_metrics:
exclude:
unused-code:
- '**/*.g.dart'
- '**/*.freezed.dart'
- '**/*.mapper.dart'
- '**/*.drift.dart'
- '**/*.gr.dart'
- 'lib/data/db/main/database.steps.dart'
# Ideally we wouldn't do this, as this allows for non-column used code, but not complaining
# about the columns is more important
- 'lib/data/db/**/table/**'
- 'lib/widgets/photo_view/**'
- 'test/drift/**/generated/**'
rules:
- banned-usage:
entries:
-2
View File
@@ -5,5 +5,3 @@ enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, gr
const Color immichBrandColorLight = Color(0xFF4150AF);
const Color immichBrandColorDark = Color(0xFFACCBFA);
const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75);
const Color red400 = Color(0xFFEF5350);
const Color grey200 = Color(0xFFEEEEEE);
+1 -2
View File
@@ -7,11 +7,9 @@ const int kLogTruncateLimit = 2000;
// Sync
const int kSyncEventBatchSize = 5000;
const int kFetchLocalAssetsBatchSize = 40000;
// Hash batch limits
final int kBatchHashFileLimit = Platform.isIOS ? 32 : 512;
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
// Secure storage keys
const String kSecuredPinCode = "secured_pin_code";
@@ -51,6 +49,7 @@ const String kImmichLatestRelease = "https://github.com/immich-app/immich/releas
const int kPhotoTabIndex = 0;
const int kSearchTabIndex = 1;
const int kAlbumTabIndex = 2;
// ignore: unused-code
const int kLibraryTabIndex = 3;
// Workaround for SQLite's variable limit (SQLITE_MAX_VARIABLE_NUMBER = 32766)
-148
View File
@@ -1,148 +0,0 @@
import 'package:flutter/material.dart';
const List<ColorFilter> filters = [
//Original
ColorFilter.matrix([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]),
//Vintage
ColorFilter.matrix([0.8, 0.1, 0.1, 0, 20, 0.1, 0.8, 0.1, 0, 20, 0.1, 0.1, 0.8, 0, 20, 0, 0, 0, 1, 0]),
//Mood
ColorFilter.matrix([1.2, 0.1, 0.1, 0, 10, 0.1, 1, 0.1, 0, 10, 0.1, 0.1, 1, 0, 10, 0, 0, 0, 1, 0]),
//Crisp
ColorFilter.matrix([1.2, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0]),
//Cool
ColorFilter.matrix([0.9, 0, 0.2, 0, 0, 0, 1, 0.1, 0, 0, 0.1, 0, 1.2, 0, 0, 0, 0, 0, 1, 0]),
//Blush
ColorFilter.matrix([1.1, 0.1, 0.1, 0, 10, 0.1, 1, 0.1, 0, 10, 0.1, 0.1, 1, 0, 5, 0, 0, 0, 1, 0]),
//Sunkissed
ColorFilter.matrix([1.3, 0, 0.1, 0, 15, 0, 1.1, 0.1, 0, 10, 0, 0, 0.9, 0, 5, 0, 0, 0, 1, 0]),
//Fresh
ColorFilter.matrix([1.2, 0, 0, 0, 20, 0, 1.2, 0, 0, 20, 0, 0, 1.1, 0, 20, 0, 0, 0, 1, 0]),
//Classic
ColorFilter.matrix([1.1, 0, -0.1, 0, 10, -0.1, 1.1, 0.1, 0, 5, 0, -0.1, 1.1, 0, 0, 0, 0, 0, 1, 0]),
//Lomo-ish
ColorFilter.matrix([1.5, 0, 0.1, 0, 0, 0, 1.45, 0, 0, 0, 0.1, 0, 1.3, 0, 0, 0, 0, 0, 1, 0]),
//Nashville
ColorFilter.matrix([1.2, 0.15, -0.15, 0, 15, 0.1, 1.1, 0.1, 0, 10, -0.05, 0.2, 1.25, 0, 5, 0, 0, 0, 1, 0]),
//Valencia
ColorFilter.matrix([1.15, 0.1, 0.1, 0, 20, 0.1, 1.1, 0, 0, 10, 0.1, 0.1, 1.2, 0, 5, 0, 0, 0, 1, 0]),
//Clarendon
ColorFilter.matrix([1.2, 0, 0, 0, 10, 0, 1.25, 0, 0, 10, 0, 0, 1.3, 0, 10, 0, 0, 0, 1, 0]),
//Moon
ColorFilter.matrix([0.33, 0.33, 0.33, 0, 0, 0.33, 0.33, 0.33, 0, 0, 0.33, 0.33, 0.33, 0, 0, 0, 0, 0, 1, 0]),
//Willow
ColorFilter.matrix([0.5, 0.5, 0.5, 0, 20, 0.5, 0.5, 0.5, 0, 20, 0.5, 0.5, 0.5, 0, 20, 0, 0, 0, 1, 0]),
//Kodak
ColorFilter.matrix([1.3, 0.1, -0.1, 0, 10, 0, 1.25, 0.1, 0, 10, 0, -0.1, 1.1, 0, 5, 0, 0, 0, 1, 0]),
//Frost
ColorFilter.matrix([0.8, 0.2, 0.1, 0, 0, 0.2, 1.1, 0.1, 0, 0, 0.1, 0.1, 1.2, 0, 10, 0, 0, 0, 1, 0]),
//Night Vision
ColorFilter.matrix([0.1, 0.95, 0.2, 0, 0, 0.1, 1.5, 0.1, 0, 0, 0.2, 0.7, 0, 0, 0, 0, 0, 0, 1, 0]),
//Sunset
ColorFilter.matrix([1.5, 0.2, 0, 0, 0, 0.1, 0.9, 0.1, 0, 0, -0.1, -0.2, 1.3, 0, 0, 0, 0, 0, 1, 0]),
//Noir
ColorFilter.matrix([1.3, -0.3, 0.1, 0, 0, -0.1, 1.2, -0.1, 0, 0, 0.1, -0.2, 1.3, 0, 0, 0, 0, 0, 1, 0]),
//Dreamy
ColorFilter.matrix([1.1, 0.1, 0.1, 0, 0, 0.1, 1.1, 0.1, 0, 0, 0.1, 0.1, 1.1, 0, 15, 0, 0, 0, 1, 0]),
//Sepia
ColorFilter.matrix([0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0]),
//Radium
ColorFilter.matrix([
1.438,
-0.062,
-0.062,
0,
0,
-0.122,
1.378,
-0.122,
0,
0,
-0.016,
-0.016,
1.483,
0,
0,
0,
0,
0,
1,
0,
]),
//Aqua
ColorFilter.matrix([
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.7873,
0.2848,
0.9278,
0,
0,
0,
0,
0,
1,
0,
]),
//Purple Haze
ColorFilter.matrix([1.3, 0, 1.2, 0, 0, 0, 1.1, 0, 0, 0, 0.2, 0, 1.3, 0, 0, 0, 0, 0, 1, 0]),
//Lemonade
ColorFilter.matrix([1.2, 0.1, 0, 0, 0, 0, 1.1, 0.2, 0, 0, 0.1, 0, 0.7, 0, 0, 0, 0, 0, 1, 0]),
//Caramel
ColorFilter.matrix([1.6, 0.2, 0, 0, 0, 0.1, 1.3, 0.1, 0, 0, 0, 0.1, 0.9, 0, 0, 0, 0, 0, 1, 0]),
//Peachy
ColorFilter.matrix([1.3, 0.5, 0, 0, 0, 0.2, 1.1, 0.3, 0, 0, 0.1, 0.1, 1.2, 0, 0, 0, 0, 0, 1, 0]),
//Neon
ColorFilter.matrix([1, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0]),
//Cold Morning
ColorFilter.matrix([0.9, 0.1, 0.2, 0, 0, 0, 1, 0.1, 0, 0, 0.1, 0, 1.2, 0, 0, 0, 0, 0, 1, 0]),
//Lush
ColorFilter.matrix([0.9, 0.2, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 1, 0]),
//Urban Neon
ColorFilter.matrix([1.1, 0, 0.3, 0, 0, 0, 0.9, 0.3, 0, 0, 0.3, 0.1, 1.2, 0, 0, 0, 0, 0, 1, 0]),
//Monochrome
ColorFilter.matrix([0.6, 0.2, 0.2, 0, 0, 0.2, 0.6, 0.2, 0, 0, 0.2, 0.2, 0.7, 0, 0, 0, 0, 0, 1, 0]),
];
const List<String> filterNames = [
'Original',
'Vintage',
'Mood',
'Crisp',
'Cool',
'Blush',
'Sunkissed',
'Fresh',
'Classic',
'Lomo-ish',
'Nashville',
'Valencia',
'Clarendon',
'Moon',
'Willow',
'Kodak',
'Frost',
'Night Vision',
'Sunset',
'Noir',
'Dreamy',
'Sepia',
'Radium',
'Aqua',
'Purple Haze',
'Lemonade',
'Caramel',
'Peachy',
'Neon',
'Cold Morning',
'Lush',
'Urban Neon',
'Monochrome',
];
+3
View File
@@ -61,9 +61,12 @@ class DataController {
return (logDb, false);
}
// ignore: unused-code
late final PeopleDatabaseRepository peopleDb = PeopleDatabaseRepository(_db);
// ignore: unused-code
late final PersonApiRepository personApi = PersonApiRepository(PeopleApi(_apiClient));
// ignore: unused-code
late final ActivityApiRepository activityApi = ActivityApiRepository(ActivitiesApi(_apiClient));
/// Direct database access for the logic that has not yet been migrated
+1
View File
@@ -7,6 +7,7 @@ import 'package:sqlite_async/sqlite_async.dart';
@DriftDatabase(tables: [LogMessageEntity])
class DriftLogger extends $DriftLogger {
// ignore: unused-code
DriftLogger.fromExecutor(super.executor);
DriftLogger.sqlite(SqliteConnection db) : super(SqliteAsyncDriftConnection(db));
+1
View File
@@ -112,6 +112,7 @@ import 'package:sqlite_async/sqlite_async.dart';
class Drift extends $Drift {
final SqliteConnectionPool? _updatePool;
@visibleForTesting
Drift(super.executor) : _updatePool = null;
Drift.sqlite(SqliteConnection db, SqliteConnectionPool updatePool)
+2
View File
@@ -1,3 +1,5 @@
// ignore_for_file: unused-code
import 'package:drift/drift.dart';
import 'package:immich_mobile/data/db/util/datetime_clamp_type.dart';
import 'package:immich_mobile/data/db/util/defaults_mixin.dart';
@@ -1,3 +1,6 @@
// Types are used for direct JSON serialization
// ignore_for_file: unused-code
import 'package:freezed_annotation/freezed_annotation.dart';
part 'asset_metadata.model.freezed.dart';
@@ -58,6 +58,7 @@ sealed class BaseAsset {
bool get hasRemote => storage == AssetState.remote || storage == AssetState.merged;
bool get hasLocal => storage == AssetState.local || storage == AssetState.merged;
// ignore: unused-code
bool get isLocalOnly => storage == AssetState.local;
bool get isRemoteOnly => storage == AssetState.remote;
bool get isMerged => storage == .merged;
@@ -73,6 +73,7 @@ class RemoteAsset extends BaseAsset {
bool get isStacked => stackId != null;
// ignore: unused-code
bool get isArchived => visibility == .archive;
bool get isLocked => visibility == .locked;
@@ -1,21 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'asset_face.model.freezed.dart';
// Model for an asset face stored in the server
@freezed
abstract class AssetFace with _$AssetFace {
const factory AssetFace({
required String id,
required String assetId,
String? personId,
required int imageWidth,
required int imageHeight,
required int boundingBoxX1,
required int boundingBoxY1,
required int boundingBoxX2,
required int boundingBoxY2,
required String sourceType,
}) = _AssetFace;
}
@@ -20,16 +20,6 @@ class ViewerShowDetailsEvent extends Event {
const ViewerShowDetailsEvent();
}
class ViewerReloadAssetEvent extends Event {
const ViewerReloadAssetEvent();
}
class ViewerStackAssetDeletedEvent extends Event {
final int stackIndex;
const ViewerStackAssetDeletedEvent({required this.stackIndex});
}
// Multi-Select Events
class MultiSelectToggleEvent extends Event {
final bool isEnabled;
@@ -26,6 +26,7 @@ abstract class MemoryData with _$MemoryData {
return MemoryData(year: map['year'] as int);
}
@visibleForTesting
String toJson() => json.encode(toMap());
factory MemoryData.fromJson(String source) => MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
-12
View File
@@ -3,18 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'stack.model.freezed.dart';
// Model for a stack stored in the server
@freezed
abstract class Stack with _$Stack {
const factory Stack({
required String id,
required DateTime createdAt,
required DateTime updatedAt,
required String ownerId,
required String primaryAssetId,
}) = _Stack;
}
@freezed
abstract class StackResponse with _$StackResponse {
const factory StackResponse({required String id, required String primaryAssetId, required List<String> assetIds}) =
+1 -52
View File
@@ -101,17 +101,6 @@ abstract class UserDto with _$UserDto {
quotaUsageInBytes.hashCode;
}
@freezed
abstract class PartnerUserDto with _$PartnerUserDto {
const factory PartnerUserDto({
required String id,
required String email,
required String name,
required bool inTimeline,
String? profileImagePath,
}) = _PartnerUserDto;
}
class User {
final String id;
final String name;
@@ -152,50 +141,10 @@ class User {
int get hashCode => Object.hash(id, name, email, profileChangedAt, hasProfileImage, avatarColor);
}
class AuthUser extends User {
final bool isAdmin;
final String? pinCode;
final int? quotaSizeInBytes;
final int quotaUsageInBytes;
const AuthUser({
required super.id,
required super.name,
required super.email,
required super.profileChangedAt,
required super.hasProfileImage,
super.avatarColor,
this.isAdmin = false,
this.pinCode,
this.quotaSizeInBytes = 0,
this.quotaUsageInBytes = 0,
});
@override
String toString() {
return 'AuthUser(user: ${super.toString()}, isAdmin: $isAdmin, pinCode: $pinCode, quotaSizeInBytes: $quotaSizeInBytes, quotaUsageInBytes: $quotaUsageInBytes)';
}
@override
bool operator ==(covariant AuthUser other) {
if (identical(this, other)) {
return true;
}
return super == other &&
other.isAdmin == isAdmin &&
other.pinCode == pinCode &&
other.quotaSizeInBytes == quotaSizeInBytes &&
other.quotaUsageInBytes == quotaUsageInBytes;
}
@override
int get hashCode => Object.hash(super.hashCode, isAdmin, pinCode, quotaSizeInBytes, quotaUsageInBytes);
}
class Partner extends User {
final bool inTimeline;
@visibleForTesting
const Partner({
required super.id,
required super.name,
@@ -56,6 +56,7 @@ class BackgroundWorkerFgService {
);
}
// ignore: unused-code
Future<void> disable() => _foregroundHostApi.disable();
Future<bool> wasLaunchedInBackground() => _foregroundHostApi.wasLaunchedInBackground();
@@ -1,14 +1,11 @@
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
import 'package:logging/logging.dart';
/// Accesses Memories; a specialized collection of assets with some novel display mechanism
class MemoryService {
final log = Logger("MemoryService");
final MemoryRepository _repository;
MemoryService(this._repository);
const MemoryService(this._repository);
Future<List<Memory>> getMemoryLane(String ownerId) {
return _repository.getAll(ownerId);
@@ -1,11 +1,6 @@
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
// Singleton instance of SettingsService, to use in places
// where reactivity is not required
// ignore: non_constant_identifier_names
final AppSetting = SettingsService(storeService: StoreService.I);
class SettingsService {
final StoreService _storeService;
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
@@ -95,6 +96,7 @@ class StoreService {
}
/// Clears all values from the store (cache and DB)
@visibleForTesting
Future<void> clear() async {
await _storeRepository.deleteAll();
_cache.clear();
@@ -1,8 +1,11 @@
import 'package:immich_mobile/platform/connectivity_api.g.dart';
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
// ignore: unused-code
bool get hasCellular => contains(NetworkCapability.cellular);
// ignore: unused-code
bool get hasWifi => contains(NetworkCapability.wifi);
// ignore: unused-code
bool get hasVpn => contains(NetworkCapability.vpn);
bool get isUnmetered => contains(NetworkCapability.unmetered);
}
@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:diacritic/diacritic.dart' as diacritic;
extension StringExtension on String {
@@ -32,11 +30,3 @@ extension DurationExtension on String {
return int.parse(this);
}
}
Map<String, dynamic>? tryJsonDecode(dynamic json) {
try {
return jsonDecode(json) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
@@ -19,6 +19,7 @@ abstract class ImageRequest {
final int requestId = _nextRequestId++;
bool _isCancelled = false;
// ignore: unused-code
bool get isCancelled => _isCancelled;
ImageRequest();
@@ -50,8 +50,6 @@ class NetworkRepository {
}
}
const NetworkRepository();
/// Returns a shared HTTP client that uses native SSL configuration.
///
/// On iOS: Uses SharedURLSessionManager's URLSession.
@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/data/db/main/database.dart';
import 'package:immich_mobile/data/db/main/table/user/partner.drift.dart';
@@ -12,6 +13,7 @@ class PartnerRepository extends DatabaseAccessor<Drift> with $PartnerRepositoryM
Drift get _db => attachedDatabase;
@visibleForTesting
Future<Partner> get({required String sharedById, required String sharedWithId}) =>
(_db.select(_db.partnerEntity).join([
innerJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById)),
@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/data/db/main/database.dart';
import 'package:immich_mobile/data/db/main/table/app/store.drift.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
@@ -40,6 +41,7 @@ class StoreRepository extends DatabaseAccessor<Drift> with $StoreRepositoryMixin
return true;
}
@visibleForTesting
Future<T?> tryGet<T>(StoreKey<T> key) async {
final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull();
if (entity == null) {
-41
View File
@@ -1,41 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
typedef AsyncFuture<T> = Future<AsyncValue<T>>;
mixin ErrorLoggerMixin {
abstract final Logger logger;
// ignore: unintended_html_in_doc_comment
/// Returns an AsyncValue<T> if the future is successfully executed
/// Else, logs the error to the overridden logger and returns an AsyncError<>
AsyncFuture<T> guardError<T>(
Future<T> Function() fn, {
required String errorMessage,
Level logLevel = Level.SEVERE,
}) async {
try {
final result = await fn();
return AsyncData(result);
} catch (error, stackTrace) {
logger.log(logLevel, errorMessage, error, stackTrace);
return AsyncError(error, stackTrace);
}
}
/// Returns the result of the future if success
/// Else, logs the error and returns the default value
Future<T> logError<T>(
Future<T> Function() fn, {
required T defaultValue,
required String errorMessage,
Level logLevel = Level.SEVERE,
}) async {
try {
return await fn();
} catch (error, stackTrace) {
logger.log(logLevel, errorMessage, error, stackTrace);
}
return defaultValue;
}
}
@@ -18,9 +18,3 @@ abstract class Activity with _$Activity {
required UserDto user,
}) = _Activity;
}
class ActivityStats {
final int comments;
const ActivityStats({required this.comments});
}
@@ -20,6 +20,7 @@ abstract class LivePhotosMetadata with _$LivePhotosMetadata {
return LivePhotosMetadata(part: LivePhotosPart.values[map['part'] as int], id: map['id'] as String);
}
@visibleForTesting
String toJson() => json.encode(toMap());
factory LivePhotosMetadata.fromJson(String source) =>
@@ -1,13 +0,0 @@
sealed class MapEvent {
const MapEvent();
}
class MapAssetsInBoundsUpdated extends MapEvent {
final List<String> assetRemoteIds;
const MapAssetsInBoundsUpdated(this.assetRemoteIds);
}
class MapCloseBottomSheet extends MapEvent {
const MapCloseBottomSheet();
}
@@ -1,14 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart';
part 'map_marker.model.freezed.dart';
@freezed
abstract class MapMarker with _$MapMarker {
const factory MapMarker({required LatLng latLng, required String assetRemoteId}) = _MapMarker;
factory MapMarker.fromDto(MapMarkerResponseDto dto) =>
MapMarker(latLng: LatLng(dto.lat, dto.lon), assetRemoteId: dto.id);
}
@@ -1,20 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'search_curated_content.model.freezed.dart';
/// A wrapper for [CuratedLocationsResponseDto] objects
/// and [CuratedObjectsResponseDto] to be displayed in
/// a view
@freezed
abstract class SearchCuratedContent with _$SearchCuratedContent {
const factory SearchCuratedContent({
/// The label to show associated with this curated object
required String label,
/// The id to lookup the asset from the server
required String id,
/// The subtitle to show below the label
String? subtitle,
}) = _SearchCuratedContent;
}
@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
@@ -7,105 +5,34 @@ import 'package:immich_mobile/utils/option.dart';
part 'search_filter.model.freezed.dart';
@Freezed(fromJson: false, toJson: false)
@freezed
abstract class SearchLocationFilter with _$SearchLocationFilter {
const SearchLocationFilter._();
const factory SearchLocationFilter({String? country, String? state, String? city}) = _SearchLocationFilter;
Map<String, dynamic> toMap() {
return <String, dynamic>{'country': country, 'state': state, 'city': city};
}
factory SearchLocationFilter.fromMap(Map<String, dynamic> map) {
return SearchLocationFilter(
country: map['country'] != null ? map['country'] as String : null,
state: map['state'] != null ? map['state'] as String : null,
city: map['city'] != null ? map['city'] as String : null,
);
}
String toJson() => json.encode(toMap());
factory SearchLocationFilter.fromJson(String source) =>
SearchLocationFilter.fromMap(json.decode(source) as Map<String, dynamic>);
}
@Freezed(fromJson: false, toJson: false)
@freezed
abstract class SearchCameraFilter with _$SearchCameraFilter {
const SearchCameraFilter._();
const factory SearchCameraFilter({String? make, String? model}) = _SearchCameraFilter;
Map<String, dynamic> toMap() {
return <String, dynamic>{'make': make, 'model': model};
}
factory SearchCameraFilter.fromMap(Map<String, dynamic> map) {
return SearchCameraFilter(
make: map['make'] != null ? map['make'] as String : null,
model: map['model'] != null ? map['model'] as String : null,
);
}
String toJson() => json.encode(toMap());
factory SearchCameraFilter.fromJson(String source) =>
SearchCameraFilter.fromMap(json.decode(source) as Map<String, dynamic>);
}
@Freezed(fromJson: false, toJson: false)
@freezed
abstract class SearchDateFilter with _$SearchDateFilter {
const SearchDateFilter._();
const factory SearchDateFilter({DateTime? takenBefore, DateTime? takenAfter}) = _SearchDateFilter;
Map<String, dynamic> toMap() {
return <String, dynamic>{
'takenBefore': takenBefore?.millisecondsSinceEpoch,
'takenAfter': takenAfter?.millisecondsSinceEpoch,
};
}
factory SearchDateFilter.fromMap(Map<String, dynamic> map) {
return SearchDateFilter(
takenBefore: map['takenBefore'] != null ? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int) : null,
takenAfter: map['takenAfter'] != null ? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int) : null,
);
}
String toJson() => json.encode(toMap());
factory SearchDateFilter.fromJson(String source) =>
SearchDateFilter.fromMap(json.decode(source) as Map<String, dynamic>);
}
@Freezed(fromJson: false, toJson: false)
@freezed
abstract class SearchRatingFilter with _$SearchRatingFilter {
const SearchRatingFilter._();
/// [rating]: none = no filter; some(null) = filter for unrated; some(1-5) = filter for that rating
// TODO(agg23): Switch to enum
const factory SearchRatingFilter({@Default(Option.none()) Option<int?> rating}) = _SearchRatingFilter;
Map<String, dynamic> toMap() {
if (rating.isNone) {
return <String, dynamic>{'active': false};
}
return <String, dynamic>{'active': true, 'value': rating.unwrapOrNull};
}
factory SearchRatingFilter.fromMap(Map<String, dynamic> map) {
if (!(map['active'] as bool? ?? false)) {
return const SearchRatingFilter();
}
return SearchRatingFilter(rating: Option.some(map['value'] as int?));
}
String toJson() => json.encode(toMap());
factory SearchRatingFilter.fromJson(String source) =>
SearchRatingFilter.fromMap(json.decode(source) as Map<String, dynamic>);
}
@freezed
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -11,7 +10,7 @@ enum ShareIntentAttachmentType { image, video }
enum UploadStatus { enqueued, running, complete, failed }
@Freezed(fromJson: false, toJson: false, equal: false)
@Freezed(equal: false)
abstract class ShareIntentAttachment with _$ShareIntentAttachment {
const ShareIntentAttachment._();
@@ -31,33 +30,11 @@ abstract class ShareIntentAttachment with _$ShareIntentAttachment {
bool get isImage => type == ShareIntentAttachmentType.image;
// ignore: unused-code
bool get isVideo => type == ShareIntentAttachmentType.video;
String get fileSize => formatHumanReadableBytes(fileLength, 2);
Map<String, dynamic> toMap() {
return <String, dynamic>{
'path': path,
'type': type.index,
'status': status.index,
'uploadProgress': uploadProgress,
};
}
factory ShareIntentAttachment.fromMap(Map<String, dynamic> map) {
return ShareIntentAttachment(
path: map['path'] as String,
type: ShareIntentAttachmentType.values[map['type'] as int],
status: UploadStatus.values[map['status'] as int],
uploadProgress: map['uploadProgress'] as double,
);
}
String toJson() => json.encode(toMap());
factory ShareIntentAttachment.fromJson(String source) =>
ShareIntentAttachment.fromMap(json.decode(source) as Map<String, dynamic>);
// Identity is sourced from the backing file, not from upload progress
@override
bool operator ==(Object other) {
@@ -11,6 +11,7 @@ extension ViewIntentPayloadX on ViewIntentPayload {
return localAssetId ?? 'view_intent_asset';
}
// ignore: unused-code
bool get isImage => mimeType.toLowerCase().startsWith('image/');
bool get isVideo => mimeType.toLowerCase().startsWith('video/');
@@ -1,52 +1,7 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
class DownloadPanel extends ConsumerWidget {
const DownloadPanel({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final showProgress = ref.watch(downloadStateProvider.select((state) => state.showProgress));
final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList();
void onCancelDownload(String id) {
unawaited(ref.read(downloadStateProvider.notifier).cancelDownload(id));
}
return Positioned(
bottom: 140,
left: 16,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: showProgress
? ConstrainedBox(
constraints: BoxConstraints.loose(Size(context.width - 32, 300)),
child: ListView.builder(
shrinkWrap: true,
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return DownloadTaskTile(
progress: task.value.progress,
fileName: task.value.fileName,
status: task.value.status,
onCancelDownload: () => onCancelDownload(task.key),
);
},
),
)
: const SizedBox.shrink(key: ValueKey('no_progress')),
),
);
}
}
class DownloadTaskTile extends StatelessWidget {
final double progress;
@@ -90,9 +90,9 @@ class FolderPage extends HookConsumerWidget {
body: folderState.when(
data: (rootFolder) {
if (folder == null) {
return FolderContent(folder: rootFolder, root: rootFolder, sortOrder: sortOrder.value);
return FolderContent(folder: rootFolder, sortOrder: sortOrder.value);
} else {
return FolderContent(folder: currentFolder.value, root: rootFolder, sortOrder: sortOrder.value);
return FolderContent(folder: currentFolder.value, sortOrder: sortOrder.value);
}
},
loading: () => const Center(child: CircularProgressIndicator()),
@@ -107,10 +107,9 @@ class FolderPage extends HookConsumerWidget {
class FolderContent extends HookConsumerWidget {
final RootFolder? folder;
final RootFolder root;
final SortOrder sortOrder;
const FolderContent({super.key, this.folder, required this.root, this.sortOrder = SortOrder.asc});
const FolderContent({super.key, this.folder, this.sortOrder = SortOrder.asc});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -143,7 +142,7 @@ class FolderContent extends HookConsumerWidget {
return Column(
children: [
FolderPath(currentFolder: folder!, root: root),
FolderPath(currentFolder: folder!),
Expanded(
child: folderRenderlist.when(
data: (folderAssets) {
@@ -224,9 +223,8 @@ class FolderContent extends HookConsumerWidget {
class FolderPath extends StatelessWidget {
final RootFolder currentFolder;
final RootFolder root;
const FolderPath({super.key, required this.currentFolder, required this.root});
const FolderPath({super.key, required this.currentFolder});
@override
Widget build(BuildContext context) {
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/pages/common/download_panel.dart';
import 'package:immich_mobile/pages/common/download_task_tile.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
@RoutePage()
@@ -8,14 +8,12 @@ import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
class ActivityTextField extends ConsumerStatefulWidget {
final bool isEnabled;
final bool isBottomSheet;
final String? likeId;
final Function(String) onSubmit;
final Function()? onKeyboardFocus;
const ActivityTextField({
required this.onSubmit,
this.isEnabled = true,
this.likeId,
this.onKeyboardFocus,
this.isBottomSheet = false,
super.key,
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@@ -221,52 +220,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
switch (event) {
case TimelineReloadEvent():
_onTimelineReloadEvent();
case ViewerReloadAssetEvent():
_onViewerReloadEvent();
case final ViewerStackAssetDeletedEvent event:
unawaited(_onViewerStackAssetDeletedEvent(event));
default:
}
}
void _onViewerReloadEvent() {
if (_totalAssets <= 1) {
return;
}
final index = _pageController.page?.round() ?? 0;
final target = index >= _totalAssets - 1 ? index - 1 : index + 1;
unawaited(_pageController.animateToPage(target, duration: Durations.medium1, curve: Curves.easeInOut));
unawaited(_onAssetChanged(target));
}
Future<void> _onViewerStackAssetDeletedEvent(ViewerStackAssetDeletedEvent event) async {
final timelineAsset = ref.read(timelineServiceProvider).getAssetSafe(_currentPage);
if (timelineAsset == null) {
_onViewerReloadEvent();
return;
}
final stackProvider = stackChildrenNotifier(timelineAsset);
ref.invalidate(stackProvider);
final stack = await ref.read(stackProvider.future);
if (!mounted) {
return;
}
if (stack.isEmpty) {
_onViewerReloadEvent();
return;
}
final targetIndex = math.min(event.stackIndex, stack.length - 1);
ref.read(assetViewerProvider.notifier)
..setAsset(stack[targetIndex])
..setStackIndex(targetIndex);
}
void _onTimelineReloadEvent() {
final timelineService = ref.read(timelineServiceProvider);
final totalAssets = timelineService.totalAssets;
@@ -1,78 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
class MotionPhotoPlayButton extends ConsumerWidget {
const MotionPhotoPlayButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(assetViewerProvider.select((state) => state.currentAsset));
final isPlaying = ref.watch(isPlayingMotionVideoProvider);
final showControls = ref.watch(assetViewerProvider.select((state) => state.showingControls));
final isShowingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails));
if (asset == null || !asset.isMotionPhoto || isShowingDetails) {
return const SizedBox.shrink();
}
return IgnorePointer(
ignoring: !showControls,
child: AnimatedOpacity(
opacity: showControls ? 1.0 : 0.0,
duration: Durations.short2,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Center(
child: _MotionButton(
isPlaying: isPlaying,
onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle,
),
),
),
),
),
);
}
}
class _MotionButton extends StatelessWidget {
final bool isPlaying;
final VoidCallback onPressed;
const _MotionButton({required this.isPlaying, required this.onPressed});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.grey[900]!.withValues(alpha: 0.4),
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: InkWell(
onTap: onPressed,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
Text(
CurrentPlatform.isAndroid ? context.t.motion : context.t.live,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
),
),
);
}
}
@@ -22,7 +22,6 @@ class NativeVideoViewer extends ConsumerStatefulWidget {
final BaseAsset asset;
final String? localFilePath;
final bool isCurrent;
final bool showControls;
final Widget image;
/// Overrides the user's configured loop video setting
@@ -34,7 +33,6 @@ class NativeVideoViewer extends ConsumerStatefulWidget {
this.localFilePath,
required this.image,
this.isCurrent = false,
this.showControls = true,
this.loopOverride,
});
@@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
import 'package:immich_ui/immich_ui.dart';
@@ -30,7 +29,6 @@ class ViewerKebabMenu extends ConsumerWidget {
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final timelineOrigin = ref.watch(timelineServiceProvider).origin;
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
@@ -40,7 +38,6 @@ class ViewerKebabMenu extends ConsumerWidget {
asset: asset,
isOwner: isOwner,
isArchived: isArchived,
isTrashEnabled: isTrashEnable,
isStacked: asset is RemoteAsset && asset.stackId != null,
isInLockedView: isInLockedView,
currentAlbum: currentAlbum,
@@ -13,8 +13,6 @@ import 'package:logging/logging.dart';
final log = Logger('ThumbnailWidget');
enum ThumbhashMode { enabled, disabled, only }
class Thumbnail extends StatefulWidget {
final ImageProvider? imageProvider;
final ImageProvider? thumbhashProvider;
@@ -17,7 +17,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
this.asset, {
this.size = kThumbnailResolution,
this.remoteSize,
this.fit = BoxFit.cover,
this.showStorageIndicator = false,
this.lockSelection = false,
this.heroOffset,
@@ -30,7 +29,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
/// Physical size to decode for remote thumbnails.
final Size? remoteSize;
final BoxFit fit;
final bool showStorageIndicator;
final bool lockSelection;
final int? heroOffset;
@@ -1,3 +1,4 @@
// TODO: Dedupe against lib/utils/map_utils.dart
import 'dart:async';
import 'package:flutter/material.dart';
@@ -13,17 +14,6 @@ class MapUtils {
static const mapZoomToAssetLevel = 12.0;
static const defaultSourceId = 'asset-map-markers';
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
static var markerCompleter = Completer()..complete();
static const defaultCircleLayerLayerProperties = 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,
);
static const defaultHeatmapLayerProperties = HeatmapLayerProperties(
heatmapColor: [
@@ -14,14 +14,12 @@ class MemoryCard extends StatelessWidget {
final String title;
final bool showTitle;
final bool isCurrent;
final Function()? onVideoEnded;
const MemoryCard({
required this.asset,
required this.title,
required this.showTitle,
this.isCurrent = false,
this.onVideoEnded,
super.key,
});
@@ -65,7 +63,6 @@ class MemoryCard extends StatelessWidget {
key: ValueKey(asset.id),
asset: asset,
isCurrent: isCurrent,
showControls: false,
image: FullImage(asset, size: context.sizeData, fit: BoxFit.contain),
),
),
@@ -68,6 +68,7 @@ class SlideshowController extends ChangeNotifier {
int get currentIndex => _currentIndex;
/// The slide to be displayed next. Null if the slideshow will end after this slide
// ignore: unused-code
int? get nextIndex => _nextIndex;
/// True when the user paused or the slideshow has completed
@@ -10,4 +10,3 @@ const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300);
const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
const Size kThumbnailResolution = Size.square(320);
const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB
@@ -2,13 +2,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
/// Whether to display the video part of a motion photo
final isPlayingMotionVideoProvider = StateNotifierProvider<IsPlayingMotionVideo, bool>((ref) {
return IsPlayingMotionVideo(ref);
return IsPlayingMotionVideo();
});
class IsPlayingMotionVideo extends StateNotifier<bool> {
IsPlayingMotionVideo(this.ref) : super(false);
final Ref ref;
IsPlayingMotionVideo() : super(false);
bool get playing => state;
@@ -1,17 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final showControlsProvider = StateNotifierProvider<ShowControls, bool>((ref) {
return ShowControls(ref);
});
class ShowControls extends StateNotifier<bool> {
ShowControls(this.ref) : super(true);
final Ref ref;
bool get show => state;
set show(bool value) {
state = value;
}
}
@@ -15,11 +15,6 @@ import 'package:logging/logging.dart';
part 'backup.provider.freezed.dart';
@freezed
abstract class EnqueueStatus with _$EnqueueStatus {
const factory EnqueueStatus({required int enqueueCount, required int totalCount}) = _EnqueueStatus;
}
@freezed
abstract class UploadStatus with _$UploadStatus {
const factory UploadStatus({
@@ -16,6 +16,7 @@ class GalleryPermissionNotifier extends StateNotifier<DevicePermissionStatus> {
final DevicePermissionService _service;
// ignore: unused-code
bool get hasPermission => state.hasAccess;
Future<DevicePermissionStatus> requestGalleryPermission() async => state = await _service.requestGallery();
@@ -1,10 +0,0 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final immichLogoProvider = FutureProvider.autoDispose<Uint8List>((ref) async {
final json = await rootBundle.loadString('assets/immich-logo.json');
final j = jsonDecode(json);
return base64Decode(j['content']);
});
@@ -1,9 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/data/db/main/database.dart';
import 'package:immich_mobile/data/store.dart';
@visibleForTesting
Drift Function(Ref ref) driftOverride(Drift drift) => (ref) {
ref.onDispose(() => unawaited(drift.close()));
ref.keepAlive();
@@ -1,30 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/providers/map/map_service.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
final mapMarkersProvider = FutureProvider.autoDispose<List<MapMarker>>((ref) async {
final service = ref.read(mapServiceProvider);
final mapState = ref.read(mapStateNotifierProvider);
DateTime? fileCreatedAfter;
bool? isFavorite;
final bool isIncludeArchived = mapState.includeArchived;
final bool isWithPartners = mapState.withPartners;
if (mapState.relativeTime != 0) {
fileCreatedAfter = DateTime.now().subtract(Duration(days: mapState.relativeTime));
}
if (mapState.showFavoriteOnly) {
isFavorite = true;
}
final markers = await service.getMapMarkers(
isFavorite: isFavorite,
withArchived: isIncludeArchived,
withPartners: isWithPartners,
fileCreatedAfter: fileCreatedAfter,
);
return markers.toList();
});
@@ -1,5 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/map.service.dart';
final mapServiceProvider = Provider.autoDispose<MapService>((ref) => MapService(ref.watch(apiServiceProvider)));
@@ -1,5 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final multiselectProvider = StateProvider((ref) {
return false;
});
@@ -1,36 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/services/search.service.dart';
final getPreviewPlacesProvider = FutureProvider.autoDispose<List<SearchCuratedContent>>((ref) async {
final SearchService searchService = ref.watch(searchServiceProvider);
final exploreData = await searchService.getExploreData();
if (exploreData == null) {
return [];
}
final locations = exploreData.firstWhere((data) => data.fieldName == "exifInfo.city").items;
final curatedContent = locations.map((l) => SearchCuratedContent(label: l.value, id: l.data.id)).toList();
return curatedContent;
});
final getAllPlacesProvider = FutureProvider.autoDispose<List<SearchCuratedContent>>((ref) async {
final SearchService searchService = ref.watch(searchServiceProvider);
final assetPlaces = await searchService.getAllPlaces();
if (assetPlaces == null) {
return [];
}
final curatedContent = assetPlaces
.map((data) => SearchCuratedContent(label: data.exifInfo.orElse(null)!.city.orElse(null)!, id: data.id))
.toList();
return curatedContent;
});
@@ -1,9 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final secureStorageProvider = StateNotifierProvider<SecureStorageProvider, void>((ref) {
return SecureStorageProvider();
});
class SecureStorageProvider extends StateNotifier<void> {
SecureStorageProvider() : super(null);
}
@@ -21,6 +21,7 @@ abstract class SyncStatusState with _$SyncStatusState {
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
bool get isHashing => hashJobStatus == SyncStatus.syncing;
// ignore: unused-code
bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing;
}
@@ -29,10 +29,11 @@ abstract class MultiSelectState with _$MultiSelectState {
bool get hasRemote =>
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
// ignore: unused-code
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);
// ignore: unused-code
bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
// ignore: unused-code
bool get onlyRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote);
}
@@ -5,8 +5,6 @@ import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
enum Direction { sharedWithMe, sharedByMe }
final partnerApiRepositoryProvider = Provider((ref) => PartnerApiRepository(ref.watch(apiServiceProvider).partnersApi));
class PartnerApiRepository extends ApiRepository {
@@ -1,29 +0,0 @@
import 'package:flutter/material.dart';
class CustomTransitionsBuilders {
const CustomTransitionsBuilders._();
static const ZoomPageTransitionsBuilder zoomPageTransitionsBuilder = ZoomPageTransitionsBuilder();
static const RouteTransitionsBuilder zoomedPage = _zoomedPage;
static Widget _zoomedPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return zoomPageTransitionsBuilder.buildTransitions(
// Empty PageRoute<> object, only used to pass allowSnapshotting to ZoomPageTransitionsBuilder
PageRouteBuilder(
allowSnapshotting: true,
fullscreenDialog: false,
pageBuilder: (context, animation, secondaryAnimation) => const SizedBox.shrink(),
),
context,
animation,
secondaryAnimation,
child,
);
}
}
+4
View File
@@ -22,17 +22,21 @@ class ApiService {
late AssetsApi assetsApi;
late SearchApi searchApi;
late ServerApi serverInfoApi;
// ignore: unused-code
late MapApi mapApi;
late PartnersApi partnersApi;
late PeopleApi peopleApi;
late SharedLinksApi sharedLinksApi;
late SyncApi syncApi;
// ignore: unused-code
late SystemConfigApi systemConfigApi;
late ActivitiesApi activitiesApi;
// ignore: unused-code
late DownloadApi downloadApi;
late TrashApi trashApi;
late StacksApi stacksApi;
late ViewsApi viewApi;
// ignore: unused-code
late MemoriesApi memoriesApi;
late SessionsApi sessionsApi;
late TagsApi tagsApi;
@@ -2,15 +2,14 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, false),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, true),
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
const AppSettingsEnum(this.storeKey, this.defaultValue);
final StoreKey<T> storeKey;
final String? hiveKey;
final T defaultValue;
}
@@ -109,7 +109,9 @@ class BackgroundUploadService {
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
// ignore: unused-code
Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream;
// ignore: unused-code
Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream;
bool shouldAbortQueuingTasks = false;
@@ -15,8 +15,6 @@ import 'package:share_plus/share_plus.dart';
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
/// and generate a csv file.
abstract final class ImmichLogger {
const ImmichLogger();
static Future<void> shareLogs(BuildContext context) async {
final tempDir = await getTemporaryDirectory();
final dateTime = DateTime.now().toIso8601String();
-47
View File
@@ -1,47 +0,0 @@
import 'dart:async';
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class MapService with ErrorLoggerMixin {
final ApiService _apiService;
@override
final logger = Logger("MapService");
MapService(this._apiService) {
unawaited(_setMapUserAgentHeader());
}
Future<void> _setMapUserAgentHeader() async {
final userAgent = await getUserAgentString();
await setHttpHeaders({'User-Agent': userAgent});
}
Future<Iterable<MapMarker>> getMapMarkers({
bool? isFavorite,
bool? withArchived,
bool? withPartners,
DateTime? fileCreatedAfter,
DateTime? fileCreatedBefore,
}) async {
return logError(
() async {
final markers = await _apiService.mapApi.getMapMarkers(
isFavorite: isFavorite,
isArchived: withArchived,
withPartners: withPartners,
fileCreatedAfter: fileCreatedAfter,
fileCreatedBefore: fileCreatedBefore,
);
return markers?.map(MapMarker.fromDto) ?? [];
},
defaultValue: [],
errorMessage: "Failed to get map markers",
);
}
}
+2
View File
@@ -39,6 +39,7 @@ class SearchService {
}
}
// ignore: unused-code
Future<List<SearchExploreResponseDto>?> getExploreData() async {
try {
return await _apiService.searchApi.getExploreData();
@@ -48,6 +49,7 @@ class SearchService {
return null;
}
// ignore: unused-code
Future<List<AssetResponseDto>?> getAllPlaces() async {
try {
return await _apiService.searchApi.getAssetsByCity();
@@ -33,7 +33,6 @@ class ActionButtonContext {
final BaseAsset asset;
final bool isOwner;
final bool isArchived;
final bool isTrashEnabled;
final bool isInLockedView;
final bool isStacked;
final RemoteAlbum? currentAlbum;
@@ -47,7 +46,6 @@ class ActionButtonContext {
required this.asset,
required this.isOwner,
required this.isArchived,
required this.isTrashEnabled,
required this.isStacked,
required this.isInLockedView,
required this.currentAlbum,
+1
View File
@@ -5,6 +5,7 @@ class AsyncMutex {
Future _running = Future.value(null);
int _enqueued = 0;
// ignore: unused-code
int get enqueued => _enqueued;
/// Execute [operation] exclusively, after any currently running operations.
+1 -33
View File
@@ -1,7 +1,5 @@
import 'dart:async';
import 'package:flutter_hooks/flutter_hooks.dart';
/// Used to debounce function calls with the [interval] provided.
/// If [maxWaitTime] is provided, the first [run] call as well as the next call since [maxWaitTime] has passed will be immediately executed, even if [interval] is not satisfied.
class Debouncer {
@@ -49,36 +47,6 @@ class Debouncer {
_actionFuture = null;
}
// ignore: unused-code
bool get isActive => _actionFuture != null || (_timer != null && _timer!.isActive);
}
/// Creates a [Debouncer] that will be disposed automatically. If no [interval] is provided, a
/// default interval of 300ms is used to debounce the function calls
Debouncer useDebouncer({
Duration interval = const Duration(milliseconds: 300),
Duration? maxWaitTime,
List<Object?>? keys,
}) => use(_DebouncerHook(interval: interval, maxWaitTime: maxWaitTime, keys: keys));
class _DebouncerHook extends Hook<Debouncer> {
const _DebouncerHook({required this.interval, this.maxWaitTime, super.keys});
final Duration interval;
final Duration? maxWaitTime;
@override
HookState<Debouncer, Hook<Debouncer>> createState() => _DebouncerHookState();
}
class _DebouncerHookState extends HookState<Debouncer, _DebouncerHook> {
late final debouncer = Debouncer(interval: hook.interval, maxWaitTime: hook.maxWaitTime);
@override
Debouncer build(_) => debouncer;
@override
void dispose() => debouncer.dispose();
@override
String get debugLabel => 'useDebouncer';
}
-14
View File
@@ -77,17 +77,3 @@ bool diffSortedListsSync<T>(
}
return diff;
}
int compareToNullable<T extends Comparable>(T? a, T? b) {
if (a == null && b == null) {
return 0;
}
if (a == null) {
return 1;
}
if (b == null) {
return -1;
}
return a.compareTo(b);
}
@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// Creates a [DraggableScrollableController] that will be disposed automatically.
///
/// See also:
/// - [DraggableScrollableController]
DraggableScrollableController useDraggableScrollController({List<Object?>? keys}) {
return use(_DraggableScrollControllerHook(keys: keys));
}
class _DraggableScrollControllerHook extends Hook<DraggableScrollableController> {
const _DraggableScrollControllerHook({super.keys});
@override
HookState<DraggableScrollableController, Hook<DraggableScrollableController>> createState() =>
_DraggableScrollControllerHookState();
}
class _DraggableScrollControllerHookState
extends HookState<DraggableScrollableController, _DraggableScrollControllerHook> {
late final controller = DraggableScrollableController();
@override
DraggableScrollableController build(BuildContext context) => controller;
@override
void dispose() => controller.dispose();
@override
String get debugLabel => 'useDraggableScrollController';
}
-15
View File
@@ -1,15 +0,0 @@
/// FNV-1a 64bit hash algorithm optimized for Dart Strings
int fastHash(String string) {
var hash = 0xcbf29ce484222325;
var i = 0;
while (i < string.length) {
final codeUnit = string.codeUnitAt(i++);
hash ^= codeUnit >> 8;
hash *= 0x100000001b3;
hash ^= codeUnit & 0xFF;
hash *= 0x100000001b3;
}
return hash;
}
@@ -1,9 +0,0 @@
import 'dart:ui'; // Import the dart:ui library for Rect
import 'package:crop_image/crop_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// A hook that provides a [CropController] instance.
CropController useCropController() {
return useMemoized(() => CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1)));
}
-36
View File
@@ -1,36 +0,0 @@
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
RestartableTimer useTimer(Duration duration, void Function() callback) {
return use(_TimerHook(duration: duration, callback: callback));
}
class _TimerHook extends Hook<RestartableTimer> {
final Duration duration;
final void Function() callback;
const _TimerHook({required this.duration, required this.callback});
@override
HookState<RestartableTimer, Hook<RestartableTimer>> createState() => _TimerHookState();
}
class _TimerHookState extends HookState<RestartableTimer, _TimerHook> {
late RestartableTimer timer;
@override
void initHook() {
super.initHook();
timer = RestartableTimer(hook.duration, hook.callback);
}
@override
RestartableTimer build(BuildContext context) {
return timer;
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
}
+1 -37
View File
@@ -1,3 +1,4 @@
// TODO: Dedupe against lib/presentation/widgets/map/map_utils.dart
import 'dart:async';
import 'package:flutter/material.dart';
@@ -5,48 +6,11 @@ import 'package:geolocator/geolocator.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class MapUtils {
const MapUtils._();
static final Logger _log = Logger("MapUtils");
static const defaultSourceId = 'asset-map-markers';
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
static const defaultHeatMapLayerProperties = HeatmapLayerProperties(
heatmapColor: [
Expressions.interpolate,
["linear"],
["heatmap-density"],
0.0,
"rgba(103,58,183,0.0)",
0.3,
"rgb(103,58,183)",
0.5,
"rgb(33,149,243)",
0.7,
"rgb(76,175,79)",
0.95,
"rgb(255,235,59)",
1.0,
"rgb(255,86,34)",
],
heatmapIntensity: [
Expressions.interpolate, ["linear"], //
[Expressions.zoom],
0, 0.5,
9, 2,
],
heatmapRadius: [
Expressions.interpolate, ["linear"], //
[Expressions.zoom],
0, 4,
4, 8,
9, 16,
],
heatmapOpacity: 0.7,
);
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
required BuildContext context,
+1
View File
@@ -45,6 +45,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
'WorkflowResponseDto': {'logging': false},
};
// ignore: unused-code
void upgradeDto(dynamic value, String targetType) {
if (value is! Map) {
return;
@@ -79,6 +79,7 @@ class UploadSpeedCalculator {
}
/// Returns the current speed in MB/s, or -1 if not available.
// ignore: unused-code
double get speed => _currentSpeed;
/// Returns a human-readable string representation of the current speed.
@@ -112,6 +113,7 @@ class UploadSpeedCalculator {
/// Returns a human-readable string representation of time remaining.
///
/// Returns '--:--' if N/A, otherwise HH:MM:SS or MM:SS format.
// ignore: unused-code
String get timeRemainingAsString {
final remaining = timeRemaining;
return switch (remaining.inSeconds) {
-16
View File
@@ -1,16 +0,0 @@
import 'dart:io' show Platform;
import 'package:package_info_plus/package_info_plus.dart';
Future<String> getUserAgentString() async {
final packageInfo = await PackageInfo.fromPlatform();
String platform;
if (Platform.isAndroid) {
platform = 'android';
} else if (Platform.isIOS) {
platform = 'ios';
} else {
platform = 'unknown';
}
return 'immich-$platform/${packageInfo.version}';
}
@@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
class DeleteLocalOnlyDialog extends StatelessWidget {
final void Function(bool onlyMerged) onDeleteLocal;
const DeleteLocalOnlyDialog({super.key, required this.onDeleteLocal});
@override
Widget build(BuildContext context) {
void onDeleteBackedUpOnly() {
context.pop(true);
onDeleteLocal(true);
}
void onForceDelete() {
context.pop(false);
onDeleteLocal(false);
}
return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
title: Text(context.t.delete_dialog_title),
content: Text(context.t.delete_dialog_alert_local_non_backed_up),
actions: [
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: () => context.pop(),
style: FilledButton.styleFrom(
backgroundColor: context.colorScheme.surfaceDim,
foregroundColor: context.primaryColor,
),
child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: onDeleteBackedUpOnly,
style: FilledButton.styleFrom(
backgroundColor: context.colorScheme.errorContainer,
foregroundColor: context.colorScheme.onErrorContainer,
),
child: Text(
context.t.delete_local_dialog_ok_backed_up_only,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: onForceDelete,
style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white),
child: Text(context.t.delete_local_dialog_ok_force, style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
],
);
}
}
@@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_ui/immich_ui.dart';
class PermanentDeleteDialog extends StatelessWidget {
const PermanentDeleteDialog({super.key, required this.count});
final int count;
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
title: Text(context.t.permanently_delete),
content: ImmichFormattedText(context.t.permanently_delete_assets_prompt(count: count)),
actions: [
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: () => context.pop(false),
style: FilledButton.styleFrom(
backgroundColor: context.colorScheme.surfaceDim,
foregroundColor: context.primaryColor,
),
child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: () => context.pop(true),
style: FilledButton.styleFrom(
backgroundColor: context.colorScheme.errorContainer,
foregroundColor: context.colorScheme.onErrorContainer,
),
child: Text(context.t.delete, style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
],
);
}
}
@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
class DelayedLoadingIndicator extends StatelessWidget {
/// The delay to avoid showing the loading indicator
final Duration delay;
/// Defaults to using the [ImmichLoadingIndicator]
final Widget? child;
/// An optional fade in duration to animate the loading
final Duration? fadeInDuration;
const DelayedLoadingIndicator({super.key, this.delay = const Duration(seconds: 3), this.child, this.fadeInDuration});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.delayed(delay),
builder: (context, snapshot) {
late Widget c;
if (snapshot.connectionState == ConnectionState.done) {
c = child ?? const ImmichLoadingIndicator(key: ValueKey('loading'));
} else {
c = Container(key: const ValueKey('hiding'));
}
return AnimatedSwitcher(duration: fadeInDuration ?? Duration.zero, child: c);
},
);
}
}
@@ -1,30 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/common/transparent_image.dart';
class FadeInPlaceholderImage extends StatelessWidget {
final Widget placeholder;
final ImageProvider image;
final Duration duration;
final BoxFit fit;
const FadeInPlaceholderImage({
super.key,
required this.placeholder,
required this.image,
this.duration = const Duration(milliseconds: 100),
this.fit = BoxFit.cover,
});
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
fit: StackFit.expand,
children: [
placeholder,
FadeInImage(fadeInDuration: duration, image: image, fit: fit, placeholder: MemoryImage(kTransparentImage)),
],
),
);
}
}
@@ -36,10 +36,7 @@ class ImmichLoadingIndicator extends HookWidget {
},
child: Padding(
padding: const EdgeInsets.all(15),
child: RotationTransition(
turns: logoAnimationController,
child: const ImmichLogo(heroTag: 'logo'),
),
child: RotationTransition(turns: logoAnimationController, child: const ImmichLogo()),
),
),
);
+1 -2
View File
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
class ImmichLogo extends StatelessWidget {
final double size;
final dynamic heroTag;
const ImmichLogo({super.key, this.size = 100, this.heroTag});
const ImmichLogo({super.key, this.size = 100});
@override
Widget build(BuildContext context) {
@@ -3,9 +3,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
class ImmichTitleText extends StatelessWidget {
final double fontSize;
final Color? color;
const ImmichTitleText({super.key, this.fontSize = 48, this.color});
const ImmichTitleText({super.key, this.fontSize = 48});
@override
Widget build(BuildContext context) {
@@ -1,32 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart';
import 'package:octo_image/octo_image.dart';
OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) {
return (context) => blurhash == null
? const ThumbnailPlaceholder()
: FadeInPlaceholderImage(
placeholder: const ThumbnailPlaceholder(),
image: MemoryImage(blurhash),
fit: fit ?? BoxFit.cover,
);
}
OctoErrorBuilder blurHashErrorBuilder(
Uint8List? blurhash, {
BoxFit? fit,
Text? message,
IconData? icon,
Color? iconColor,
double? iconSize,
}) {
return OctoError.placeholderWithErrorIcon(
blurHashPlaceholderBuilder(blurhash, fit: fit),
message: message,
icon: icon,
iconColor: iconColor,
iconSize: iconSize,
);
}
@@ -1,68 +0,0 @@
import 'dart:typed_data';
final Uint8List kTransparentImage = Uint8List.fromList(<int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
]);
@@ -1,19 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
return CircleAvatar(
radius: radius,
backgroundColor: context.primaryColor.withAlpha(50),
foregroundImage: RemoteImageProvider(url: url),
// silence errors if user has no profile image, use initials as fallback
onForegroundImageError: (exception, stackTrace) {},
child: Text(nameFirstLetter.toUpperCase()),
);
}
@@ -57,7 +57,6 @@ class ChangePasswordForm extends HookConsumerWidget {
),
),
ChangePasswordButton(
passwordController: passwordController,
onPressed: () async {
if (formKey.currentState!.validate()) {
final isSuccess = await ref
@@ -159,9 +158,8 @@ class ConfirmPasswordInput extends StatelessWidget {
}
class ChangePasswordButton extends StatelessWidget {
final TextEditingController passwordController;
final VoidCallback onPressed;
const ChangePasswordButton({super.key, required this.passwordController, required this.onPressed});
const ChangePasswordButton({super.key, required this.onPressed});
@override
Widget build(BuildContext context) {
@@ -554,10 +554,7 @@ class LoginForm extends HookConsumerWidget {
GestureDetector(
onDoubleTap: () => populateTestLoginInfo(),
onLongPress: () => populateTestLoginInfo1(),
child: RotationTransition(
turns: logoAnimationController,
child: const ImmichLogo(heroTag: 'logo'),
),
child: RotationTransition(turns: logoAnimationController, child: const ImmichLogo()),
),
const Padding(padding: EdgeInsets.only(top: 8.0, bottom: 16), child: ImmichTitleText()),
],
@@ -4,10 +4,9 @@ import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.
import 'package:immich_mobile/utils/image_url_builder.dart';
class AssetMarkerIcon extends StatelessWidget {
const AssetMarkerIcon({required this.id, required this.thumbhash, super.key});
const AssetMarkerIcon({required this.id, super.key});
final String id;
final String thumbhash;
@override
Widget build(BuildContext context) {
+1 -1
View File
@@ -106,7 +106,7 @@ class MapThumbnail extends HookWidget {
alignment: Alignment.bottomCenter,
child: SizedBox.square(
dimension: height / 2.5,
child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!),
child: AssetMarkerIcon(id: assetMarkerRemoteId!),
),
),
],
@@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
class SearchRowTitle extends StatelessWidget {
const SearchRowTitle({super.key, required this.onViewAllPressed, required this.title});
final Function() onViewAllPressed;
final String title;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)),
TextButton(
onPressed: onViewAllPressed,
child: Text(
context.t.search_page_view_all_button,
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
),
),
],
);
}
}
@@ -97,7 +97,6 @@ class LanguageSettings extends HookWidget {
return _LanguageItem(
key: ValueKey(localeValue.toString()),
countryName: countryName,
localeValue: localeValue,
isSelected: isSelected,
onTap: () {
selectedLocale.value = localeValue;
@@ -225,16 +224,9 @@ class _LanguageApplyButton extends StatelessWidget {
}
class _LanguageItem extends StatelessWidget {
const _LanguageItem({
super.key,
required this.countryName,
required this.localeValue,
required this.isSelected,
required this.onTap,
});
const _LanguageItem({super.key, required this.countryName, required this.isSelected, required this.onTap});
final String countryName;
final Locale localeValue;
final bool isSelected;
final VoidCallback onTap;
+12
View File
@@ -109,7 +109,15 @@ run = "flutter test"
[tasks.analyze]
alias = "lint"
description = "Analyze Dart code"
usage = '''
flag "--full" help="Check for unused code" default=#false
'''
depends = ["analyze:dart", "analyze:dcm"]
run = '''
if [ "${usage_full}" = "true" ]; then
mise run analyze:unused
fi
'''
[tasks."analyze-fix"]
alias = "lint-fix"
@@ -200,6 +208,10 @@ description = "Run Dart Code Metrics"
hide = true
run = "dcm analyze lib --fatal-style --fatal-warnings"
[tasks."analyze:unused"]
description = "Find unused code"
run = "dcm check-unused-code lib test integration_test --concise"
[tasks."analyze-fix:dart"]
description = "Auto-fix Dart analysis"
hide = true

Some files were not shown because too many files have changed in this diff Show More