mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 09:59:00 -07:00
refactor(mobile): add Isar DB & Store class (#1574)
* refactor(mobile): add Isar DB & Store class new Store: globally accessible key-value store like Hive (but based on Isar) replace first few places of Hive usage with the new Store * reduce max. DB size to prevent errors on older iOS devices --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
adb265794c
commit
911c35a7f1
@ -1,7 +1,9 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:immich_mobile/main.dart' as app;
|
||||
@ -34,8 +36,12 @@ class ImmichTestHelper {
|
||||
// Clear all data from Hive
|
||||
await Hive.deleteFromDisk();
|
||||
await app.openBoxes();
|
||||
// Clear all data from Isar (reuse existing instance if available)
|
||||
final db = Isar.getInstance() ?? await app.loadDb();
|
||||
await Store.clear();
|
||||
await db.writeTxn(() => db.clear());
|
||||
// Load main Widget
|
||||
await tester.pumpWidget(app.getMainWidget());
|
||||
await tester.pumpWidget(app.getMainWidget(db));
|
||||
// Post run tasks
|
||||
await tester.pumpAndSettle();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
@ -17,8 +17,10 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
@ -26,11 +28,16 @@ import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'constants/hive_box.dart';
|
||||
|
||||
void main() async {
|
||||
await initApp();
|
||||
runApp(getMainWidget());
|
||||
final db = await loadDb();
|
||||
await migrateHiveToStoreIfNecessary();
|
||||
runApp(getMainWidget(db));
|
||||
}
|
||||
|
||||
Future<void> openBoxes() async {
|
||||
@ -70,13 +77,27 @@ Future<void> initApp() async {
|
||||
ImmichLogger().init();
|
||||
}
|
||||
|
||||
Widget getMainWidget() {
|
||||
Future<Isar> loadDb() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
Isar db = await Isar.open(
|
||||
[StoreValueSchema],
|
||||
directory: dir.path,
|
||||
maxSizeMiB: 256,
|
||||
);
|
||||
Store.init(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
Widget getMainWidget(Isar db) {
|
||||
return EasyLocalization(
|
||||
supportedLocales: locales,
|
||||
path: translationsPath,
|
||||
useFallbackTranslations: true,
|
||||
fallbackLocale: locales.first,
|
||||
child: const ProviderScope(child: ImmichApp()),
|
||||
child: ProviderScope(
|
||||
overrides: [dbProvider.overrideWithValue(db)],
|
||||
child: const ImmichApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||
@ -94,7 +95,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
await Future.wait([
|
||||
_apiService.authenticationApi.logout(),
|
||||
Hive.box(userInfoBox).delete(accessTokenKey),
|
||||
Hive.box(userInfoBox).delete(assetEtagKey),
|
||||
Store.delete(StoreKey.assetETag),
|
||||
Store.delete(StoreKey.userRemoteId),
|
||||
_assetCacheService.invalidate(),
|
||||
_albumCacheService.invalidate(),
|
||||
_sharedAlbumCacheService.invalidate(),
|
||||
@ -153,7 +155,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
|
||||
userInfoHiveBox.put(accessTokenKey, accessToken);
|
||||
userInfoHiveBox.put(userIdKey, userResponseDto.id);
|
||||
Store.put(StoreKey.userRemoteId, userResponseDto.id);
|
||||
|
||||
state = state.copyWith(
|
||||
isAuthenticated: true,
|
||||
|
96
mobile/lib/shared/models/store.dart
Normal file
96
mobile/lib/shared/models/store.dart
Normal file
@ -0,0 +1,96 @@
|
||||
import 'package:isar/isar.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
part 'store.g.dart';
|
||||
|
||||
/// Key-value store for individual items enumerated in StoreKey.
|
||||
/// Supports String, int and JSON-serializable Objects
|
||||
/// Can be used concurrently from multiple isolates
|
||||
class Store {
|
||||
static late final Isar _db;
|
||||
static final List<dynamic> _cache = List.filled(StoreKey.values.length, null);
|
||||
|
||||
/// Initializes the store (call exactly once per app start)
|
||||
static void init(Isar db) {
|
||||
_db = db;
|
||||
_populateCache();
|
||||
_db.storeValues.where().build().watch().listen(_onChangeListener);
|
||||
}
|
||||
|
||||
/// clears all values from this store (cache and DB), only for testing!
|
||||
static Future<void> clear() {
|
||||
_cache.fillRange(0, _cache.length, null);
|
||||
return _db.writeTxn(() => _db.storeValues.clear());
|
||||
}
|
||||
|
||||
/// Returns the stored value for the given key, or the default value if null
|
||||
static T? get<T>(StoreKey key, [T? defaultValue]) =>
|
||||
_cache[key._id] ?? defaultValue;
|
||||
|
||||
/// Stores the value synchronously in the cache and asynchronously in the DB
|
||||
static Future<void> put<T>(StoreKey key, T value) {
|
||||
_cache[key._id] = value;
|
||||
return _db.writeTxn(() => _db.storeValues.put(StoreValue._of(value, key)));
|
||||
}
|
||||
|
||||
/// Removes the value synchronously from the cache and asynchronously from the DB
|
||||
static Future<void> delete(StoreKey key) {
|
||||
_cache[key._id] = null;
|
||||
return _db.writeTxn(() => _db.storeValues.delete(key._id));
|
||||
}
|
||||
|
||||
/// Fills the cache with the values from the DB
|
||||
static _populateCache() {
|
||||
for (StoreKey key in StoreKey.values) {
|
||||
final StoreValue? value = _db.storeValues.getSync(key._id);
|
||||
if (value != null) {
|
||||
_cache[key._id] = value._extract(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// updates the state if a value is updated in any isolate
|
||||
static void _onChangeListener(List<StoreValue>? data) {
|
||||
if (data != null) {
|
||||
for (StoreValue value in data) {
|
||||
_cache[value.id] = value._extract(StoreKey.values[value.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal class for `Store`, do not use elsewhere.
|
||||
@Collection(inheritance: false)
|
||||
class StoreValue {
|
||||
StoreValue(this.id, {this.intValue, this.strValue});
|
||||
Id id;
|
||||
int? intValue;
|
||||
String? strValue;
|
||||
|
||||
T? _extract<T>(StoreKey key) => key._isInt
|
||||
? intValue
|
||||
: (key._fromJson != null
|
||||
? key._fromJson!(json.decode(strValue!))
|
||||
: strValue);
|
||||
static StoreValue _of(dynamic value, StoreKey key) => StoreValue(
|
||||
key._id,
|
||||
intValue: key._isInt ? value : null,
|
||||
strValue: key._isInt
|
||||
? null
|
||||
: (key._fromJson == null ? value : json.encode(value.toJson())),
|
||||
);
|
||||
}
|
||||
|
||||
/// Key for each possible value in the `Store`.
|
||||
/// Defines the data type (int, String, JSON) for each value
|
||||
enum StoreKey {
|
||||
userRemoteId(0),
|
||||
assetETag(1),
|
||||
;
|
||||
|
||||
// ignore: unused_element
|
||||
const StoreKey(this._id, [this._isInt = false, this._fromJson]);
|
||||
final int _id;
|
||||
final bool _isInt;
|
||||
final Function(dynamic)? _fromJson;
|
||||
}
|
574
mobile/lib/shared/models/store.g.dart
Normal file
574
mobile/lib/shared/models/store.g.dart
Normal file
@ -0,0 +1,574 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'store.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
|
||||
|
||||
extension GetStoreValueCollection on Isar {
|
||||
IsarCollection<StoreValue> get storeValues => this.collection();
|
||||
}
|
||||
|
||||
const StoreValueSchema = CollectionSchema(
|
||||
name: r'StoreValue',
|
||||
id: 902899285492123510,
|
||||
properties: {
|
||||
r'intValue': PropertySchema(
|
||||
id: 0,
|
||||
name: r'intValue',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'strValue': PropertySchema(
|
||||
id: 1,
|
||||
name: r'strValue',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _storeValueEstimateSize,
|
||||
serialize: _storeValueSerialize,
|
||||
deserialize: _storeValueDeserialize,
|
||||
deserializeProp: _storeValueDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _storeValueGetId,
|
||||
getLinks: _storeValueGetLinks,
|
||||
attach: _storeValueAttach,
|
||||
version: '3.0.5',
|
||||
);
|
||||
|
||||
int _storeValueEstimateSize(
|
||||
StoreValue object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
{
|
||||
final value = object.strValue;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _storeValueSerialize(
|
||||
StoreValue object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeLong(offsets[0], object.intValue);
|
||||
writer.writeString(offsets[1], object.strValue);
|
||||
}
|
||||
|
||||
StoreValue _storeValueDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = StoreValue(
|
||||
id,
|
||||
intValue: reader.readLongOrNull(offsets[0]),
|
||||
strValue: reader.readStringOrNull(offsets[1]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _storeValueDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readLongOrNull(offset)) as P;
|
||||
case 1:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _storeValueGetId(StoreValue object) {
|
||||
return object.id;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _storeValueGetLinks(StoreValue object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _storeValueAttach(IsarCollection<dynamic> col, Id id, StoreValue object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension StoreValueQueryWhereSort
|
||||
on QueryBuilder<StoreValue, StoreValue, QWhere> {
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreValueQueryWhere
|
||||
on QueryBuilder<StoreValue, StoreValue, QWhereClause> {
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterWhereClause> idEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: id,
|
||||
upper: id,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterWhereClause> idNotEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterWhereClause> idGreaterThan(Id id,
|
||||
{bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterWhereClause> idLessThan(Id id,
|
||||
{bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterWhereClause> idBetween(
|
||||
Id lowerId,
|
||||
Id upperId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerId,
|
||||
includeLower: includeLower,
|
||||
upper: upperId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreValueQueryFilter
|
||||
on QueryBuilder<StoreValue, StoreValue, QFilterCondition> {
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> idEqualTo(
|
||||
Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> idGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> idLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> idBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> intValueIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'intValue',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
intValueIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'intValue',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> intValueEqualTo(
|
||||
int? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'intValue',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
intValueGreaterThan(
|
||||
int? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'intValue',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> intValueLessThan(
|
||||
int? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'intValue',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> intValueBetween(
|
||||
int? lower,
|
||||
int? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'intValue',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'strValue',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
strValueIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'strValue',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'strValue',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
strValueGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'strValue',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'strValue',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'strValue',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
strValueStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'strValue',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'strValue',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueContains(
|
||||
String value,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'strValue',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition> strValueMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'strValue',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
strValueIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'strValue',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterFilterCondition>
|
||||
strValueIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'strValue',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreValueQueryObject
|
||||
on QueryBuilder<StoreValue, StoreValue, QFilterCondition> {}
|
||||
|
||||
extension StoreValueQueryLinks
|
||||
on QueryBuilder<StoreValue, StoreValue, QFilterCondition> {}
|
||||
|
||||
extension StoreValueQuerySortBy
|
||||
on QueryBuilder<StoreValue, StoreValue, QSortBy> {
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> sortByIntValue() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'intValue', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> sortByIntValueDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'intValue', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> sortByStrValue() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'strValue', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> sortByStrValueDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'strValue', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreValueQuerySortThenBy
|
||||
on QueryBuilder<StoreValue, StoreValue, QSortThenBy> {
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> thenByIntValue() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'intValue', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> thenByIntValueDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'intValue', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> thenByStrValue() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'strValue', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QAfterSortBy> thenByStrValueDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'strValue', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreValueQueryWhereDistinct
|
||||
on QueryBuilder<StoreValue, StoreValue, QDistinct> {
|
||||
QueryBuilder<StoreValue, StoreValue, QDistinct> distinctByIntValue() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'intValue');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, StoreValue, QDistinct> distinctByStrValue(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'strValue', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreValueQueryProperty
|
||||
on QueryBuilder<StoreValue, StoreValue, QQueryProperty> {
|
||||
QueryBuilder<StoreValue, int, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, int?, QQueryOperations> intValueProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'intValue');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<StoreValue, String?, QQueryOperations> strValueProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'strValue');
|
||||
});
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
@ -106,7 +107,6 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
_getAllAssetInProgress = true;
|
||||
bool isCacheValid = await _assetCacheService.isValid();
|
||||
stopwatch.start();
|
||||
final Box box = Hive.box(userInfoBox);
|
||||
if (isCacheValid && state.allAssets.isEmpty) {
|
||||
final List<Asset>? cachedData = await _assetCacheService.get();
|
||||
if (cachedData == null) {
|
||||
@ -122,7 +122,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
}
|
||||
final localTask = _assetService.getLocalAssets(urgent: !isCacheValid);
|
||||
final remoteTask = _assetService.getRemoteAssets(
|
||||
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
||||
etag: isCacheValid ? Store.get(StoreKey.assetETag) : null,
|
||||
);
|
||||
|
||||
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
||||
@ -151,7 +151,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
|
||||
log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
|
||||
box.put(assetEtagKey, remoteResult.second);
|
||||
Store.put(StoreKey.assetETag, remoteResult.second);
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
}
|
||||
@ -279,8 +279,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
|
||||
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
||||
if (index > 0) {
|
||||
state.allAssets.removeAt(index);
|
||||
state.allAssets.insert(index, Asset.remote(newAsset));
|
||||
state.allAssets[index] = newAsset;
|
||||
_updateAssetsState(state.allAssets);
|
||||
}
|
||||
|
||||
|
5
mobile/lib/shared/providers/db.provider.dart
Normal file
5
mobile/lib/shared/providers/db.provider.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
// overwritten in main.dart due to async loading
|
||||
final dbProvider = Provider<Isar>((_) => throw UnimplementedError());
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/backup/background_service/background.servi
|
||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/openapi_extensions.dart';
|
||||
@ -37,7 +38,7 @@ class AssetService {
|
||||
final Pair<List<AssetResponseDto>, String?>? remote =
|
||||
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
|
||||
if (remote == null) {
|
||||
return const Pair(null, null);
|
||||
return Pair(null, etag);
|
||||
}
|
||||
return Pair(
|
||||
remote.first.map(Asset.remote).toList(growable: false),
|
||||
@ -45,7 +46,7 @@ class AssetService {
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log.severe('Error while getting remote assets', e, stack);
|
||||
return const Pair(null, null);
|
||||
return Pair(null, etag);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +63,7 @@ class AssetService {
|
||||
}
|
||||
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||
final String userId = Hive.box(userInfoBox).get(userIdKey);
|
||||
final String userId = Store.get(StoreKey.userRemoteId);
|
||||
if (backupAlbumInfo != null) {
|
||||
return (await _backupService
|
||||
.buildUploadCandidates(backupAlbumInfo.deepCopy()))
|
||||
@ -105,12 +106,16 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<AssetResponseDto?> updateAsset(Asset asset, UpdateAssetDto updateAssetDto) async {
|
||||
return await _apiService.assetApi.updateAsset(asset.id, updateAssetDto);
|
||||
Future<Asset?> updateAsset(
|
||||
Asset asset,
|
||||
UpdateAssetDto updateAssetDto,
|
||||
) async {
|
||||
final dto =
|
||||
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
||||
return dto == null ? null : Asset.remote(dto);
|
||||
}
|
||||
|
||||
Future<AssetResponseDto?> changeFavoriteStatus(Asset asset, bool isFavorite) {
|
||||
Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
|
||||
return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
|
||||
}
|
||||
|
||||
}
|
||||
|
24
mobile/lib/utils/migration.dart
Normal file
24
mobile/lib/utils/migration.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
|
||||
Future<void> migrateHiveToStoreIfNecessary() async {
|
||||
try {
|
||||
if (await Hive.boxExists(userInfoBox)) {
|
||||
final Box box = await Hive.openBox(userInfoBox);
|
||||
await _migrateSingleKey(box, userIdKey, StoreKey.userRemoteId);
|
||||
await _migrateSingleKey(box, assetEtagKey, StoreKey.assetETag);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error while migrating userInfoBox $e");
|
||||
}
|
||||
}
|
||||
|
||||
_migrateSingleKey(Box box, String hiveKey, StoreKey key) async {
|
||||
final String? value = box.get(hiveKey);
|
||||
if (value != null) {
|
||||
await Store.put(key, value);
|
||||
await box.delete(hiveKey);
|
||||
}
|
||||
}
|
@ -239,6 +239,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
dartx:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dartx
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
easy_image_viewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -547,6 +554,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
isar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: isar
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
isar_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: isar_flutter_libs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
isar_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: isar_generator
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1063,6 +1091,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.12"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: time
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1301,6 +1336,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
xxh3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xxh3
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -3,6 +3,7 @@ description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.45.0+68
|
||||
isar_version: &isar_version 3.0.5
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
@ -41,6 +42,8 @@ dependencies:
|
||||
http_parser: ^4.0.1
|
||||
flutter_web_auth: ^0.5.0
|
||||
easy_image_viewer: ^1.2.0
|
||||
isar: *isar_version
|
||||
isar_flutter_libs: *isar_version # contains Isar Core
|
||||
|
||||
openapi:
|
||||
path: openapi
|
||||
@ -58,6 +61,7 @@ dev_dependencies:
|
||||
auto_route_generator: ^5.0.2
|
||||
flutter_launcher_icons: "^0.9.2"
|
||||
flutter_native_splash: ^2.2.16
|
||||
isar_generator: *isar_version
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user