mirror of
https://github.com/immich-app/immich.git
synced 2025-06-09 16:44:45 -04:00
feat: home grid
This commit is contained in:
parent
80009a77ec
commit
419d3669a2
@ -15,6 +15,7 @@ targets:
|
|||||||
skip_verification_code: true
|
skip_verification_code: true
|
||||||
generate_for: &drift_generate_for
|
generate_for: &drift_generate_for
|
||||||
- lib/domain/entities/*.dart
|
- lib/domain/entities/*.dart
|
||||||
|
- lib/domain/entities/views/*.dart
|
||||||
- lib/domain/repositories/database.repository.dart
|
- lib/domain/repositories/database.repository.dart
|
||||||
drift_dev:modular:
|
drift_dev:modular:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import 'package:drift/drift.dart';
|
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 {
|
class Asset extends Table {
|
||||||
const Asset();
|
const Asset();
|
||||||
|
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
TextColumn get name => text()();
|
TextColumn get name => text()();
|
||||||
TextColumn get checksum => text().unique()();
|
TextColumn get checksum => text().unique()();
|
||||||
IntColumn get height => integer().nullable()();
|
IntColumn get height => integer().nullable()();
|
||||||
@ -12,4 +17,11 @@ class Asset extends Table {
|
|||||||
DateTimeColumn get modifiedTime =>
|
DateTimeColumn get modifiedTime =>
|
||||||
dateTime().withDefault(currentDateAndTime)();
|
dateTime().withDefault(currentDateAndTime)();
|
||||||
IntColumn get duration => integer().withDefault(const Constant(0))();
|
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()();
|
||||||
}
|
}
|
||||||
|
@ -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<Column> get primaryKey => {localId};
|
|
||||||
}
|
|
@ -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<Column> get primaryKey => {remoteId};
|
|
||||||
}
|
|
16
mobile-v2/lib/domain/interfaces/asset.interface.dart
Normal file
16
mobile-v2/lib/domain/interfaces/asset.interface.dart
Normal file
@ -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<bool> addAll(Iterable<Asset> assets);
|
||||||
|
|
||||||
|
/// Removes all assets
|
||||||
|
Future<bool> clearAll();
|
||||||
|
|
||||||
|
/// Fetch assets from the [offset] with the [limit]
|
||||||
|
Future<List<Asset>> fetchAssets({int? offset, int? limit});
|
||||||
|
|
||||||
|
/// Streams assets as groups grouped by the group type passed
|
||||||
|
Stream<RenderList> getRenderList();
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart';
|
|
||||||
|
|
||||||
abstract class IRemoteAssetRepository {
|
|
||||||
/// Batch insert asset
|
|
||||||
Future<bool> addAll(Iterable<RemoteAsset> assets);
|
|
||||||
}
|
|
150
mobile-v2/lib/domain/models/asset.model.dart
Normal file
150
mobile-v2/lib/domain/models/asset.model.dart
Normal file
@ -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,
|
||||||
|
};
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
};
|
|
64
mobile-v2/lib/domain/models/render_list.model.dart
Normal file
64
mobile-v2/lib/domain/models/render_list.model.dart
Normal file
@ -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<RenderListElement> 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<Asset> _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<List<Asset>> 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<IAssetRepository>().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);
|
||||||
|
}
|
||||||
|
}
|
56
mobile-v2/lib/domain/models/render_list_element.model.dart
Normal file
56
mobile-v2/lib/domain/models/render_list_element.model.dart
Normal file
@ -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;
|
||||||
|
}
|
137
mobile-v2/lib/domain/repositories/asset.repository.dart
Normal file
137
mobile-v2/lib/domain/repositories/asset.repository.dart
Normal file
@ -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<bool> addAll(Iterable<Asset> 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<bool> clearAll() async {
|
||||||
|
try {
|
||||||
|
await _db.asset.deleteAll();
|
||||||
|
return true;
|
||||||
|
} catch (e, s) {
|
||||||
|
log.severe("Cannot clear remote assets", e, s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> 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<RenderList> 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<DateTime>(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<RenderListAssetElement>().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,
|
||||||
|
);
|
||||||
|
}
|
@ -4,15 +4,14 @@ import 'package:drift_dev/api/migrations.dart';
|
|||||||
import 'package:drift_flutter/drift_flutter.dart';
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/domain/entities/album.entity.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/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/store.entity.dart';
|
||||||
import 'package:immich_mobile/domain/entities/user.entity.dart';
|
import 'package:immich_mobile/domain/entities/user.entity.dart';
|
||||||
|
|
||||||
import 'database.repository.drift.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 {
|
class DriftDatabaseRepository extends $DriftDatabaseRepository {
|
||||||
DriftDatabaseRepository([QueryExecutor? executor])
|
DriftDatabaseRepository([QueryExecutor? executor])
|
||||||
: super(executor ?? driftDatabase(name: 'db'));
|
: super(executor ?? driftDatabase(name: 'db'));
|
||||||
@ -27,6 +26,9 @@ class DriftDatabaseRepository extends $DriftDatabaseRepository {
|
|||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
await validateDatabaseSchema();
|
await validateDatabaseSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await customStatement('PRAGMA journal_mode = WAL');
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
},
|
},
|
||||||
// ignore: no-empty-block
|
// ignore: no-empty-block
|
||||||
onUpgrade: (m, from, to) async {},
|
onUpgrade: (m, from, to) async {},
|
||||||
|
@ -8,21 +8,21 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
|||||||
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
||||||
|
|
||||||
class LogDriftRepository implements ILogRepository {
|
class LogDriftRepository implements ILogRepository {
|
||||||
final DriftDatabaseRepository db;
|
final DriftDatabaseRepository _db;
|
||||||
|
|
||||||
const LogDriftRepository(this.db);
|
const LogDriftRepository(this._db);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<LogMessage>> fetchAll() async {
|
Future<List<LogMessage>> fetchAll() async {
|
||||||
return await db.managers.logs.map(_toModel).get();
|
return await _db.managers.logs.map(_toModel).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> truncateLogs({int limit = 250}) async {
|
Future<void> truncateLogs({int limit = 250}) async {
|
||||||
final totalCount = await db.managers.logs.count();
|
final totalCount = await _db.managers.logs.count();
|
||||||
if (totalCount > limit) {
|
if (totalCount > limit) {
|
||||||
final rowsToDelete = totalCount - limit;
|
final rowsToDelete = totalCount - limit;
|
||||||
await db.managers.logs
|
await _db.managers.logs
|
||||||
.orderBy((o) => o.createdAt.desc())
|
.orderBy((o) => o.createdAt.desc())
|
||||||
.limit(rowsToDelete)
|
.limit(rowsToDelete)
|
||||||
.delete();
|
.delete();
|
||||||
@ -32,7 +32,7 @@ class LogDriftRepository implements ILogRepository {
|
|||||||
@override
|
@override
|
||||||
FutureOr<bool> add(LogMessage log) async {
|
FutureOr<bool> add(LogMessage log) async {
|
||||||
try {
|
try {
|
||||||
await db.into(db.logs).insert(LogsCompanion.insert(
|
await _db.into(_db.logs).insert(LogsCompanion.insert(
|
||||||
content: log.content,
|
content: log.content,
|
||||||
level: log.level,
|
level: log.level,
|
||||||
createdAt: Value(log.createdAt),
|
createdAt: Value(log.createdAt),
|
||||||
@ -50,9 +50,9 @@ class LogDriftRepository implements ILogRepository {
|
|||||||
@override
|
@override
|
||||||
FutureOr<bool> addAll(List<LogMessage> logs) async {
|
FutureOr<bool> addAll(List<LogMessage> logs) async {
|
||||||
try {
|
try {
|
||||||
await db.batch((b) {
|
await _db.batch((b) {
|
||||||
b.insertAll(
|
b.insertAll(
|
||||||
db.logs,
|
_db.logs,
|
||||||
logs.map((log) => LogsCompanion.insert(
|
logs.map((log) => LogsCompanion.insert(
|
||||||
content: log.content,
|
content: log.content,
|
||||||
level: log.level,
|
level: log.level,
|
||||||
@ -73,7 +73,7 @@ class LogDriftRepository implements ILogRepository {
|
|||||||
@override
|
@override
|
||||||
FutureOr<bool> clear() async {
|
FutureOr<bool> clear() async {
|
||||||
try {
|
try {
|
||||||
await db.managers.logs.delete();
|
await _db.managers.logs.delete();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error while clearning the logs in DB - $e");
|
debugPrint("Error while clearning the logs in DB - $e");
|
||||||
|
@ -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<bool> addAll(Iterable<RemoteAsset> 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),
|
|
||||||
);
|
|
||||||
}
|
|
@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
|||||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||||
|
|
||||||
class StoreDriftRepository with LogContext implements IStoreRepository {
|
class StoreDriftRepository with LogContext implements IStoreRepository {
|
||||||
final DriftDatabaseRepository db;
|
final DriftDatabaseRepository _db;
|
||||||
|
|
||||||
const StoreDriftRepository(this.db);
|
const StoreDriftRepository(this._db);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key) async {
|
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key) async {
|
||||||
final storeData = await db.managers.store
|
final storeData = await _db.managers.store
|
||||||
.filter((s) => s.id.equals(key.id))
|
.filter((s) => s.id.equals(key.id))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
return _getValueFromStoreData(key, storeData);
|
return _getValueFromStoreData(key, storeData);
|
||||||
@ -35,7 +35,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
|||||||
final storeValue = key.converter.toPrimitive(value);
|
final storeValue = key.converter.toPrimitive(value);
|
||||||
final intValue = (key.type == int) ? storeValue as int : null;
|
final intValue = (key.type == int) ? storeValue as int : null;
|
||||||
final stringValue = (key.type == String) ? storeValue as String : 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),
|
id: Value(key.id),
|
||||||
intValue: Value(intValue),
|
intValue: Value(intValue),
|
||||||
stringValue: Value(stringValue),
|
stringValue: Value(stringValue),
|
||||||
@ -49,12 +49,12 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> delete(StoreKey key) async {
|
FutureOr<void> 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
|
@override
|
||||||
Stream<T?> watch<T, U>(StoreKey<T, U> key) {
|
Stream<T?> watch<T, U>(StoreKey<T, U> key) {
|
||||||
return db.managers.store
|
return _db.managers.store
|
||||||
.filter((s) => s.id.equals(key.id))
|
.filter((s) => s.id.equals(key.id))
|
||||||
.watchSingleOrNull()
|
.watchSingleOrNull()
|
||||||
.asyncMap((e) async => await _getValueFromStoreData(key, e));
|
.asyncMap((e) async => await _getValueFromStoreData(key, e));
|
||||||
@ -62,8 +62,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> clearStore() async {
|
FutureOr<void> clearStore() async {
|
||||||
await db.managers.store.delete();
|
await _db.managers.store.delete();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<T?> _getValueFromStoreData<T, U>(
|
FutureOr<T?> _getValueFromStoreData<T, U>(
|
||||||
|
@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
|||||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||||
|
|
||||||
class UserDriftRepository with LogContext implements IUserRepository {
|
class UserDriftRepository with LogContext implements IUserRepository {
|
||||||
final DriftDatabaseRepository db;
|
final DriftDatabaseRepository _db;
|
||||||
|
|
||||||
const UserDriftRepository(this.db);
|
const UserDriftRepository(this._db);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<User?> fetch(String userId) async {
|
FutureOr<User?> fetch(String userId) async {
|
||||||
return await db.managers.user
|
return await _db.managers.user
|
||||||
.filter((f) => f.id.equals(userId))
|
.filter((f) => f.id.equals(userId))
|
||||||
.map(_toModel)
|
.map(_toModel)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
@ -23,7 +23,7 @@ class UserDriftRepository with LogContext implements IUserRepository {
|
|||||||
@override
|
@override
|
||||||
FutureOr<bool> add(User user) async {
|
FutureOr<bool> add(User user) async {
|
||||||
try {
|
try {
|
||||||
await db.into(db.user).insertOnConflictUpdate(
|
await _db.into(_db.user).insertOnConflictUpdate(
|
||||||
UserCompanion.insert(
|
UserCompanion.insert(
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:drift/isolate.dart';
|
import 'package:drift/isolate.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
@ -53,8 +53,7 @@ class SyncService with LogContext {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await di<IRemoteAssetRepository>()
|
await di<IAssetRepository>().addAll(assets.map(Asset.remote));
|
||||||
.addAll(assets.map(RemoteAsset.fromDto));
|
|
||||||
|
|
||||||
lastAssetId = assets.lastOrNull?.id;
|
lastAssetId = assets.lastOrNull?.id;
|
||||||
if (assets.length != chunkSize) break;
|
if (assets.length != chunkSize) break;
|
||||||
|
@ -9,14 +9,14 @@ import 'package:immich_mobile/presentation/router/router.dart';
|
|||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
import 'package:immich_mobile/utils/constants/globals.dart';
|
||||||
|
|
||||||
class ImmichApp extends StatefulWidget {
|
class ImApp extends StatefulWidget {
|
||||||
const ImmichApp({super.key});
|
const ImApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State createState() => _ImmichAppState();
|
State createState() => _ImAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImmichAppState extends State<ImmichApp> with WidgetsBindingObserver {
|
class _ImAppState extends State<ImApp> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TranslationProvider(
|
return TranslationProvider(
|
||||||
|
@ -14,5 +14,5 @@ void main() {
|
|||||||
// Init localization
|
// Init localization
|
||||||
LocaleSettings.useDeviceLocale();
|
LocaleSettings.useDeviceLocale();
|
||||||
|
|
||||||
runApp(const ImmichApp());
|
runApp(const ImApp());
|
||||||
}
|
}
|
||||||
|
@ -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<IAssetRepository>().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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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<ImmichApiClient>().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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/services/sync.service.dart';
|
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart';
|
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatelessWidget {
|
||||||
@ -10,12 +8,6 @@ class HomePage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return const ImAssetGrid();
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => di<SyncService>()
|
|
||||||
.doFullSyncForUserDrift(di<CurrentUserCubit>().state),
|
|
||||||
child: const Text('Sync'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
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/store.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
@ -135,7 +136,8 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
|
|||||||
// Register user
|
// Register user
|
||||||
ServiceLocator.registerCurrentUser(user);
|
ServiceLocator.registerCurrentUser(user);
|
||||||
await di<IUserRepository>().add(user);
|
await di<IUserRepository>().add(user);
|
||||||
// Sync assets in background
|
// Remove and Sync assets in background
|
||||||
|
await di<IAssetRepository>().clearAll();
|
||||||
unawaited(di<SyncService>().doFullSyncForUserDrift(user));
|
unawaited(di<SyncService>().doFullSyncForUserDrift(user));
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
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/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/store.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/user.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/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/database.repository.dart';
|
||||||
import 'package:immich_mobile/domain/repositories/log.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/store.repository.dart';
|
||||||
import 'package:immich_mobile/domain/repositories/user.repository.dart';
|
import 'package:immich_mobile/domain/repositories/user.repository.dart';
|
||||||
import 'package:immich_mobile/domain/services/app_setting.service.dart';
|
import 'package:immich_mobile/domain/services/app_setting.service.dart';
|
||||||
@ -44,7 +44,7 @@ class ServiceLocator {
|
|||||||
di.registerFactory<ILogRepository>(() => LogDriftRepository(di()));
|
di.registerFactory<ILogRepository>(() => LogDriftRepository(di()));
|
||||||
di.registerFactory<AppSettingService>(() => AppSettingService(di()));
|
di.registerFactory<AppSettingService>(() => AppSettingService(di()));
|
||||||
di.registerFactory<IUserRepository>(() => UserDriftRepository(di()));
|
di.registerFactory<IUserRepository>(() => UserDriftRepository(di()));
|
||||||
di.registerFactory<IRemoteAssetRepository>(
|
di.registerFactory<IAssetRepository>(
|
||||||
() => RemoteAssetDriftRepository(di()),
|
() => RemoteAssetDriftRepository(di()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
30
mobile-v2/lib/utils/extensions/drift.extension.dart
Normal file
30
mobile-v2/lib/utils/extensions/drift.extension.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
extension ExpandQuery<T> on Selectable<T> {
|
||||||
|
/// 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<N> expand<N>(Iterable<N> Function(T) expander) {
|
||||||
|
return _ExpandedSelectable<T, N>(this, expander);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpandedSelectable<S, T> extends Selectable<T> {
|
||||||
|
final Selectable<S> _source;
|
||||||
|
final Iterable<T> Function(S) expander;
|
||||||
|
|
||||||
|
_ExpandedSelectable(this._source, this.expander);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<T>> get() {
|
||||||
|
return _source.get().then(_mapResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<T>> watch() {
|
||||||
|
return _source.watch().map(_mapResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> _mapResults(List<S> results) => results.expand<T>(expander).toList();
|
||||||
|
}
|
@ -13,19 +13,21 @@ import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class ImmichApiClientData {
|
class ImApiClientData {
|
||||||
final String endpoint;
|
final String endpoint;
|
||||||
final Map<String, String> headersMap;
|
final Map<String, String> headersMap;
|
||||||
|
|
||||||
const ImmichApiClientData({required this.endpoint, required this.headersMap});
|
const ImApiClientData({required this.endpoint, required this.headersMap});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApiClient extends ApiClient with LogContext {
|
class ImmichApiClient extends ApiClient with LogContext {
|
||||||
ImmichApiClient({required String endpoint}) : super(basePath: endpoint);
|
ImmichApiClient({required String endpoint}) : super(basePath: endpoint);
|
||||||
|
|
||||||
/// Used to recreate the client in Isolates
|
/// Used to recreate the client in Isolates
|
||||||
ImmichApiClientData get clientData =>
|
ImApiClientData get clientData =>
|
||||||
ImmichApiClientData(endpoint: basePath, headersMap: defaultHeaderMap);
|
ImApiClientData(endpoint: basePath, headersMap: defaultHeaderMap);
|
||||||
|
|
||||||
|
Map<String, String> get headers => defaultHeaderMap;
|
||||||
|
|
||||||
Future<void> init({String? accessToken}) async {
|
Future<void> init({String? accessToken}) async {
|
||||||
final token =
|
final token =
|
||||||
@ -47,7 +49,7 @@ class ImmichApiClient extends ApiClient with LogContext {
|
|||||||
addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem);
|
addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ImmichApiClient.clientData(ImmichApiClientData data) {
|
factory ImmichApiClient.clientData(ImApiClientData data) {
|
||||||
final client = ImmichApiClient(endpoint: data.endpoint);
|
final client = ImmichApiClient(endpoint: data.endpoint);
|
||||||
|
|
||||||
for (final entry in data.headersMap.entries) {
|
for (final entry in data.headersMap.entries) {
|
||||||
|
41
mobile-v2/lib/utils/immich_image_url_helper.dart
Normal file
41
mobile-v2/lib/utils/immich_image_url_helper.dart
Normal file
@ -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<ImmichApiClient>().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}';
|
||||||
|
}
|
||||||
|
}
|
@ -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/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:logging/logging.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.
|
/// [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.
|
/// 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}');
|
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
||||||
if (record.error != null && record.stackTrace != null) {
|
if (record.error != null && record.stackTrace != null) {
|
||||||
debugPrint('${record.error}');
|
debugPrint('${record.error}');
|
||||||
debugPrintStack(stackTrace: record.stackTrace);
|
debugPrint('${record.stackTrace}');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
@ -76,12 +74,6 @@ class LogManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void setGlobalErrorCallbacks() {
|
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) {
|
FlutterError.onError = (details) {
|
||||||
Logger("FlutterError").severe(
|
Logger("FlutterError").severe(
|
||||||
'Unknown framework error occured in library ${details.library ?? "<unknown>"} at node ${details.context ?? "<unkown>"}',
|
'Unknown framework error occured in library ${details.library ?? "<unknown>"} at node ${details.context ?? "<unkown>"}',
|
||||||
|
@ -142,6 +142,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.9.2"
|
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:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -234,10 +258,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.6"
|
version: "2.3.7"
|
||||||
dartx:
|
dartx:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -266,18 +290,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: "15b51e0ee1970455c0c3f7e560f3ac02ecb9c04711a9657586e470b234659dba"
|
sha256: "5b561ec76fff260e1e0593a29ca0d058a140a4b4dfb11dcc0c3813820cd20200"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.20.0"
|
version: "2.20.2"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: b9ec6159a731288e805a44225ccbebad507dd84d52ab71352c52584f13199d2d
|
sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.20.1"
|
version: "2.20.3"
|
||||||
drift_flutter:
|
drift_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -335,10 +359,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_adaptive_scaffold
|
name: flutter_adaptive_scaffold
|
||||||
sha256: "3b8f56e0282659db2ebb2edacf61332c1178e8dc03d933709c5af88f92f31dd5"
|
sha256: "6b587d439c7da037432bbfc78d9676e1d08f2d7490f08e8d689a20f08e049802"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.6"
|
||||||
flutter_bloc:
|
flutter_bloc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -347,22 +371,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.6"
|
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:
|
flutter_gen_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_gen_core
|
name: flutter_gen_core
|
||||||
sha256: d8e828ad015a8511624491b78ad8e3f86edb7993528b1613aefbb4ad95947795
|
sha256: "638d518897f1aefc55a24278968027591d50223a6943b6ae9aa576fe1494d99d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.6.0"
|
version: "5.7.0"
|
||||||
flutter_gen_runner:
|
flutter_gen_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_gen_runner
|
name: flutter_gen_runner
|
||||||
sha256: "931b03f77c164df0a4815aac0efc619a6ac8ec4cada55025119fca4894dada90"
|
sha256: "7f2f02d95e3ec96cf70a1c515700c0dd3ea905af003303a55d6fb081240e6b8a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.6.0"
|
version: "5.7.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -475,7 +507,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
@ -582,10 +614,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: material_symbols_icons
|
name: material_symbols_icons
|
||||||
sha256: "8f4abdb6bc714526ccf66e825b7391d7ca65239484ad92be71980fe73a57521c"
|
sha256: "66416c4e30bd363508e12669634fc4f3250b83b69e862de67f4f9c480cf42414"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2780.0"
|
version: "4.2785.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -598,10 +630,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.6"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -610,6 +642,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
openapi:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -717,10 +757,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: photo_manager
|
name: photo_manager
|
||||||
sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f"
|
sha256: e29619443803c40385ee509abc7937835d9b5122f899940080d28b2dceed59c1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.3"
|
version: "3.3.0"
|
||||||
photo_manager_image_provider:
|
photo_manager_image_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -785,6 +825,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
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:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -810,10 +866,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: slang
|
name: slang
|
||||||
sha256: f68f6d6709890f85efabfb0318e9d694be2ebdd333e57fe5cb50eee449e4e3ab
|
sha256: a2f704508bf9f209b71c881347bd27de45309651e9bd63570e4dd6ed2a77fbd2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.31.1"
|
version: "3.31.2"
|
||||||
slang_build_runner:
|
slang_build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -846,6 +902,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
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:
|
sqlite3:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -866,10 +946,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
sha256: "3be52b4968fc2f098ba735863404756d2fe3ea0729cf006a5b5612618f74ca04"
|
sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.37.1"
|
version: "0.38.1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -902,6 +982,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1006,6 +1094,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
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:
|
vector_graphics_codec:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -13,13 +13,17 @@ dependencies:
|
|||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# OS specific path
|
# Platform related
|
||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
path: ^1.9.0
|
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
|
# State handling
|
||||||
flutter_bloc: ^8.1.6
|
flutter_bloc: ^8.1.6
|
||||||
# Database
|
# Database
|
||||||
drift: ^2.20.0
|
drift: ^2.20.2
|
||||||
drift_flutter: ^0.2.0
|
drift_flutter: ^0.2.0
|
||||||
sqlite3: ^2.4.6
|
sqlite3: ^2.4.6
|
||||||
sqlite3_flutter_libs: ^0.5.24
|
sqlite3_flutter_libs: ^0.5.24
|
||||||
@ -34,24 +38,20 @@ dependencies:
|
|||||||
# service_locator
|
# service_locator
|
||||||
get_it: ^7.7.0
|
get_it: ^7.7.0
|
||||||
# Photo Manager
|
# Photo Manager
|
||||||
photo_manager: ^3.2.3
|
photo_manager: ^3.3.0
|
||||||
photo_manager_image_provider: ^2.1.1
|
photo_manager_image_provider: ^2.1.1
|
||||||
# Dynamic colors - Android
|
|
||||||
dynamic_color: ^1.7.0
|
|
||||||
# Material symbols
|
|
||||||
material_symbols_icons: ^4.2780.0
|
|
||||||
# Localization
|
# Localization
|
||||||
slang: ^3.31.1
|
intl: ^0.19.0
|
||||||
|
slang: ^3.31.2
|
||||||
slang_flutter: ^3.31.0
|
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
|
# oauth login
|
||||||
flutter_web_auth_2: ^3.1.2
|
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:
|
openapi:
|
||||||
path: openapi
|
path: openapi
|
||||||
@ -70,13 +70,13 @@ dev_dependencies:
|
|||||||
# Code generator
|
# Code generator
|
||||||
build_runner: ^2.4.12
|
build_runner: ^2.4.12
|
||||||
# Database helper
|
# Database helper
|
||||||
drift_dev: ^2.20.1
|
drift_dev: ^2.20.3
|
||||||
# Route helper
|
# Route helper
|
||||||
auto_route_generator: ^9.0.0
|
auto_route_generator: ^9.0.0
|
||||||
# Localization generator
|
# Localization generator
|
||||||
slang_build_runner: ^3.31.0
|
slang_build_runner: ^3.31.0
|
||||||
# Assets constant generator
|
# Assets constant generator
|
||||||
flutter_gen_runner: ^5.6.0
|
flutter_gen_runner: ^5.7.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user