mirror of
https://github.com/immich-app/immich.git
synced 2026-09-30 13:23:21 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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}) =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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/');
|
||||
|
||||
-45
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user