feat: home grid

This commit is contained in:
shenlong-tanwen
2024-09-10 01:14:05 +05:30
parent 80009a77ec
commit 419d3669a2
33 changed files with 808 additions and 429 deletions
@@ -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,
};
@@ -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);
}
}
@@ -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;
}