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
|
run: mise //mobile:codegen
|
||||||
|
|
||||||
- name: Run analyze
|
- name: Run analyze
|
||||||
run: mise //mobile:analyze
|
run: mise //mobile:analyze --full
|
||||||
|
|
||||||
- name: Run format
|
- name: Run format
|
||||||
run: mise //mobile:format
|
run: mise //mobile:format
|
||||||
|
|||||||
@@ -104,6 +104,19 @@ analyzer:
|
|||||||
deprecated_member_use_from_same_package: error
|
deprecated_member_use_from_same_package: error
|
||||||
|
|
||||||
dart_code_metrics:
|
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:
|
rules:
|
||||||
- banned-usage:
|
- banned-usage:
|
||||||
entries:
|
entries:
|
||||||
|
|||||||
@@ -5,5 +5,3 @@ enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, gr
|
|||||||
const Color immichBrandColorLight = Color(0xFF4150AF);
|
const Color immichBrandColorLight = Color(0xFF4150AF);
|
||||||
const Color immichBrandColorDark = Color(0xFFACCBFA);
|
const Color immichBrandColorDark = Color(0xFFACCBFA);
|
||||||
const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75);
|
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
|
// Sync
|
||||||
const int kSyncEventBatchSize = 5000;
|
const int kSyncEventBatchSize = 5000;
|
||||||
const int kFetchLocalAssetsBatchSize = 40000;
|
|
||||||
|
|
||||||
// Hash batch limits
|
// Hash batch limits
|
||||||
final int kBatchHashFileLimit = Platform.isIOS ? 32 : 512;
|
final int kBatchHashFileLimit = Platform.isIOS ? 32 : 512;
|
||||||
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
|
||||||
|
|
||||||
// Secure storage keys
|
// Secure storage keys
|
||||||
const String kSecuredPinCode = "secured_pin_code";
|
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 kPhotoTabIndex = 0;
|
||||||
const int kSearchTabIndex = 1;
|
const int kSearchTabIndex = 1;
|
||||||
const int kAlbumTabIndex = 2;
|
const int kAlbumTabIndex = 2;
|
||||||
|
// ignore: unused-code
|
||||||
const int kLibraryTabIndex = 3;
|
const int kLibraryTabIndex = 3;
|
||||||
|
|
||||||
// Workaround for SQLite's variable limit (SQLITE_MAX_VARIABLE_NUMBER = 32766)
|
// 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);
|
return (logDb, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
late final PeopleDatabaseRepository peopleDb = PeopleDatabaseRepository(_db);
|
late final PeopleDatabaseRepository peopleDb = PeopleDatabaseRepository(_db);
|
||||||
|
// ignore: unused-code
|
||||||
late final PersonApiRepository personApi = PersonApiRepository(PeopleApi(_apiClient));
|
late final PersonApiRepository personApi = PersonApiRepository(PeopleApi(_apiClient));
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
late final ActivityApiRepository activityApi = ActivityApiRepository(ActivitiesApi(_apiClient));
|
late final ActivityApiRepository activityApi = ActivityApiRepository(ActivitiesApi(_apiClient));
|
||||||
|
|
||||||
/// Direct database access for the logic that has not yet been migrated
|
/// 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])
|
@DriftDatabase(tables: [LogMessageEntity])
|
||||||
class DriftLogger extends $DriftLogger {
|
class DriftLogger extends $DriftLogger {
|
||||||
|
// ignore: unused-code
|
||||||
DriftLogger.fromExecutor(super.executor);
|
DriftLogger.fromExecutor(super.executor);
|
||||||
|
|
||||||
DriftLogger.sqlite(SqliteConnection db) : super(SqliteAsyncDriftConnection(db));
|
DriftLogger.sqlite(SqliteConnection db) : super(SqliteAsyncDriftConnection(db));
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ import 'package:sqlite_async/sqlite_async.dart';
|
|||||||
class Drift extends $Drift {
|
class Drift extends $Drift {
|
||||||
final SqliteConnectionPool? _updatePool;
|
final SqliteConnectionPool? _updatePool;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
Drift(super.executor) : _updatePool = null;
|
Drift(super.executor) : _updatePool = null;
|
||||||
|
|
||||||
Drift.sqlite(SqliteConnection db, SqliteConnectionPool updatePool)
|
Drift.sqlite(SqliteConnection db, SqliteConnectionPool updatePool)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: unused-code
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/data/db/util/datetime_clamp_type.dart';
|
import 'package:immich_mobile/data/db/util/datetime_clamp_type.dart';
|
||||||
import 'package:immich_mobile/data/db/util/defaults_mixin.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';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'asset_metadata.model.freezed.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 hasRemote => storage == AssetState.remote || storage == AssetState.merged;
|
||||||
bool get hasLocal => storage == AssetState.local || storage == AssetState.merged;
|
bool get hasLocal => storage == AssetState.local || storage == AssetState.merged;
|
||||||
|
// ignore: unused-code
|
||||||
bool get isLocalOnly => storage == AssetState.local;
|
bool get isLocalOnly => storage == AssetState.local;
|
||||||
bool get isRemoteOnly => storage == AssetState.remote;
|
bool get isRemoteOnly => storage == AssetState.remote;
|
||||||
bool get isMerged => storage == .merged;
|
bool get isMerged => storage == .merged;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
|
|
||||||
bool get isStacked => stackId != null;
|
bool get isStacked => stackId != null;
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get isArchived => visibility == .archive;
|
bool get isArchived => visibility == .archive;
|
||||||
|
|
||||||
bool get isLocked => visibility == .locked;
|
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();
|
const ViewerShowDetailsEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewerReloadAssetEvent extends Event {
|
|
||||||
const ViewerReloadAssetEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewerStackAssetDeletedEvent extends Event {
|
|
||||||
final int stackIndex;
|
|
||||||
|
|
||||||
const ViewerStackAssetDeletedEvent({required this.stackIndex});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-Select Events
|
// Multi-Select Events
|
||||||
class MultiSelectToggleEvent extends Event {
|
class MultiSelectToggleEvent extends Event {
|
||||||
final bool isEnabled;
|
final bool isEnabled;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ abstract class MemoryData with _$MemoryData {
|
|||||||
return MemoryData(year: map['year'] as int);
|
return MemoryData(year: map['year'] as int);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory MemoryData.fromJson(String source) => MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
|
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';
|
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
|
@freezed
|
||||||
abstract class StackResponse with _$StackResponse {
|
abstract class StackResponse with _$StackResponse {
|
||||||
const factory StackResponse({required String id, required String primaryAssetId, required List<String> assetIds}) =
|
const factory StackResponse({required String id, required String primaryAssetId, required List<String> assetIds}) =
|
||||||
|
|||||||
@@ -101,17 +101,6 @@ abstract class UserDto with _$UserDto {
|
|||||||
quotaUsageInBytes.hashCode;
|
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 {
|
class User {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@@ -152,50 +141,10 @@ class User {
|
|||||||
int get hashCode => Object.hash(id, name, email, profileChangedAt, hasProfileImage, avatarColor);
|
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 {
|
class Partner extends User {
|
||||||
final bool inTimeline;
|
final bool inTimeline;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
const Partner({
|
const Partner({
|
||||||
required super.id,
|
required super.id,
|
||||||
required super.name,
|
required super.name,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class BackgroundWorkerFgService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
Future<void> disable() => _foregroundHostApi.disable();
|
Future<void> disable() => _foregroundHostApi.disable();
|
||||||
|
|
||||||
Future<bool> wasLaunchedInBackground() => _foregroundHostApi.wasLaunchedInBackground();
|
Future<bool> wasLaunchedInBackground() => _foregroundHostApi.wasLaunchedInBackground();
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/memory.repository.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
|
/// Accesses Memories; a specialized collection of assets with some novel display mechanism
|
||||||
class MemoryService {
|
class MemoryService {
|
||||||
final log = Logger("MemoryService");
|
|
||||||
|
|
||||||
final MemoryRepository _repository;
|
final MemoryRepository _repository;
|
||||||
|
|
||||||
MemoryService(this._repository);
|
const MemoryService(this._repository);
|
||||||
|
|
||||||
Future<List<Memory>> getMemoryLane(String ownerId) {
|
Future<List<Memory>> getMemoryLane(String ownerId) {
|
||||||
return _repository.getAll(ownerId);
|
return _repository.getAll(ownerId);
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.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 {
|
class SettingsService {
|
||||||
final StoreService _storeService;
|
final StoreService _storeService;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.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)
|
/// Clears all values from the store (cache and DB)
|
||||||
|
@visibleForTesting
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
await _storeRepository.deleteAll();
|
await _storeRepository.deleteAll();
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||||
|
|
||||||
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
|
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
|
||||||
|
// ignore: unused-code
|
||||||
bool get hasCellular => contains(NetworkCapability.cellular);
|
bool get hasCellular => contains(NetworkCapability.cellular);
|
||||||
|
// ignore: unused-code
|
||||||
bool get hasWifi => contains(NetworkCapability.wifi);
|
bool get hasWifi => contains(NetworkCapability.wifi);
|
||||||
|
// ignore: unused-code
|
||||||
bool get hasVpn => contains(NetworkCapability.vpn);
|
bool get hasVpn => contains(NetworkCapability.vpn);
|
||||||
bool get isUnmetered => contains(NetworkCapability.unmetered);
|
bool get isUnmetered => contains(NetworkCapability.unmetered);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:diacritic/diacritic.dart' as diacritic;
|
import 'package:diacritic/diacritic.dart' as diacritic;
|
||||||
|
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
@@ -32,11 +30,3 @@ extension DurationExtension on String {
|
|||||||
return int.parse(this);
|
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++;
|
final int requestId = _nextRequestId++;
|
||||||
bool _isCancelled = false;
|
bool _isCancelled = false;
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get isCancelled => _isCancelled;
|
bool get isCancelled => _isCancelled;
|
||||||
|
|
||||||
ImageRequest();
|
ImageRequest();
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ class NetworkRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NetworkRepository();
|
|
||||||
|
|
||||||
/// Returns a shared HTTP client that uses native SSL configuration.
|
/// Returns a shared HTTP client that uses native SSL configuration.
|
||||||
///
|
///
|
||||||
/// On iOS: Uses SharedURLSessionManager's URLSession.
|
/// On iOS: Uses SharedURLSessionManager's URLSession.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/data/db/main/database.dart';
|
import 'package:immich_mobile/data/db/main/database.dart';
|
||||||
import 'package:immich_mobile/data/db/main/table/user/partner.drift.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;
|
Drift get _db => attachedDatabase;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
Future<Partner> get({required String sharedById, required String sharedWithId}) =>
|
Future<Partner> get({required String sharedById, required String sharedWithId}) =>
|
||||||
(_db.select(_db.partnerEntity).join([
|
(_db.select(_db.partnerEntity).join([
|
||||||
innerJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById)),
|
innerJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById)),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:drift/drift.dart';
|
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/database.dart';
|
||||||
import 'package:immich_mobile/data/db/main/table/app/store.drift.dart';
|
import 'package:immich_mobile/data/db/main/table/app/store.drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
@@ -40,6 +41,7 @@ class StoreRepository extends DatabaseAccessor<Drift> with $StoreRepositoryMixin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
||||||
final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull();
|
final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull();
|
||||||
if (entity == null) {
|
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,
|
required UserDto user,
|
||||||
}) = _Activity;
|
}) = _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);
|
return LivePhotosMetadata(part: LivePhotosPart.values[map['part'] as int], id: map['id'] as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory LivePhotosMetadata.fromJson(String source) =>
|
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:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/person.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';
|
part 'search_filter.model.freezed.dart';
|
||||||
|
|
||||||
@Freezed(fromJson: false, toJson: false)
|
@freezed
|
||||||
abstract class SearchLocationFilter with _$SearchLocationFilter {
|
abstract class SearchLocationFilter with _$SearchLocationFilter {
|
||||||
const SearchLocationFilter._();
|
const SearchLocationFilter._();
|
||||||
|
|
||||||
const factory SearchLocationFilter({String? country, String? state, String? city}) = _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 {
|
abstract class SearchCameraFilter with _$SearchCameraFilter {
|
||||||
const SearchCameraFilter._();
|
const SearchCameraFilter._();
|
||||||
|
|
||||||
const factory SearchCameraFilter({String? make, String? model}) = _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 {
|
abstract class SearchDateFilter with _$SearchDateFilter {
|
||||||
const SearchDateFilter._();
|
const SearchDateFilter._();
|
||||||
|
|
||||||
const factory SearchDateFilter({DateTime? takenBefore, DateTime? takenAfter}) = _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 {
|
abstract class SearchRatingFilter with _$SearchRatingFilter {
|
||||||
const SearchRatingFilter._();
|
const SearchRatingFilter._();
|
||||||
|
|
||||||
/// [rating]: none = no filter; some(null) = filter for unrated; some(1-5) = filter for that rating
|
/// [rating]: none = no filter; some(null) = filter for unrated; some(1-5) = filter for that rating
|
||||||
// TODO(agg23): Switch to enum
|
// TODO(agg23): Switch to enum
|
||||||
const factory SearchRatingFilter({@Default(Option.none()) Option<int?> rating}) = _SearchRatingFilter;
|
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
|
@freezed
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
@@ -11,7 +10,7 @@ enum ShareIntentAttachmentType { image, video }
|
|||||||
|
|
||||||
enum UploadStatus { enqueued, running, complete, failed }
|
enum UploadStatus { enqueued, running, complete, failed }
|
||||||
|
|
||||||
@Freezed(fromJson: false, toJson: false, equal: false)
|
@Freezed(equal: false)
|
||||||
abstract class ShareIntentAttachment with _$ShareIntentAttachment {
|
abstract class ShareIntentAttachment with _$ShareIntentAttachment {
|
||||||
const ShareIntentAttachment._();
|
const ShareIntentAttachment._();
|
||||||
|
|
||||||
@@ -31,33 +30,11 @@ abstract class ShareIntentAttachment with _$ShareIntentAttachment {
|
|||||||
|
|
||||||
bool get isImage => type == ShareIntentAttachmentType.image;
|
bool get isImage => type == ShareIntentAttachmentType.image;
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get isVideo => type == ShareIntentAttachmentType.video;
|
bool get isVideo => type == ShareIntentAttachmentType.video;
|
||||||
|
|
||||||
String get fileSize => formatHumanReadableBytes(fileLength, 2);
|
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
|
// Identity is sourced from the backing file, not from upload progress
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extension ViewIntentPayloadX on ViewIntentPayload {
|
|||||||
return localAssetId ?? 'view_intent_asset';
|
return localAssetId ?? 'view_intent_asset';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get isImage => mimeType.toLowerCase().startsWith('image/');
|
bool get isImage => mimeType.toLowerCase().startsWith('image/');
|
||||||
|
|
||||||
bool get isVideo => mimeType.toLowerCase().startsWith('video/');
|
bool get isVideo => mimeType.toLowerCase().startsWith('video/');
|
||||||
|
|||||||
-45
@@ -1,52 +1,7 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:flutter/material.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.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 {
|
class DownloadTaskTile extends StatelessWidget {
|
||||||
final double progress;
|
final double progress;
|
||||||
@@ -90,9 +90,9 @@ class FolderPage extends HookConsumerWidget {
|
|||||||
body: folderState.when(
|
body: folderState.when(
|
||||||
data: (rootFolder) {
|
data: (rootFolder) {
|
||||||
if (folder == null) {
|
if (folder == null) {
|
||||||
return FolderContent(folder: rootFolder, root: rootFolder, sortOrder: sortOrder.value);
|
return FolderContent(folder: rootFolder, sortOrder: sortOrder.value);
|
||||||
} else {
|
} else {
|
||||||
return FolderContent(folder: currentFolder.value, root: rootFolder, sortOrder: sortOrder.value);
|
return FolderContent(folder: currentFolder.value, sortOrder: sortOrder.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
@@ -107,10 +107,9 @@ class FolderPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
class FolderContent extends HookConsumerWidget {
|
class FolderContent extends HookConsumerWidget {
|
||||||
final RootFolder? folder;
|
final RootFolder? folder;
|
||||||
final RootFolder root;
|
|
||||||
final SortOrder sortOrder;
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -143,7 +142,7 @@ class FolderContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
FolderPath(currentFolder: folder!, root: root),
|
FolderPath(currentFolder: folder!),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: folderRenderlist.when(
|
child: folderRenderlist.when(
|
||||||
data: (folderAssets) {
|
data: (folderAssets) {
|
||||||
@@ -224,9 +223,8 @@ class FolderContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
class FolderPath extends StatelessWidget {
|
class FolderPath extends StatelessWidget {
|
||||||
final RootFolder currentFolder;
|
final RootFolder currentFolder;
|
||||||
final RootFolder root;
|
|
||||||
|
|
||||||
const FolderPath({super.key, required this.currentFolder, required this.root});
|
const FolderPath({super.key, required this.currentFolder});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.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';
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
|||||||
class ActivityTextField extends ConsumerStatefulWidget {
|
class ActivityTextField extends ConsumerStatefulWidget {
|
||||||
final bool isEnabled;
|
final bool isEnabled;
|
||||||
final bool isBottomSheet;
|
final bool isBottomSheet;
|
||||||
final String? likeId;
|
|
||||||
final Function(String) onSubmit;
|
final Function(String) onSubmit;
|
||||||
final Function()? onKeyboardFocus;
|
final Function()? onKeyboardFocus;
|
||||||
|
|
||||||
const ActivityTextField({
|
const ActivityTextField({
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
this.isEnabled = true,
|
this.isEnabled = true,
|
||||||
this.likeId,
|
|
||||||
this.onKeyboardFocus,
|
this.onKeyboardFocus,
|
||||||
this.isBottomSheet = false,
|
this.isBottomSheet = false,
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -221,52 +220,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
case TimelineReloadEvent():
|
case TimelineReloadEvent():
|
||||||
_onTimelineReloadEvent();
|
_onTimelineReloadEvent();
|
||||||
case ViewerReloadAssetEvent():
|
|
||||||
_onViewerReloadEvent();
|
|
||||||
case final ViewerStackAssetDeletedEvent event:
|
|
||||||
unawaited(_onViewerStackAssetDeletedEvent(event));
|
|
||||||
default:
|
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() {
|
void _onTimelineReloadEvent() {
|
||||||
final timelineService = ref.read(timelineServiceProvider);
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
final totalAssets = timelineService.totalAssets;
|
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 BaseAsset asset;
|
||||||
final String? localFilePath;
|
final String? localFilePath;
|
||||||
final bool isCurrent;
|
final bool isCurrent;
|
||||||
final bool showControls;
|
|
||||||
final Widget image;
|
final Widget image;
|
||||||
|
|
||||||
/// Overrides the user's configured loop video setting
|
/// Overrides the user's configured loop video setting
|
||||||
@@ -34,7 +33,6 @@ class NativeVideoViewer extends ConsumerStatefulWidget {
|
|||||||
this.localFilePath,
|
this.localFilePath,
|
||||||
required this.image,
|
required this.image,
|
||||||
this.isCurrent = false,
|
this.isCurrent = false,
|
||||||
this.showControls = true,
|
|
||||||
this.loopOverride,
|
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/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.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/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
import 'package:immich_ui/immich_ui.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 isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
final timelineOrigin = ref.watch(timelineServiceProvider).origin;
|
final timelineOrigin = ref.watch(timelineServiceProvider).origin;
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||||
@@ -40,7 +38,6 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
isTrashEnabled: isTrashEnable,
|
|
||||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||||
isInLockedView: isInLockedView,
|
isInLockedView: isInLockedView,
|
||||||
currentAlbum: currentAlbum,
|
currentAlbum: currentAlbum,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import 'package:logging/logging.dart';
|
|||||||
|
|
||||||
final log = Logger('ThumbnailWidget');
|
final log = Logger('ThumbnailWidget');
|
||||||
|
|
||||||
enum ThumbhashMode { enabled, disabled, only }
|
|
||||||
|
|
||||||
class Thumbnail extends StatefulWidget {
|
class Thumbnail extends StatefulWidget {
|
||||||
final ImageProvider? imageProvider;
|
final ImageProvider? imageProvider;
|
||||||
final ImageProvider? thumbhashProvider;
|
final ImageProvider? thumbhashProvider;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
|
|||||||
this.asset, {
|
this.asset, {
|
||||||
this.size = kThumbnailResolution,
|
this.size = kThumbnailResolution,
|
||||||
this.remoteSize,
|
this.remoteSize,
|
||||||
this.fit = BoxFit.cover,
|
|
||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator = false,
|
||||||
this.lockSelection = false,
|
this.lockSelection = false,
|
||||||
this.heroOffset,
|
this.heroOffset,
|
||||||
@@ -30,7 +29,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
/// Physical size to decode for remote thumbnails.
|
/// Physical size to decode for remote thumbnails.
|
||||||
final Size? remoteSize;
|
final Size? remoteSize;
|
||||||
final BoxFit fit;
|
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool lockSelection;
|
final bool lockSelection;
|
||||||
final int? heroOffset;
|
final int? heroOffset;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// TODO: Dedupe against lib/utils/map_utils.dart
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -13,17 +14,6 @@ class MapUtils {
|
|||||||
static const mapZoomToAssetLevel = 12.0;
|
static const mapZoomToAssetLevel = 12.0;
|
||||||
static const defaultSourceId = 'asset-map-markers';
|
static const defaultSourceId = 'asset-map-markers';
|
||||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
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(
|
static const defaultHeatmapLayerProperties = HeatmapLayerProperties(
|
||||||
heatmapColor: [
|
heatmapColor: [
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ class MemoryCard extends StatelessWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final bool showTitle;
|
final bool showTitle;
|
||||||
final bool isCurrent;
|
final bool isCurrent;
|
||||||
final Function()? onVideoEnded;
|
|
||||||
|
|
||||||
const MemoryCard({
|
const MemoryCard({
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.showTitle,
|
required this.showTitle,
|
||||||
this.isCurrent = false,
|
this.isCurrent = false,
|
||||||
this.onVideoEnded,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,7 +63,6 @@ class MemoryCard extends StatelessWidget {
|
|||||||
key: ValueKey(asset.id),
|
key: ValueKey(asset.id),
|
||||||
asset: asset,
|
asset: asset,
|
||||||
isCurrent: isCurrent,
|
isCurrent: isCurrent,
|
||||||
showControls: false,
|
|
||||||
image: FullImage(asset, size: context.sizeData, fit: BoxFit.contain),
|
image: FullImage(asset, size: context.sizeData, fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class SlideshowController extends ChangeNotifier {
|
|||||||
int get currentIndex => _currentIndex;
|
int get currentIndex => _currentIndex;
|
||||||
|
|
||||||
/// The slide to be displayed next. Null if the slideshow will end after this slide
|
/// The slide to be displayed next. Null if the slideshow will end after this slide
|
||||||
|
// ignore: unused-code
|
||||||
int? get nextIndex => _nextIndex;
|
int? get nextIndex => _nextIndex;
|
||||||
|
|
||||||
/// True when the user paused or the slideshow has completed
|
/// 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 Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
|
||||||
|
|
||||||
const Size kThumbnailResolution = Size.square(320);
|
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
|
/// Whether to display the video part of a motion photo
|
||||||
final isPlayingMotionVideoProvider = StateNotifierProvider<IsPlayingMotionVideo, bool>((ref) {
|
final isPlayingMotionVideoProvider = StateNotifierProvider<IsPlayingMotionVideo, bool>((ref) {
|
||||||
return IsPlayingMotionVideo(ref);
|
return IsPlayingMotionVideo();
|
||||||
});
|
});
|
||||||
|
|
||||||
class IsPlayingMotionVideo extends StateNotifier<bool> {
|
class IsPlayingMotionVideo extends StateNotifier<bool> {
|
||||||
IsPlayingMotionVideo(this.ref) : super(false);
|
IsPlayingMotionVideo() : super(false);
|
||||||
|
|
||||||
final Ref ref;
|
|
||||||
|
|
||||||
bool get playing => state;
|
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';
|
part 'backup.provider.freezed.dart';
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class EnqueueStatus with _$EnqueueStatus {
|
|
||||||
const factory EnqueueStatus({required int enqueueCount, required int totalCount}) = _EnqueueStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class UploadStatus with _$UploadStatus {
|
abstract class UploadStatus with _$UploadStatus {
|
||||||
const factory UploadStatus({
|
const factory UploadStatus({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class GalleryPermissionNotifier extends StateNotifier<DevicePermissionStatus> {
|
|||||||
|
|
||||||
final DevicePermissionService _service;
|
final DevicePermissionService _service;
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get hasPermission => state.hasAccess;
|
bool get hasPermission => state.hasAccess;
|
||||||
|
|
||||||
Future<DevicePermissionStatus> requestGalleryPermission() async => state = await _service.requestGallery();
|
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 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/data/db/main/database.dart';
|
import 'package:immich_mobile/data/db/main/database.dart';
|
||||||
import 'package:immich_mobile/data/store.dart';
|
import 'package:immich_mobile/data/store.dart';
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
Drift Function(Ref ref) driftOverride(Drift drift) => (ref) {
|
Drift Function(Ref ref) driftOverride(Drift drift) => (ref) {
|
||||||
ref.onDispose(() => unawaited(drift.close()));
|
ref.onDispose(() => unawaited(drift.close()));
|
||||||
ref.keepAlive();
|
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 isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
|
||||||
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
|
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
|
||||||
bool get isHashing => hashJobStatus == SyncStatus.syncing;
|
bool get isHashing => hashJobStatus == SyncStatus.syncing;
|
||||||
|
// ignore: unused-code
|
||||||
bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing;
|
bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ abstract class MultiSelectState with _$MultiSelectState {
|
|||||||
bool get hasRemote =>
|
bool get hasRemote =>
|
||||||
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
|
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);
|
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);
|
||||||
|
// ignore: unused-code
|
||||||
bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
|
bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
|
||||||
|
// ignore: unused-code
|
||||||
bool get onlyRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote);
|
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:immich_mobile/repositories/api.repository.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
enum Direction { sharedWithMe, sharedByMe }
|
|
||||||
|
|
||||||
final partnerApiRepositoryProvider = Provider((ref) => PartnerApiRepository(ref.watch(apiServiceProvider).partnersApi));
|
final partnerApiRepositoryProvider = Provider((ref) => PartnerApiRepository(ref.watch(apiServiceProvider).partnersApi));
|
||||||
|
|
||||||
class PartnerApiRepository extends ApiRepository {
|
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 AssetsApi assetsApi;
|
||||||
late SearchApi searchApi;
|
late SearchApi searchApi;
|
||||||
late ServerApi serverInfoApi;
|
late ServerApi serverInfoApi;
|
||||||
|
// ignore: unused-code
|
||||||
late MapApi mapApi;
|
late MapApi mapApi;
|
||||||
late PartnersApi partnersApi;
|
late PartnersApi partnersApi;
|
||||||
late PeopleApi peopleApi;
|
late PeopleApi peopleApi;
|
||||||
late SharedLinksApi sharedLinksApi;
|
late SharedLinksApi sharedLinksApi;
|
||||||
late SyncApi syncApi;
|
late SyncApi syncApi;
|
||||||
|
// ignore: unused-code
|
||||||
late SystemConfigApi systemConfigApi;
|
late SystemConfigApi systemConfigApi;
|
||||||
late ActivitiesApi activitiesApi;
|
late ActivitiesApi activitiesApi;
|
||||||
|
// ignore: unused-code
|
||||||
late DownloadApi downloadApi;
|
late DownloadApi downloadApi;
|
||||||
late TrashApi trashApi;
|
late TrashApi trashApi;
|
||||||
late StacksApi stacksApi;
|
late StacksApi stacksApi;
|
||||||
late ViewsApi viewApi;
|
late ViewsApi viewApi;
|
||||||
|
// ignore: unused-code
|
||||||
late MemoriesApi memoriesApi;
|
late MemoriesApi memoriesApi;
|
||||||
late SessionsApi sessionsApi;
|
late SessionsApi sessionsApi;
|
||||||
late TagsApi tagsApi;
|
late TagsApi tagsApi;
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
|
||||||
enum AppSettingsEnum<T> {
|
enum AppSettingsEnum<T> {
|
||||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, false),
|
||||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, true),
|
||||||
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
|
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, false);
|
||||||
|
|
||||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
const AppSettingsEnum(this.storeKey, this.defaultValue);
|
||||||
|
|
||||||
final StoreKey<T> storeKey;
|
final StoreKey<T> storeKey;
|
||||||
final String? hiveKey;
|
|
||||||
final T defaultValue;
|
final T defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ class BackgroundUploadService {
|
|||||||
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
|
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
|
||||||
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
|
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream;
|
Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream;
|
||||||
|
// ignore: unused-code
|
||||||
Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream;
|
Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream;
|
||||||
|
|
||||||
bool shouldAbortQueuingTasks = false;
|
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
|
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||||
/// and generate a csv file.
|
/// and generate a csv file.
|
||||||
abstract final class ImmichLogger {
|
abstract final class ImmichLogger {
|
||||||
const ImmichLogger();
|
|
||||||
|
|
||||||
static Future<void> shareLogs(BuildContext context) async {
|
static Future<void> shareLogs(BuildContext context) async {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final dateTime = DateTime.now().toIso8601String();
|
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 {
|
Future<List<SearchExploreResponseDto>?> getExploreData() async {
|
||||||
try {
|
try {
|
||||||
return await _apiService.searchApi.getExploreData();
|
return await _apiService.searchApi.getExploreData();
|
||||||
@@ -48,6 +49,7 @@ class SearchService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
Future<List<AssetResponseDto>?> getAllPlaces() async {
|
Future<List<AssetResponseDto>?> getAllPlaces() async {
|
||||||
try {
|
try {
|
||||||
return await _apiService.searchApi.getAssetsByCity();
|
return await _apiService.searchApi.getAssetsByCity();
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class ActionButtonContext {
|
|||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
final bool isArchived;
|
final bool isArchived;
|
||||||
final bool isTrashEnabled;
|
|
||||||
final bool isInLockedView;
|
final bool isInLockedView;
|
||||||
final bool isStacked;
|
final bool isStacked;
|
||||||
final RemoteAlbum? currentAlbum;
|
final RemoteAlbum? currentAlbum;
|
||||||
@@ -47,7 +46,6 @@ class ActionButtonContext {
|
|||||||
required this.asset,
|
required this.asset,
|
||||||
required this.isOwner,
|
required this.isOwner,
|
||||||
required this.isArchived,
|
required this.isArchived,
|
||||||
required this.isTrashEnabled,
|
|
||||||
required this.isStacked,
|
required this.isStacked,
|
||||||
required this.isInLockedView,
|
required this.isInLockedView,
|
||||||
required this.currentAlbum,
|
required this.currentAlbum,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class AsyncMutex {
|
|||||||
Future _running = Future.value(null);
|
Future _running = Future.value(null);
|
||||||
int _enqueued = 0;
|
int _enqueued = 0;
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
int get enqueued => _enqueued;
|
int get enqueued => _enqueued;
|
||||||
|
|
||||||
/// Execute [operation] exclusively, after any currently running operations.
|
/// Execute [operation] exclusively, after any currently running operations.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
|
|
||||||
/// Used to debounce function calls with the [interval] provided.
|
/// 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.
|
/// 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 {
|
class Debouncer {
|
||||||
@@ -49,36 +47,6 @@ class Debouncer {
|
|||||||
_actionFuture = null;
|
_actionFuture = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
bool get isActive => _actionFuture != null || (_timer != null && _timer!.isActive);
|
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;
|
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 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
||||||
|
|
||||||
class MapUtils {
|
class MapUtils {
|
||||||
const MapUtils._();
|
const MapUtils._();
|
||||||
|
|
||||||
static final Logger _log = Logger("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({
|
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
|||||||
'WorkflowResponseDto': {'logging': false},
|
'WorkflowResponseDto': {'logging': false},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ignore: unused-code
|
||||||
void upgradeDto(dynamic value, String targetType) {
|
void upgradeDto(dynamic value, String targetType) {
|
||||||
if (value is! Map) {
|
if (value is! Map) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class UploadSpeedCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current speed in MB/s, or -1 if not available.
|
/// Returns the current speed in MB/s, or -1 if not available.
|
||||||
|
// ignore: unused-code
|
||||||
double get speed => _currentSpeed;
|
double get speed => _currentSpeed;
|
||||||
|
|
||||||
/// Returns a human-readable string representation of the current speed.
|
/// 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 a human-readable string representation of time remaining.
|
||||||
///
|
///
|
||||||
/// Returns '--:--' if N/A, otherwise HH:MM:SS or MM:SS format.
|
/// Returns '--:--' if N/A, otherwise HH:MM:SS or MM:SS format.
|
||||||
|
// ignore: unused-code
|
||||||
String get timeRemainingAsString {
|
String get timeRemainingAsString {
|
||||||
final remaining = timeRemaining;
|
final remaining = timeRemaining;
|
||||||
return switch (remaining.inSeconds) {
|
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
child: RotationTransition(
|
child: RotationTransition(turns: logoAnimationController, child: const ImmichLogo()),
|
||||||
turns: logoAnimationController,
|
|
||||||
child: const ImmichLogo(heroTag: 'logo'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class ImmichLogo extends StatelessWidget {
|
class ImmichLogo extends StatelessWidget {
|
||||||
final double size;
|
final double size;
|
||||||
final dynamic heroTag;
|
|
||||||
|
|
||||||
const ImmichLogo({super.key, this.size = 100, this.heroTag});
|
const ImmichLogo({super.key, this.size = 100});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
|
|
||||||
class ImmichTitleText extends StatelessWidget {
|
class ImmichTitleText extends StatelessWidget {
|
||||||
final double fontSize;
|
final double fontSize;
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
const ImmichTitleText({super.key, this.fontSize = 48, this.color});
|
const ImmichTitleText({super.key, this.fontSize = 48});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
ChangePasswordButton(
|
||||||
passwordController: passwordController,
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (formKey.currentState!.validate()) {
|
if (formKey.currentState!.validate()) {
|
||||||
final isSuccess = await ref
|
final isSuccess = await ref
|
||||||
@@ -159,9 +158,8 @@ class ConfirmPasswordInput extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChangePasswordButton extends StatelessWidget {
|
class ChangePasswordButton extends StatelessWidget {
|
||||||
final TextEditingController passwordController;
|
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
const ChangePasswordButton({super.key, required this.passwordController, required this.onPressed});
|
const ChangePasswordButton({super.key, required this.onPressed});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -554,10 +554,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onDoubleTap: () => populateTestLoginInfo(),
|
onDoubleTap: () => populateTestLoginInfo(),
|
||||||
onLongPress: () => populateTestLoginInfo1(),
|
onLongPress: () => populateTestLoginInfo1(),
|
||||||
child: RotationTransition(
|
child: RotationTransition(turns: logoAnimationController, child: const ImmichLogo()),
|
||||||
turns: logoAnimationController,
|
|
||||||
child: const ImmichLogo(heroTag: 'logo'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.only(top: 8.0, bottom: 16), child: ImmichTitleText()),
|
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';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
class AssetMarkerIcon extends StatelessWidget {
|
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 id;
|
||||||
final String thumbhash;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class MapThumbnail extends HookWidget {
|
|||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: height / 2.5,
|
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(
|
return _LanguageItem(
|
||||||
key: ValueKey(localeValue.toString()),
|
key: ValueKey(localeValue.toString()),
|
||||||
countryName: countryName,
|
countryName: countryName,
|
||||||
localeValue: localeValue,
|
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
selectedLocale.value = localeValue;
|
selectedLocale.value = localeValue;
|
||||||
@@ -225,16 +224,9 @@ class _LanguageApplyButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LanguageItem extends StatelessWidget {
|
class _LanguageItem extends StatelessWidget {
|
||||||
const _LanguageItem({
|
const _LanguageItem({super.key, required this.countryName, required this.isSelected, required this.onTap});
|
||||||
super.key,
|
|
||||||
required this.countryName,
|
|
||||||
required this.localeValue,
|
|
||||||
required this.isSelected,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String countryName;
|
final String countryName;
|
||||||
final Locale localeValue;
|
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,15 @@ run = "flutter test"
|
|||||||
[tasks.analyze]
|
[tasks.analyze]
|
||||||
alias = "lint"
|
alias = "lint"
|
||||||
description = "Analyze Dart code"
|
description = "Analyze Dart code"
|
||||||
|
usage = '''
|
||||||
|
flag "--full" help="Check for unused code" default=#false
|
||||||
|
'''
|
||||||
depends = ["analyze:dart", "analyze:dcm"]
|
depends = ["analyze:dart", "analyze:dcm"]
|
||||||
|
run = '''
|
||||||
|
if [ "${usage_full}" = "true" ]; then
|
||||||
|
mise run analyze:unused
|
||||||
|
fi
|
||||||
|
'''
|
||||||
|
|
||||||
[tasks."analyze-fix"]
|
[tasks."analyze-fix"]
|
||||||
alias = "lint-fix"
|
alias = "lint-fix"
|
||||||
@@ -200,6 +208,10 @@ description = "Run Dart Code Metrics"
|
|||||||
hide = true
|
hide = true
|
||||||
run = "dcm analyze lib --fatal-style --fatal-warnings"
|
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"]
|
[tasks."analyze-fix:dart"]
|
||||||
description = "Auto-fix Dart analysis"
|
description = "Auto-fix Dart analysis"
|
||||||
hide = true
|
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