diff --git a/mobile-v2/build.yaml b/mobile-v2/build.yaml index 33ed84b736..04eca9fab7 100644 --- a/mobile-v2/build.yaml +++ b/mobile-v2/build.yaml @@ -15,6 +15,7 @@ targets: skip_verification_code: true generate_for: &drift_generate_for - lib/domain/entities/*.dart + - lib/domain/entities/views/*.dart - lib/domain/repositories/database.repository.dart drift_dev:modular: enabled: true diff --git a/mobile-v2/ios/Runner/AppDelegate.swift b/mobile-v2/ios/Runner/AppDelegate.swift index 70693e4a8c..b636303481 100644 --- a/mobile-v2/ios/Runner/AppDelegate.swift +++ b/mobile-v2/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/mobile-v2/lib/domain/entities/asset.entity.dart b/mobile-v2/lib/domain/entities/asset.entity.dart index be4d45a530..564f458b6e 100644 --- a/mobile-v2/lib/domain/entities/asset.entity.dart +++ b/mobile-v2/lib/domain/entities/asset.entity.dart @@ -1,8 +1,13 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; +@TableIndex(name: 'asset_localid', columns: {#localId}) +@TableIndex(name: 'asset_remoteid', columns: {#remoteId}) class Asset extends Table { const Asset(); + + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); TextColumn get checksum => text().unique()(); IntColumn get height => integer().nullable()(); @@ -12,4 +17,11 @@ class Asset extends Table { DateTimeColumn get modifiedTime => dateTime().withDefault(currentDateAndTime)(); IntColumn get duration => integer().withDefault(const Constant(0))(); + + // Local only + TextColumn get localId => text().nullable()(); + + // Remote only + TextColumn get remoteId => text().nullable()(); + TextColumn get livePhotoVideoId => text().nullable()(); } diff --git a/mobile-v2/lib/domain/entities/local_asset.entity.dart b/mobile-v2/lib/domain/entities/local_asset.entity.dart deleted file mode 100644 index 71e7dd702f..0000000000 --- a/mobile-v2/lib/domain/entities/local_asset.entity.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/entities/asset.entity.dart'; - -class LocalAsset extends Asset { - const LocalAsset(); - - TextColumn get localId => text()(); - - @override - Set get primaryKey => {localId}; -} diff --git a/mobile-v2/lib/domain/entities/remote_asset.entity.dart b/mobile-v2/lib/domain/entities/remote_asset.entity.dart deleted file mode 100644 index 37171fdd30..0000000000 --- a/mobile-v2/lib/domain/entities/remote_asset.entity.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/entities/asset.entity.dart'; - -class RemoteAsset extends Asset { - const RemoteAsset(); - - TextColumn get remoteId => text()(); - TextColumn get livePhotoVideoId => text().nullable()(); - - @override - Set get primaryKey => {remoteId}; -} diff --git a/mobile-v2/lib/domain/interfaces/asset.interface.dart b/mobile-v2/lib/domain/interfaces/asset.interface.dart new file mode 100644 index 0000000000..5e040bd0ef --- /dev/null +++ b/mobile-v2/lib/domain/interfaces/asset.interface.dart @@ -0,0 +1,16 @@ +import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/domain/models/render_list.model.dart'; + +abstract class IAssetRepository { + /// Batch insert asset + Future addAll(Iterable assets); + + /// Removes all assets + Future clearAll(); + + /// Fetch assets from the [offset] with the [limit] + Future> fetchAssets({int? offset, int? limit}); + + /// Streams assets as groups grouped by the group type passed + Stream getRenderList(); +} diff --git a/mobile-v2/lib/domain/interfaces/remote_asset.interface.dart b/mobile-v2/lib/domain/interfaces/remote_asset.interface.dart deleted file mode 100644 index b21ae549d1..0000000000 --- a/mobile-v2/lib/domain/interfaces/remote_asset.interface.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart'; - -abstract class IRemoteAssetRepository { - /// Batch insert asset - Future addAll(Iterable assets); -} diff --git a/mobile-v2/lib/domain/models/asset.model.dart b/mobile-v2/lib/domain/models/asset.model.dart new file mode 100644 index 0000000000..66f4c3a6e9 --- /dev/null +++ b/mobile-v2/lib/domain/models/asset.model.dart @@ -0,0 +1,150 @@ +import 'package:immich_mobile/utils/extensions/string.extension.dart'; +import 'package:openapi/api.dart'; + +enum AssetType { + // do not change this order! + other, + image, + video, + audio, +} + +class Asset { + final int id; + final String name; + final String checksum; + final int? height; + final int? width; + final AssetType type; + final DateTime createdTime; + final DateTime modifiedTime; + final int duration; + + // local only + final String? localId; + + // remote only + final String? remoteId; + final String? livePhotoVideoId; + + bool get isRemote => remoteId != null; + bool get isLocal => localId != null; + bool get isMerged => isRemote && isLocal; + + const Asset({ + required this.id, + required this.name, + required this.checksum, + this.height, + this.width, + required this.type, + required this.createdTime, + required this.modifiedTime, + required this.duration, + this.localId, + this.remoteId, + this.livePhotoVideoId, + }); + + factory Asset.remote(AssetResponseDto dto) => Asset( + id: 0, // assign a temporary auto gen ID + remoteId: dto.id, + createdTime: dto.fileCreatedAt, + duration: dto.duration.tryParseInt() ?? 0, + height: dto.exifInfo?.exifImageHeight?.toInt(), + width: dto.exifInfo?.exifImageWidth?.toInt(), + checksum: dto.checksum, + name: dto.originalFileName, + livePhotoVideoId: dto.livePhotoVideoId, + modifiedTime: dto.fileModifiedAt, + type: _toAssetType(dto.type), + ); + + Asset copyWith({ + int? id, + String? name, + String? checksum, + int? height, + int? width, + AssetType? type, + DateTime? createdTime, + DateTime? modifiedTime, + int? duration, + String? localId, + String? remoteId, + String? livePhotoVideoId, + }) { + return Asset( + id: id ?? this.id, + name: name ?? this.name, + checksum: checksum ?? this.checksum, + height: height ?? this.height, + width: width ?? this.width, + type: type ?? this.type, + createdTime: createdTime ?? this.createdTime, + modifiedTime: modifiedTime ?? this.modifiedTime, + duration: duration ?? this.duration, + localId: localId ?? this.localId, + remoteId: remoteId ?? this.remoteId, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + ); + } + + @override + String toString() => """ +{ + "id": "$id", + "remoteId": "${remoteId ?? "-"}", + "localId": "${localId ?? "-"}", + "name": "$name", + "checksum": "$checksum", + "height": ${height ?? "-"}, + "width": ${width ?? "-"}, + "type": "$type", + "createdTime": "$createdTime", + "modifiedTime": "$modifiedTime", + "duration": "$duration", + "livePhotoVideoId": "${livePhotoVideoId ?? "-"}", +}"""; + + @override + bool operator ==(covariant Asset other) { + if (identical(this, other)) return true; + + return other.id == id && + other.name == name && + other.checksum == checksum && + other.height == height && + other.width == width && + other.type == type && + other.createdTime == createdTime && + other.modifiedTime == modifiedTime && + other.duration == duration && + other.localId == localId && + other.remoteId == remoteId && + other.livePhotoVideoId == livePhotoVideoId; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + checksum.hashCode ^ + height.hashCode ^ + width.hashCode ^ + type.hashCode ^ + createdTime.hashCode ^ + modifiedTime.hashCode ^ + duration.hashCode ^ + localId.hashCode ^ + remoteId.hashCode ^ + livePhotoVideoId.hashCode; + } +} + +AssetType _toAssetType(AssetTypeEnum type) => switch (type) { + AssetTypeEnum.AUDIO => AssetType.audio, + AssetTypeEnum.IMAGE => AssetType.image, + AssetTypeEnum.VIDEO => AssetType.video, + _ => AssetType.other, + }; diff --git a/mobile-v2/lib/domain/models/asset/asset.model.dart b/mobile-v2/lib/domain/models/asset/asset.model.dart deleted file mode 100644 index 9d9b25b557..0000000000 --- a/mobile-v2/lib/domain/models/asset/asset.model.dart +++ /dev/null @@ -1,90 +0,0 @@ -enum AssetType { - // do not change this order! - other, - image, - video, - audio, -} - -class Asset { - final String name; - final String checksum; - final int? height; - final int? width; - final AssetType type; - final DateTime createdTime; - final DateTime modifiedTime; - final int duration; - - const Asset({ - required this.name, - required this.checksum, - this.height, - this.width, - required this.type, - required this.createdTime, - required this.modifiedTime, - required this.duration, - }); - - Asset copyWith({ - String? name, - String? checksum, - int? height, - int? width, - AssetType? type, - DateTime? createdTime, - DateTime? modifiedTime, - int? duration, - }) { - return Asset( - name: name ?? this.name, - checksum: checksum ?? this.checksum, - height: height ?? this.height, - width: width ?? this.width, - type: type ?? this.type, - createdTime: createdTime ?? this.createdTime, - modifiedTime: modifiedTime ?? this.modifiedTime, - duration: duration ?? this.duration, - ); - } - - @override - String toString() => """ -{ - "name": "$name", - "checksum": "$checksum", - "height": ${height ?? "-"}, - "width": ${width ?? "-"}, - "type": "$type", - "createdTime": "$createdTime", - "modifiedTime": "$modifiedTime", - "duration": "$duration", -}"""; - - @override - bool operator ==(covariant Asset other) { - if (identical(this, other)) return true; - - return other.name == name && - other.checksum == checksum && - other.height == height && - other.width == width && - other.type == type && - other.createdTime == createdTime && - other.modifiedTime == modifiedTime && - other.duration == duration; - } - - @override - int get hashCode { - return name.hashCode ^ - checksum.hashCode ^ - height.hashCode ^ - width.hashCode ^ - type.hashCode ^ - createdTime.hashCode ^ - modifiedTime.hashCode ^ - duration.hashCode; - } -} diff --git a/mobile-v2/lib/domain/models/asset/local_asset.model.dart b/mobile-v2/lib/domain/models/asset/local_asset.model.dart deleted file mode 100644 index d78074606f..0000000000 --- a/mobile-v2/lib/domain/models/asset/local_asset.model.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/models/asset/asset.model.dart'; - -@immutable -class LocalAsset extends Asset { - final String localId; - - const LocalAsset({ - required this.localId, - required super.name, - required super.checksum, - required super.height, - required super.width, - required super.type, - required super.createdTime, - required super.modifiedTime, - required super.duration, - }); - - @override - String toString() => """ -{ - "localId": "$localId", - "name": "$name", - "checksum": "$checksum", - "height": ${height ?? "-"}, - "width": ${width ?? "-"}, - "type": "$type", - "createdTime": "$createdTime", - "modifiedTime": "$modifiedTime", - "duration": "$duration", -}"""; - - @override - bool operator ==(covariant LocalAsset other) { - if (identical(this, other)) return true; - - return super == (other) && other.localId == localId; - } - - @override - int get hashCode => super.hashCode ^ localId.hashCode; - - @override - LocalAsset copyWith({ - String? localId, - String? name, - String? checksum, - int? height, - int? width, - AssetType? type, - DateTime? createdTime, - DateTime? modifiedTime, - int? duration, - }) { - return LocalAsset( - localId: localId ?? this.localId, - name: name ?? this.name, - checksum: checksum ?? this.checksum, - height: height ?? this.height, - width: width ?? this.width, - type: type ?? this.type, - createdTime: createdTime ?? this.createdTime, - modifiedTime: modifiedTime ?? this.modifiedTime, - duration: duration ?? this.duration, - ); - } -} diff --git a/mobile-v2/lib/domain/models/asset/remote_asset.model.dart b/mobile-v2/lib/domain/models/asset/remote_asset.model.dart deleted file mode 100644 index cd88c1adff..0000000000 --- a/mobile-v2/lib/domain/models/asset/remote_asset.model.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/models/asset/asset.model.dart'; -import 'package:immich_mobile/utils/extensions/string.extension.dart'; -import 'package:openapi/api.dart'; - -@immutable -class RemoteAsset extends Asset { - final String remoteId; - final String? livePhotoVideoId; - - const RemoteAsset({ - required this.remoteId, - required super.name, - required super.checksum, - required super.height, - required super.width, - required super.type, - required super.createdTime, - required super.modifiedTime, - required super.duration, - this.livePhotoVideoId, - }); - - @override - String toString() => """ -{ - "remoteId": "$remoteId", - "name": "$name", - "checksum": "$checksum", - "height": ${height ?? "-"}, - "width": ${width ?? "-"}, - "type": "$type", - "createdTime": "$createdTime", - "modifiedTime": "$modifiedTime", - "duration": "$duration", - "livePhotoVideoId": "${livePhotoVideoId ?? "-"}", -}"""; - - @override - bool operator ==(covariant RemoteAsset other) { - if (identical(this, other)) return true; - - return super == (other) && other.remoteId == remoteId; - } - - @override - int get hashCode => super.hashCode ^ remoteId.hashCode; - - @override - RemoteAsset copyWith({ - String? remoteId, - String? name, - String? checksum, - int? height, - int? width, - AssetType? type, - DateTime? createdTime, - DateTime? modifiedTime, - int? duration, - String? livePhotoVideoId, - }) { - return RemoteAsset( - remoteId: remoteId ?? this.remoteId, - name: name ?? this.name, - checksum: checksum ?? this.checksum, - height: height ?? this.height, - width: width ?? this.width, - type: type ?? this.type, - createdTime: createdTime ?? this.createdTime, - modifiedTime: modifiedTime ?? this.modifiedTime, - duration: duration ?? this.duration, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - ); - } - - factory RemoteAsset.fromDto(AssetResponseDto dto) => RemoteAsset( - remoteId: dto.id, - createdTime: dto.fileCreatedAt, - duration: dto.duration.tryParseInt() ?? 0, - height: dto.exifInfo?.exifImageHeight?.toInt(), - width: dto.exifInfo?.exifImageWidth?.toInt(), - checksum: dto.checksum, - name: dto.originalFileName, - livePhotoVideoId: dto.livePhotoVideoId, - modifiedTime: dto.fileModifiedAt, - type: _toAssetType(dto.type), - ); -} - -AssetType _toAssetType(AssetTypeEnum type) => switch (type) { - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - _ => AssetType.other, - }; diff --git a/mobile-v2/lib/domain/models/render_list.model.dart b/mobile-v2/lib/domain/models/render_list.model.dart new file mode 100644 index 0000000000..fa6f1bd937 --- /dev/null +++ b/mobile-v2/lib/domain/models/render_list.model.dart @@ -0,0 +1,64 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/domain/models/render_list_element.model.dart'; +import 'package:immich_mobile/service_locator.dart'; + +class RenderList { + final List elements; + final int totalCount; + + /// global offset of assets in [_buf] + int _bufOffset = 0; + + /// reference to batch of assets loaded from DB with offset [_bufOffset] + List _buf = []; + + RenderList({required this.elements, required this.totalCount}); + + /// Loads the requested assets from the database to an internal buffer if not cached + /// and returns a slice of that buffer + Future> loadAssets(int offset, int count) async { + assert(offset >= 0); + assert(count > 0); + assert(offset + count <= totalCount); + + // general case: we have the query to load assets via offset from the DB on demand + if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) { + // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf` + // thus, fill the buffer with a new batch of assets that at least contains the requested + // assets and some more + + final bool forward = _bufOffset < offset; + // if the requested offset is greater than the cached offset, the user scrolls forward "down" + const batchSize = 256; + const oppositeSize = 64; + + // make sure to load a meaningful amount of data (and not only the requested slice) + // otherwise, each call to [loadAssets] would result in DB call trashing performance + // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests + final len = math.max(batchSize, count + oppositeSize); + // when scrolling forward, start shortly before the requested offset... + // when scrolling backward, end shortly after the requested offset... + // ... to guard against the user scrolling in the other direction + // a tiny bit resulting in a another required load from the DB + final start = math.max( + 0, + forward + ? offset - oppositeSize + : (len > batchSize ? offset : offset + count - len), + ); + // load the calculated batch (start:start+len) from the DB and put it into the buffer + _buf = + await di().fetchAssets(offset: start, limit: len); + _bufOffset = start; + + assert(_bufOffset <= offset); + assert(_bufOffset + _buf.length >= offset + count); + } + // return the requested slice from the buffer (we made sure before that the assets are loaded!) + return _buf.slice(offset - _bufOffset, offset - _bufOffset + count); + } +} diff --git a/mobile-v2/lib/domain/models/render_list_element.model.dart b/mobile-v2/lib/domain/models/render_list_element.model.dart new file mode 100644 index 0000000000..2d90794b31 --- /dev/null +++ b/mobile-v2/lib/domain/models/render_list_element.model.dart @@ -0,0 +1,56 @@ +sealed class RenderListElement { + const RenderListElement(); +} + +class RenderListMonthHeaderElement extends RenderListElement { + final String header; + + const RenderListMonthHeaderElement({required this.header}); +} + +class RenderListDayHeaderElement extends RenderListElement { + final String header; + + const RenderListDayHeaderElement({required this.header}); +} + +class RenderListAssetElement extends RenderListElement { + final DateTime date; + final int assetCount; + final int assetOffset; + + const RenderListAssetElement({ + required this.date, + required this.assetCount, + required this.assetOffset, + }); + + RenderListAssetElement copyWith({ + DateTime? date, + int? assetCount, + int? assetOffset, + }) { + return RenderListAssetElement( + date: date ?? this.date, + assetCount: assetCount ?? this.assetCount, + assetOffset: assetOffset ?? this.assetOffset, + ); + } + + @override + String toString() => + 'RenderListAssetElement(date: $date, assetCount: $assetCount, assetOffset: $assetOffset)'; + + @override + bool operator ==(covariant RenderListAssetElement other) { + if (identical(this, other)) return true; + + return other.date == date && + other.assetCount == assetCount && + other.assetOffset == assetOffset; + } + + @override + int get hashCode => + date.hashCode ^ assetCount.hashCode ^ assetOffset.hashCode; +} diff --git a/mobile-v2/lib/domain/repositories/asset.repository.dart b/mobile-v2/lib/domain/repositories/asset.repository.dart new file mode 100644 index 0000000000..e9cb398314 --- /dev/null +++ b/mobile-v2/lib/domain/repositories/asset.repository.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/entities/asset.entity.drift.dart'; +import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/domain/models/render_list.model.dart'; +import 'package:immich_mobile/domain/models/render_list_element.model.dart'; +import 'package:immich_mobile/domain/repositories/database.repository.dart'; +import 'package:immich_mobile/utils/extensions/drift.extension.dart'; +import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; +import 'package:intl/intl.dart'; + +class RemoteAssetDriftRepository with LogContext implements IAssetRepository { + final DriftDatabaseRepository _db; + + const RemoteAssetDriftRepository(this._db); + + @override + Future addAll(Iterable assets) async { + try { + await _db.batch((batch) => batch.insertAllOnConflictUpdate( + _db.asset, + assets.map(_toEntity), + )); + + return true; + } catch (e, s) { + log.severe("Cannot insert remote assets into table", e, s); + return false; + } + } + + @override + Future clearAll() async { + try { + await _db.asset.deleteAll(); + return true; + } catch (e, s) { + log.severe("Cannot clear remote assets", e, s); + return false; + } + } + + @override + Future> fetchAssets({int? offset, int? limit}) async { + final query = _db.asset.select() + ..orderBy([(asset) => OrderingTerm.desc(asset.createdTime)]); + + if (limit != null) { + query.limit(limit, offset: offset); + } + + return (await query.get()).map(_toModel).toList(); + } + + @override + Stream getRenderList() { + final assetCountExp = _db.asset.id.count(); + final createdTimeExp = _db.asset.createdTime; + final monthYearExp = _db.asset.createdTime.strftime('%m-%Y'); + + final query = _db.asset.selectOnly() + ..addColumns([assetCountExp, createdTimeExp]) + ..groupBy([monthYearExp]) + ..orderBy([OrderingTerm.desc(createdTimeExp)]); + + int lastAssetOffset = 0; + final monthFormatter = DateFormat.yMMMM(); + + return query + .expand((row) { + final createdTime = row.read(createdTimeExp)!; + final assetCount = row.read(assetCountExp)!; + final assetOffset = lastAssetOffset; + lastAssetOffset += assetCount; + + return [ + RenderListMonthHeaderElement( + header: monthFormatter.format(createdTime), + ), + RenderListAssetElement( + date: createdTime, + assetCount: assetCount, + assetOffset: assetOffset, + ), + ]; + }) + .watch() + .map((elements) { + final int totalCount; + final lastAssetElement = + elements.whereType().lastOrNull; + if (lastAssetElement == null) { + totalCount = 0; + } else { + totalCount = + lastAssetElement.assetCount + lastAssetElement.assetOffset; + } + + return RenderList(elements: elements, totalCount: totalCount); + }); + } +} + +AssetCompanion _toEntity(Asset asset) { + return AssetCompanion.insert( + localId: Value(asset.localId), + remoteId: Value(asset.remoteId), + name: asset.name, + checksum: asset.checksum, + height: Value(asset.height), + width: Value(asset.width), + type: asset.type, + createdTime: asset.createdTime, + duration: Value(asset.duration), + modifiedTime: Value(asset.modifiedTime), + livePhotoVideoId: Value(asset.livePhotoVideoId), + ); +} + +Asset _toModel(AssetData asset) { + return Asset( + id: asset.id, + localId: asset.localId, + remoteId: asset.remoteId, + name: asset.name, + type: asset.type, + checksum: asset.checksum, + createdTime: asset.createdTime, + modifiedTime: asset.modifiedTime, + height: asset.height, + width: asset.width, + livePhotoVideoId: asset.livePhotoVideoId, + duration: asset.duration, + ); +} diff --git a/mobile-v2/lib/domain/repositories/database.repository.dart b/mobile-v2/lib/domain/repositories/database.repository.dart index ebd905cc46..aeb99befa1 100644 --- a/mobile-v2/lib/domain/repositories/database.repository.dart +++ b/mobile-v2/lib/domain/repositories/database.repository.dart @@ -4,15 +4,14 @@ import 'package:drift_dev/api/migrations.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/entities/album.entity.dart'; -import 'package:immich_mobile/domain/entities/local_asset.entity.dart'; +import 'package:immich_mobile/domain/entities/asset.entity.dart'; import 'package:immich_mobile/domain/entities/log.entity.dart'; -import 'package:immich_mobile/domain/entities/remote_asset.entity.dart'; import 'package:immich_mobile/domain/entities/store.entity.dart'; import 'package:immich_mobile/domain/entities/user.entity.dart'; import 'database.repository.drift.dart'; -@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset]) +@DriftDatabase(tables: [Logs, Store, LocalAlbum, Asset, User]) class DriftDatabaseRepository extends $DriftDatabaseRepository { DriftDatabaseRepository([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'db')); @@ -27,6 +26,9 @@ class DriftDatabaseRepository extends $DriftDatabaseRepository { if (kDebugMode) { await validateDatabaseSchema(); } + + await customStatement('PRAGMA journal_mode = WAL'); + await customStatement('PRAGMA foreign_keys = ON'); }, // ignore: no-empty-block onUpgrade: (m, from, to) async {}, diff --git a/mobile-v2/lib/domain/repositories/log.repository.dart b/mobile-v2/lib/domain/repositories/log.repository.dart index 90192e5e1a..f4975c98e4 100644 --- a/mobile-v2/lib/domain/repositories/log.repository.dart +++ b/mobile-v2/lib/domain/repositories/log.repository.dart @@ -8,21 +8,21 @@ import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart'; class LogDriftRepository implements ILogRepository { - final DriftDatabaseRepository db; + final DriftDatabaseRepository _db; - const LogDriftRepository(this.db); + const LogDriftRepository(this._db); @override Future> fetchAll() async { - return await db.managers.logs.map(_toModel).get(); + return await _db.managers.logs.map(_toModel).get(); } @override Future truncateLogs({int limit = 250}) async { - final totalCount = await db.managers.logs.count(); + final totalCount = await _db.managers.logs.count(); if (totalCount > limit) { final rowsToDelete = totalCount - limit; - await db.managers.logs + await _db.managers.logs .orderBy((o) => o.createdAt.desc()) .limit(rowsToDelete) .delete(); @@ -32,7 +32,7 @@ class LogDriftRepository implements ILogRepository { @override FutureOr add(LogMessage log) async { try { - await db.into(db.logs).insert(LogsCompanion.insert( + await _db.into(_db.logs).insert(LogsCompanion.insert( content: log.content, level: log.level, createdAt: Value(log.createdAt), @@ -50,9 +50,9 @@ class LogDriftRepository implements ILogRepository { @override FutureOr addAll(List logs) async { try { - await db.batch((b) { + await _db.batch((b) { b.insertAll( - db.logs, + _db.logs, logs.map((log) => LogsCompanion.insert( content: log.content, level: log.level, @@ -73,7 +73,7 @@ class LogDriftRepository implements ILogRepository { @override FutureOr clear() async { try { - await db.managers.logs.delete(); + await _db.managers.logs.delete(); return true; } catch (e) { debugPrint("Error while clearning the logs in DB - $e"); diff --git a/mobile-v2/lib/domain/repositories/remote_asset.repository.dart b/mobile-v2/lib/domain/repositories/remote_asset.repository.dart deleted file mode 100644 index 1b64225029..0000000000 --- a/mobile-v2/lib/domain/repositories/remote_asset.repository.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/entities/remote_asset.entity.drift.dart'; -import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart'; -import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart'; -import 'package:immich_mobile/domain/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; - -class RemoteAssetDriftRepository - with LogContext - implements IRemoteAssetRepository { - final DriftDatabaseRepository _db; - - const RemoteAssetDriftRepository(this._db); - - @override - Future addAll(Iterable assets) async { - try { - await _db.batch((batch) => batch.insertAllOnConflictUpdate( - _db.remoteAsset, - assets.map(_toEntity), - )); - - return true; - } catch (e, s) { - log.severe("Cannot insert remote assets into table", e, s); - return false; - } - } -} - -RemoteAssetCompanion _toEntity(RemoteAsset asset) { - return RemoteAssetCompanion.insert( - name: asset.name, - checksum: asset.checksum, - height: Value(asset.height), - width: Value(asset.width), - type: asset.type, - createdTime: asset.createdTime, - remoteId: asset.remoteId, - duration: Value(asset.duration), - modifiedTime: Value(asset.modifiedTime), - livePhotoVideoId: Value(asset.livePhotoVideoId), - ); -} diff --git a/mobile-v2/lib/domain/repositories/store.repository.dart b/mobile-v2/lib/domain/repositories/store.repository.dart index da1c1c2153..8896c5ac98 100644 --- a/mobile-v2/lib/domain/repositories/store.repository.dart +++ b/mobile-v2/lib/domain/repositories/store.repository.dart @@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; class StoreDriftRepository with LogContext implements IStoreRepository { - final DriftDatabaseRepository db; + final DriftDatabaseRepository _db; - const StoreDriftRepository(this.db); + const StoreDriftRepository(this._db); @override FutureOr tryGet(StoreKey key) async { - final storeData = await db.managers.store + final storeData = await _db.managers.store .filter((s) => s.id.equals(key.id)) .getSingleOrNull(); return _getValueFromStoreData(key, storeData); @@ -35,7 +35,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository { final storeValue = key.converter.toPrimitive(value); final intValue = (key.type == int) ? storeValue as int : null; final stringValue = (key.type == String) ? storeValue as String : null; - await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert( + await _db.into(_db.store).insertOnConflictUpdate(StoreCompanion.insert( id: Value(key.id), intValue: Value(intValue), stringValue: Value(stringValue), @@ -49,12 +49,12 @@ class StoreDriftRepository with LogContext implements IStoreRepository { @override FutureOr delete(StoreKey key) async { - await db.managers.store.filter((s) => s.id.equals(key.id)).delete(); + await _db.managers.store.filter((s) => s.id.equals(key.id)).delete(); } @override Stream watch(StoreKey key) { - return db.managers.store + return _db.managers.store .filter((s) => s.id.equals(key.id)) .watchSingleOrNull() .asyncMap((e) async => await _getValueFromStoreData(key, e)); @@ -62,8 +62,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository { @override FutureOr clearStore() async { - await db.managers.store.delete(); - + await _db.managers.store.delete(); } FutureOr _getValueFromStoreData( diff --git a/mobile-v2/lib/domain/repositories/user.repository.dart b/mobile-v2/lib/domain/repositories/user.repository.dart index 4792878470..7a8c3ea222 100644 --- a/mobile-v2/lib/domain/repositories/user.repository.dart +++ b/mobile-v2/lib/domain/repositories/user.repository.dart @@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; class UserDriftRepository with LogContext implements IUserRepository { - final DriftDatabaseRepository db; + final DriftDatabaseRepository _db; - const UserDriftRepository(this.db); + const UserDriftRepository(this._db); @override FutureOr fetch(String userId) async { - return await db.managers.user + return await _db.managers.user .filter((f) => f.id.equals(userId)) .map(_toModel) .getSingleOrNull(); @@ -23,7 +23,7 @@ class UserDriftRepository with LogContext implements IUserRepository { @override FutureOr add(User user) async { try { - await db.into(db.user).insertOnConflictUpdate( + await _db.into(_db.user).insertOnConflictUpdate( UserCompanion.insert( id: user.id, name: user.name, diff --git a/mobile-v2/lib/domain/services/sync.service.dart b/mobile-v2/lib/domain/services/sync.service.dart index 4c2d29075e..aed0986648 100644 --- a/mobile-v2/lib/domain/services/sync.service.dart +++ b/mobile-v2/lib/domain/services/sync.service.dart @@ -1,6 +1,6 @@ import 'package:drift/isolate.dart'; -import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart'; -import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart'; +import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/service_locator.dart'; @@ -53,8 +53,7 @@ class SyncService with LogContext { break; } - await di() - .addAll(assets.map(RemoteAsset.fromDto)); + await di().addAll(assets.map(Asset.remote)); lastAssetId = assets.lastOrNull?.id; if (assets.length != chunkSize) break; diff --git a/mobile-v2/lib/immich_app.dart b/mobile-v2/lib/immich_app.dart index d207cacb02..4ac9b61d25 100644 --- a/mobile-v2/lib/immich_app.dart +++ b/mobile-v2/lib/immich_app.dart @@ -9,14 +9,14 @@ import 'package:immich_mobile/presentation/router/router.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/constants/globals.dart'; -class ImmichApp extends StatefulWidget { - const ImmichApp({super.key}); +class ImApp extends StatefulWidget { + const ImApp({super.key}); @override - State createState() => _ImmichAppState(); + State createState() => _ImAppState(); } -class _ImmichAppState extends State with WidgetsBindingObserver { +class _ImAppState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { return TranslationProvider( diff --git a/mobile-v2/lib/main.dart b/mobile-v2/lib/main.dart index a37187541f..e7cdca3eff 100644 --- a/mobile-v2/lib/main.dart +++ b/mobile-v2/lib/main.dart @@ -14,5 +14,5 @@ void main() { // Init localization LocaleSettings.useDeviceLocale(); - runApp(const ImmichApp()); + runApp(const ImApp()); } diff --git a/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart b/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart new file mode 100644 index 0000000000..4531af9147 --- /dev/null +++ b/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; +import 'package:immich_mobile/domain/models/render_list_element.model.dart'; +import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart'; +import 'package:immich_mobile/service_locator.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +class ImAssetGrid extends StatelessWidget { + const ImAssetGrid({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: di().getRenderList(), + builder: (_, renderSnap) { + final renderList = renderSnap.data; + if (renderList == null) { + return const SizedBox.shrink(); + } + + final elements = renderList.elements; + return ScrollablePositionedList.builder( + itemCount: elements.length, + itemBuilder: (_, sectionIndex) { + final section = elements[sectionIndex]; + + return switch (section) { + RenderListMonthHeaderElement() => Text(section.header), + RenderListDayHeaderElement() => Text(section.header), + RenderListAssetElement() => FutureBuilder( + future: renderList.loadAssets( + section.assetOffset, + section.assetCount, + ), + builder: (_, assetsSnap) { + final assets = assetsSnap.data; + if (assets == null) { + return const SizedBox.shrink(); + } + return GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + ), + itemBuilder: (_, i) { + return SizedBox.square( + dimension: 200, + // ignore: avoid-unsafe-collection-methods + child: ImImage(assets.elementAt(i)), + ); + }, + itemCount: section.assetCount, + ); + }, + ), + }; + }, + ); + }, + ); + } +} diff --git a/mobile-v2/lib/presentation/components/image/immich_image.widget.dart b/mobile-v2/lib/presentation/components/image/immich_image.widget.dart new file mode 100644 index 0000000000..515c74150e --- /dev/null +++ b/mobile-v2/lib/presentation/components/image/immich_image.widget.dart @@ -0,0 +1,50 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/immich_api_client.dart'; +import 'package:immich_mobile/utils/immich_image_url_helper.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class ImImage extends StatelessWidget { + final Asset asset; + final double? width; + final double? height; + + const ImImage(this.asset, {this.width, this.height, super.key}); + + @override + Widget build(BuildContext context) { + return CachedNetworkImage( + imageUrl: ImImageUrlHelper.getThumbnailUrl(asset), + httpHeaders: di().headers, + cacheKey: ImImageUrlHelper.getThumbnailUrl(asset), + width: width, + height: height, + // keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and + // maxHeightDiskCache = null allows to simply store the webp thumbnail + // from the server and use it for all rendered thumbnail sizes + fit: BoxFit.cover, + fadeInDuration: const Duration(milliseconds: 250), + progressIndicatorBuilder: (_, url, downloadProgress) { + // Show loading if desired + return const SizedBox.square( + dimension: 250, + child: DecoratedBox(decoration: BoxDecoration(color: Colors.grey)), + ); + }, + errorWidget: (_, url, error) { + if (error is HttpExceptionWithStatus && + error.statusCode >= 400 && + error.statusCode < 500) { + CachedNetworkImage.evictFromCache(url); + } + return Icon( + Symbols.image_not_supported_rounded, + color: Theme.of(context).primaryColor, + ); + }, + ); + } +} diff --git a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart index 444398bf0f..257d3c0afe 100644 --- a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart +++ b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart @@ -1,8 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/services/sync.service.dart'; -import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart'; -import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart'; @RoutePage() class HomePage extends StatelessWidget { @@ -10,12 +8,6 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: ElevatedButton( - onPressed: () => di() - .doFullSyncForUserDrift(di().state), - child: const Text('Sync'), - ), - ); + return const ImAssetGrid(); } } diff --git a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart index 34a5b5ed2a..beab452d86 100644 --- a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart +++ b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; @@ -135,7 +136,8 @@ class LoginPageCubit extends Cubit with LogContext { // Register user ServiceLocator.registerCurrentUser(user); await di().add(user); - // Sync assets in background + // Remove and Sync assets in background + await di().clearAll(); unawaited(di().doFullSyncForUserDrift(user)); emit(state.copyWith( diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart index 7b0298abfe..77cc7403e5 100644 --- a/mobile-v2/lib/service_locator.dart +++ b/mobile-v2/lib/service_locator.dart @@ -1,12 +1,12 @@ import 'package:get_it/get_it.dart'; +import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; -import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/repositories/asset.repository.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/domain/repositories/log.repository.dart'; -import 'package:immich_mobile/domain/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/domain/repositories/store.repository.dart'; import 'package:immich_mobile/domain/repositories/user.repository.dart'; import 'package:immich_mobile/domain/services/app_setting.service.dart'; @@ -44,7 +44,7 @@ class ServiceLocator { di.registerFactory(() => LogDriftRepository(di())); di.registerFactory(() => AppSettingService(di())); di.registerFactory(() => UserDriftRepository(di())); - di.registerFactory( + di.registerFactory( () => RemoteAssetDriftRepository(di()), ); diff --git a/mobile-v2/lib/utils/extensions/drift.extension.dart b/mobile-v2/lib/utils/extensions/drift.extension.dart new file mode 100644 index 0000000000..27121f4b1c --- /dev/null +++ b/mobile-v2/lib/utils/extensions/drift.extension.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; + +extension ExpandQuery on Selectable { + /// Expands this selectable by the [expand] function. + /// + /// Each entry emitted by this [Selectable] will be transformed by the + /// [expander] and then emitted to the selectable returned. + Selectable expand(Iterable Function(T) expander) { + return _ExpandedSelectable(this, expander); + } +} + +class _ExpandedSelectable extends Selectable { + final Selectable _source; + final Iterable Function(S) expander; + + _ExpandedSelectable(this._source, this.expander); + + @override + Future> get() { + return _source.get().then(_mapResults); + } + + @override + Stream> watch() { + return _source.watch().map(_mapResults); + } + + List _mapResults(List results) => results.expand(expander).toList(); +} diff --git a/mobile-v2/lib/utils/immich_api_client.dart b/mobile-v2/lib/utils/immich_api_client.dart index 0899779e85..ea16096f64 100644 --- a/mobile-v2/lib/utils/immich_api_client.dart +++ b/mobile-v2/lib/utils/immich_api_client.dart @@ -13,19 +13,21 @@ import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:openapi/api.dart'; @immutable -class ImmichApiClientData { +class ImApiClientData { final String endpoint; final Map headersMap; - const ImmichApiClientData({required this.endpoint, required this.headersMap}); + const ImApiClientData({required this.endpoint, required this.headersMap}); } class ImmichApiClient extends ApiClient with LogContext { ImmichApiClient({required String endpoint}) : super(basePath: endpoint); /// Used to recreate the client in Isolates - ImmichApiClientData get clientData => - ImmichApiClientData(endpoint: basePath, headersMap: defaultHeaderMap); + ImApiClientData get clientData => + ImApiClientData(endpoint: basePath, headersMap: defaultHeaderMap); + + Map get headers => defaultHeaderMap; Future init({String? accessToken}) async { final token = @@ -47,7 +49,7 @@ class ImmichApiClient extends ApiClient with LogContext { addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem); } - factory ImmichApiClient.clientData(ImmichApiClientData data) { + factory ImmichApiClient.clientData(ImApiClientData data) { final client = ImmichApiClient(endpoint: data.endpoint); for (final entry in data.headersMap.entries) { diff --git a/mobile-v2/lib/utils/immich_image_url_helper.dart b/mobile-v2/lib/utils/immich_image_url_helper.dart new file mode 100644 index 0000000000..ca0063027c --- /dev/null +++ b/mobile-v2/lib/utils/immich_image_url_helper.dart @@ -0,0 +1,41 @@ +import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/immich_api_client.dart'; +import 'package:openapi/api.dart'; + +class ImImageUrlHelper { + const ImImageUrlHelper(); + + static String get _serverUrl => di().basePath; + + static String getThumbnailUrl( + final Asset asset, { + AssetMediaSize type = AssetMediaSize.thumbnail, + }) { + return _getThumbnailUrlForRemoteId(asset.remoteId!, type: type); + } + + static String getThumbnailCacheKey( + final Asset asset, { + AssetMediaSize type = AssetMediaSize.thumbnail, + }) { + return _getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type); + } + + static String _getThumbnailCacheKeyForRemoteId( + final String id, { + AssetMediaSize type = AssetMediaSize.thumbnail, + }) { + if (type == AssetMediaSize.thumbnail) { + return 'thumbnail-image-$id'; + } + return 'preview-image-$id'; + } + + static String _getThumbnailUrlForRemoteId( + final String id, { + AssetMediaSize type = AssetMediaSize.thumbnail, + }) { + return '$_serverUrl/assets/$id/thumbnail?size=${type.value}'; + } +} diff --git a/mobile-v2/lib/utils/log_manager.dart b/mobile-v2/lib/utils/log_manager.dart index af168c0062..f7ccb75934 100644 --- a/mobile-v2/lib/utils/log_manager.dart +++ b/mobile-v2/lib/utils/log_manager.dart @@ -5,8 +5,6 @@ import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:logging/logging.dart'; -// ignore: depend_on_referenced_packages -import 'package:stack_trace/stack_trace.dart' as stack_trace; /// [LogManager] is a custom logger that is built on top of the [logging] package. /// The logs are written to the database and onto console, using `debugPrint` method. @@ -29,7 +27,7 @@ class LogManager { debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); if (record.error != null && record.stackTrace != null) { debugPrint('${record.error}'); - debugPrintStack(stackTrace: record.stackTrace); + debugPrint('${record.stackTrace}'); } return true; }()); @@ -76,12 +74,6 @@ class LogManager { } static void setGlobalErrorCallbacks() { - FlutterError.demangleStackTrace = (StackTrace stack) { - if (stack is stack_trace.Trace) return stack.vmTrace; - if (stack is stack_trace.Chain) return stack.toTrace().vmTrace; - return stack; - }; - FlutterError.onError = (details) { Logger("FlutterError").severe( 'Unknown framework error occured in library ${details.library ?? ""} at node ${details.context ?? ""}', diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index edc4c34633..4a19dd2aef 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -142,6 +142,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -234,10 +258,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dartx: dependency: transitive description: @@ -266,18 +290,18 @@ packages: dependency: "direct main" description: name: drift - sha256: "15b51e0ee1970455c0c3f7e560f3ac02ecb9c04711a9657586e470b234659dba" + sha256: "5b561ec76fff260e1e0593a29ca0d058a140a4b4dfb11dcc0c3813820cd20200" url: "https://pub.dev" source: hosted - version: "2.20.0" + version: "2.20.2" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: b9ec6159a731288e805a44225ccbebad507dd84d52ab71352c52584f13199d2d + sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756" url: "https://pub.dev" source: hosted - version: "2.20.1" + version: "2.20.3" drift_flutter: dependency: "direct main" description: @@ -335,10 +359,10 @@ packages: dependency: "direct main" description: name: flutter_adaptive_scaffold - sha256: "3b8f56e0282659db2ebb2edacf61332c1178e8dc03d933709c5af88f92f31dd5" + sha256: "6b587d439c7da037432bbfc78d9676e1d08f2d7490f08e8d689a20f08e049802" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.6" flutter_bloc: dependency: "direct main" description: @@ -347,22 +371,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_gen_core: dependency: transitive description: name: flutter_gen_core - sha256: d8e828ad015a8511624491b78ad8e3f86edb7993528b1613aefbb4ad95947795 + sha256: "638d518897f1aefc55a24278968027591d50223a6943b6ae9aa576fe1494d99d" url: "https://pub.dev" source: hosted - version: "5.6.0" + version: "5.7.0" flutter_gen_runner: dependency: "direct dev" description: name: flutter_gen_runner - sha256: "931b03f77c164df0a4815aac0efc619a6ac8ec4cada55025119fca4894dada90" + sha256: "7f2f02d95e3ec96cf70a1c515700c0dd3ea905af003303a55d6fb081240e6b8a" url: "https://pub.dev" source: hosted - version: "5.6.0" + version: "5.7.0" flutter_lints: dependency: "direct dev" description: @@ -475,7 +507,7 @@ packages: source: hosted version: "2.1.3" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf @@ -582,10 +614,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: "8f4abdb6bc714526ccf66e825b7391d7ca65239484ad92be71980fe73a57521c" + sha256: "66416c4e30bd363508e12669634fc4f3250b83b69e862de67f4f9c480cf42414" url: "https://pub.dev" source: hosted - version: "4.2780.0" + version: "4.2785.1" meta: dependency: transitive description: @@ -598,10 +630,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" nested: dependency: transitive description: @@ -610,6 +642,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" openapi: dependency: "direct main" description: @@ -717,10 +757,10 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f" + sha256: e29619443803c40385ee509abc7937835d9b5122f899940080d28b2dceed59c1 url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.3.0" photo_manager_image_provider: dependency: "direct main" description: @@ -785,6 +825,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" shelf: dependency: transitive description: @@ -810,10 +866,10 @@ packages: dependency: "direct main" description: name: slang - sha256: f68f6d6709890f85efabfb0318e9d694be2ebdd333e57fe5cb50eee449e4e3ab + sha256: a2f704508bf9f209b71c881347bd27de45309651e9bd63570e4dd6ed2a77fbd2 url: "https://pub.dev" source: hosted - version: "3.31.1" + version: "3.31.2" slang_build_runner: dependency: "direct dev" description: @@ -846,6 +902,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + url: "https://pub.dev" + source: hosted + version: "2.5.4+2" sqlite3: dependency: "direct main" description: @@ -866,10 +946,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "3be52b4968fc2f098ba735863404756d2fe3ea0729cf006a5b5612618f74ca04" + sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b" url: "https://pub.dev" source: hosted - version: "0.37.1" + version: "0.38.1" stack_trace: dependency: transitive description: @@ -902,6 +982,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: @@ -1006,6 +1094,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_graphics_codec: dependency: transitive description: diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml index 7b1c8a8f72..b17ed90f8a 100644 --- a/mobile-v2/pubspec.yaml +++ b/mobile-v2/pubspec.yaml @@ -13,13 +13,17 @@ dependencies: flutter_localizations: sdk: flutter - # OS specific path + # Platform related path_provider: ^2.1.4 path: ^1.9.0 + dynamic_color: ^1.7.0 + url_launcher: ^6.3.0 + package_info_plus: ^8.0.2 + device_info_plus: ^10.1.2 # State handling flutter_bloc: ^8.1.6 # Database - drift: ^2.20.0 + drift: ^2.20.2 drift_flutter: ^0.2.0 sqlite3: ^2.4.6 sqlite3_flutter_libs: ^0.5.24 @@ -34,24 +38,20 @@ dependencies: # service_locator get_it: ^7.7.0 # Photo Manager - photo_manager: ^3.2.3 + photo_manager: ^3.3.0 photo_manager_image_provider: ^2.1.1 - # Dynamic colors - Android - dynamic_color: ^1.7.0 - # Material symbols - material_symbols_icons: ^4.2780.0 # Localization - slang: ^3.31.1 + intl: ^0.19.0 + slang: ^3.31.2 slang_flutter: ^3.31.0 - # Adaptive scaffold - flutter_adaptive_scaffold: ^0.2.2 - # URL launching - url_launcher: ^6.3.0 - # plus_extensions - package_info_plus: ^8.0.2 - device_info_plus: ^10.1.2 # oauth login flutter_web_auth_2: ^3.1.2 + # components + material_symbols_icons: ^4.2785.1 + flutter_adaptive_scaffold: ^0.2.6 + scrollable_positioned_list: ^0.3.8 + cached_network_image: ^3.4.1 + flutter_cache_manager: ^3.4.1 openapi: path: openapi @@ -70,13 +70,13 @@ dev_dependencies: # Code generator build_runner: ^2.4.12 # Database helper - drift_dev: ^2.20.1 + drift_dev: ^2.20.3 # Route helper auto_route_generator: ^9.0.0 # Localization generator slang_build_runner: ^3.31.0 # Assets constant generator - flutter_gen_runner: ^5.6.0 + flutter_gen_runner: ^5.7.0 flutter: uses-material-design: true