mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
feat(mobile): sqlite (#16861)
* refactor: user entity * chore: rebase fixes * refactor: remove int user Id * refactor: migrate store userId from int to string * refactor: rename uid to id * feat: drift * pr feedback * refactor: move common overrides to mixin --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
5cb5fcbf62
commit
5a456ef277
23 changed files with 321 additions and 30 deletions
3
.gitattributes
vendored
3
.gitattributes
vendored
|
|
@ -6,6 +6,9 @@ mobile/openapi/**/*.dart linguist-generated=true
|
|||
mobile/lib/**/*.g.dart -diff -merge
|
||||
mobile/lib/**/*.g.dart linguist-generated=true
|
||||
|
||||
mobile/lib/**/*.drift.dart -diff -merge
|
||||
mobile/lib/**/*.drift.dart linguist-generated=true
|
||||
|
||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||
|
||||
|
|
|
|||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -39,6 +39,7 @@
|
|||
],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ analyzer:
|
|||
exclude:
|
||||
- openapi/**
|
||||
- lib/generated_plugin_registrant.dart
|
||||
- lib/**/*.g.dart
|
||||
- lib/**/*.drift.dart
|
||||
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
|
|
|||
24
mobile/build.yaml
Normal file
24
mobile/build.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
targets:
|
||||
$default:
|
||||
builders:
|
||||
#drift @DriftDatabase()
|
||||
drift_dev:
|
||||
# Disable default builder to use modular builder instead
|
||||
enabled: false
|
||||
drift_dev:analyzer:
|
||||
enabled: true
|
||||
options: &drift_options
|
||||
store_date_time_values_as_text: true
|
||||
named_parameters: true
|
||||
write_from_json_string_constructor: false
|
||||
data_class_to_companions: false
|
||||
# Required for make-migrations
|
||||
databases:
|
||||
main: lib/infrastructure/repositories/db.repository.dart
|
||||
generate_for: &drift_generate_for
|
||||
- lib/infrastructure/entities/*.dart
|
||||
- lib/infrastructure/repositories/db.repository.dart
|
||||
drift_dev:modular:
|
||||
enabled: true
|
||||
options: *drift_options
|
||||
generate_for: *drift_generate_for
|
||||
BIN
mobile/drift_schemas/main/drift_schema_v1.json
Normal file
BIN
mobile/drift_schemas/main/drift_schema_v1.json
Normal file
Binary file not shown.
|
|
@ -1,32 +1,4 @@
|
|||
import 'dart:ui';
|
||||
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber;
|
||||
|
||||
Color toColor({bool isDarkTheme = false}) => switch (this) {
|
||||
AvatarColor.primary =>
|
||||
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
|
||||
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
|
||||
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
|
||||
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
|
||||
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
|
||||
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
|
||||
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
|
||||
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
|
||||
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
|
||||
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
|
||||
};
|
||||
}
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
|
||||
// TODO: Rename to User once Isar is removed
|
||||
class UserDto {
|
||||
|
|
|
|||
105
mobile/lib/domain/models/user_metadata.model.dart
Normal file
105
mobile/lib/domain/models/user_metadata.model.dart
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import 'dart:ui';
|
||||
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary("primary"),
|
||||
pink("pink"),
|
||||
red("red"),
|
||||
yellow("yellow"),
|
||||
blue("blue"),
|
||||
green("green"),
|
||||
purple("purple"),
|
||||
orange("orange"),
|
||||
gray("gray"),
|
||||
amber("amber");
|
||||
|
||||
final String value;
|
||||
const AvatarColor(this.value);
|
||||
|
||||
Color toColor({bool isDarkTheme = false}) => switch (this) {
|
||||
AvatarColor.primary =>
|
||||
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
|
||||
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
|
||||
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
|
||||
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
|
||||
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
|
||||
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
|
||||
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
|
||||
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
|
||||
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
|
||||
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
|
||||
};
|
||||
}
|
||||
|
||||
class UserPreferences {
|
||||
final bool foldersEnabled;
|
||||
final bool memoriesEnabled;
|
||||
final bool peopleEnabled;
|
||||
final bool ratingsEnabled;
|
||||
final bool sharedLinksEnabled;
|
||||
final bool tagsEnabled;
|
||||
final AvatarColor userAvatarColor;
|
||||
final bool showSupportBadge;
|
||||
|
||||
const UserPreferences({
|
||||
this.foldersEnabled = false,
|
||||
this.memoriesEnabled = true,
|
||||
this.peopleEnabled = true,
|
||||
this.ratingsEnabled = false,
|
||||
this.sharedLinksEnabled = true,
|
||||
this.tagsEnabled = false,
|
||||
this.userAvatarColor = AvatarColor.primary,
|
||||
this.showSupportBadge = true,
|
||||
});
|
||||
|
||||
UserPreferences copyWith({
|
||||
bool? foldersEnabled,
|
||||
bool? memoriesEnabled,
|
||||
bool? peopleEnabled,
|
||||
bool? ratingsEnabled,
|
||||
bool? sharedLinksEnabled,
|
||||
bool? tagsEnabled,
|
||||
AvatarColor? userAvatarColor,
|
||||
bool? showSupportBadge,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
|
||||
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
|
||||
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
|
||||
ratingsEnabled: ratingsEnabled ?? this.ratingsEnabled,
|
||||
sharedLinksEnabled: sharedLinksEnabled ?? this.sharedLinksEnabled,
|
||||
tagsEnabled: tagsEnabled ?? this.tagsEnabled,
|
||||
userAvatarColor: userAvatarColor ?? this.userAvatarColor,
|
||||
showSupportBadge: showSupportBadge ?? this.showSupportBadge,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
final preferences = <String, Object?>{};
|
||||
preferences["folders-Enabled"] = foldersEnabled;
|
||||
preferences["memories-Enabled"] = memoriesEnabled;
|
||||
preferences["people-Enabled"] = peopleEnabled;
|
||||
preferences["ratings-Enabled"] = ratingsEnabled;
|
||||
preferences["sharedLinks-Enabled"] = sharedLinksEnabled;
|
||||
preferences["tags-Enabled"] = tagsEnabled;
|
||||
preferences["avatar-Color"] = userAvatarColor.value;
|
||||
preferences["purchase-ShowSupportBadge"] = showSupportBadge;
|
||||
return preferences;
|
||||
}
|
||||
|
||||
factory UserPreferences.fromMap(Map<String, Object?> map) {
|
||||
return UserPreferences(
|
||||
foldersEnabled: map["folders-Enabled"] as bool? ?? false,
|
||||
memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
|
||||
peopleEnabled: map["people-Enabled"] as bool? ?? true,
|
||||
ratingsEnabled: map["ratings-Enabled"] as bool? ?? false,
|
||||
sharedLinksEnabled: map["sharedLinks-Enabled"] as bool? ?? true,
|
||||
tagsEnabled: map["tags-Enabled"] as bool? ?? false,
|
||||
userAvatarColor: AvatarColor.values.firstWhere(
|
||||
(e) => e.value == map["avatar-Color"] as String?,
|
||||
orElse: () => AvatarColor.primary,
|
||||
),
|
||||
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
18
mobile/lib/infrastructure/entities/partner.entity.dart
Normal file
18
mobile/lib/infrastructure/entities/partner.entity.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class PartnerEntity extends Table with DriftDefaultsMixin {
|
||||
const PartnerEntity();
|
||||
|
||||
BlobColumn get sharedById =>
|
||||
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
BlobColumn get sharedWithId =>
|
||||
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {sharedById, sharedWithId};
|
||||
}
|
||||
BIN
mobile/lib/infrastructure/entities/partner.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/partner.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:drift/drift.dart' hide Index;
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
|
|
@ -71,3 +74,20 @@ class User {
|
|||
quotaSizeInBytes: quotaSizeInBytes,
|
||||
);
|
||||
}
|
||||
|
||||
class UserEntity extends Table with DriftDefaultsMixin {
|
||||
const UserEntity();
|
||||
|
||||
BlobColumn get id => blob()();
|
||||
TextColumn get name => text()();
|
||||
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
|
||||
TextColumn get email => text()();
|
||||
TextColumn get profileImagePath => text().nullable()();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
// Quota
|
||||
IntColumn get quotaSizeInBytes => integer().nullable()();
|
||||
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
|
|
|||
BIN
mobile/lib/infrastructure/entities/user.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/user.entity.drift.dart
generated
Normal file
Binary file not shown.
21
mobile/lib/infrastructure/entities/user_metadata.entity.dart
Normal file
21
mobile/lib/infrastructure/entities/user_metadata.entity.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class UserMetadataEntity extends Table with DriftDefaultsMixin {
|
||||
const UserMetadataEntity();
|
||||
|
||||
BlobColumn get userId =>
|
||||
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get preferences => text().map(userPreferenceConverter)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {userId};
|
||||
}
|
||||
|
||||
final JsonTypeConverter2<UserPreferences, String, Object?>
|
||||
userPreferenceConverter = TypeConverter.json2(
|
||||
fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
|
||||
toJson: (pref) => pref.toMap(),
|
||||
);
|
||||
BIN
mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -1,8 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import 'db.repository.drift.dart';
|
||||
|
||||
// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone
|
||||
// ref: isar/isar_common.dart
|
||||
const Symbol _kzoneTxn = #zoneTxn;
|
||||
|
|
@ -17,3 +24,35 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||
Future<T> transaction<T>(Future<T> Function() callback) =>
|
||||
Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback();
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity])
|
||||
class Drift extends $Drift implements IDatabaseRepository {
|
||||
Drift([QueryExecutor? executor])
|
||||
: super(
|
||||
executor ??
|
||||
driftDatabase(
|
||||
name: 'immich',
|
||||
native: const DriftNativeOptions(shareAcrossIsolates: true),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA journal_mode = WAL');
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class DriftDatabaseRepository implements IDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftDatabaseRepository(this._db);
|
||||
|
||||
@override
|
||||
Future<T> transaction<T>(Future<T> Function() callback) =>
|
||||
_db.transaction(callback);
|
||||
}
|
||||
|
|
|
|||
BIN
mobile/lib/infrastructure/repositories/db.repository.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/repositories/db.repository.drift.dart
generated
Normal file
Binary file not shown.
9
mobile/lib/infrastructure/utils/drift_default.mixin.dart
Normal file
9
mobile/lib/infrastructure/utils/drift_default.mixin.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
mixin DriftDefaultsMixin on Table {
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
// TODO: Move to repository once all classes are refactored
|
||||
abstract final class ExifDtoConverter {
|
||||
static ExifInfo fromDto(ExifResponseDto dto) {
|
||||
return ExifInfo(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
// TODO: Move to repository once all classes are refactored
|
||||
abstract final class UserConverter {
|
||||
/// Base user dto used where the complete user object is not required
|
||||
static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
|
|
|
|||
|
|
@ -14,3 +14,6 @@ create_splash:
|
|||
|
||||
build_release_android:
|
||||
flutter build appbundle
|
||||
|
||||
migrations:
|
||||
dart run drift_dev make-migrations
|
||||
|
|
|
|||
|
|
@ -206,6 +206,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -382,6 +390,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.26.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.26.0"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1288,6 +1320,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1549,6 +1589,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.5"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.31"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.41.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ dependencies:
|
|||
isar_flutter_libs: # contains Isar Core
|
||||
version: *isar_version
|
||||
hosted: https://pub.isar-community.dev/
|
||||
# DB
|
||||
drift: ^2.23.1
|
||||
drift_flutter: ^0.2.4
|
||||
|
||||
dependency_overrides:
|
||||
analyzer: ^6.0.0
|
||||
|
|
@ -99,6 +102,8 @@ dev_dependencies:
|
|||
immich_mobile_immich_lint:
|
||||
path: './immich_lint'
|
||||
fake_async: ^1.3.1
|
||||
# Drift generator
|
||||
drift_dev: ^2.23.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
|
|||
1
mobile/test/fixtures/user.stub.dart
vendored
1
mobile/test/fixtures/user.stub.dart
vendored
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
|
||||
abstract final class UserStub {
|
||||
const UserStub._();
|
||||
|
|
|
|||
Loading…
Reference in a new issue