Merge branch 'main' of github.com:immich-app/immich

This commit is contained in:
Alex
2025-07-07 15:44:28 -05:00
48 changed files with 2788 additions and 2463 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,84 @@
import 'dart:convert';
// Model for a stack stored in the server
class Stack {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
final String ownerId;
final String primaryAssetId;
const Stack({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.ownerId,
required this.primaryAssetId,
});
Stack copyWith({
String? id,
DateTime? createdAt,
DateTime? updatedAt,
String? ownerId,
String? primaryAssetId,
}) {
return Stack(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'id': id,
'createdAt': createdAt.millisecondsSinceEpoch,
'updatedAt': updatedAt.millisecondsSinceEpoch,
'ownerId': ownerId,
'primaryAssetId': primaryAssetId,
};
}
factory Stack.fromMap(Map<String, dynamic> map) {
return Stack(
id: map['id'] as String,
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
ownerId: map['ownerId'] as String,
primaryAssetId: map['primaryAssetId'] as String,
);
}
String toJson() => json.encode(toMap());
factory Stack.fromJson(String source) =>
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() {
return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
}
@override
bool operator ==(covariant Stack other) {
if (identical(this, other)) return true;
return other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.ownerId == ownerId &&
other.primaryAssetId == primaryAssetId;
}
@override
int get hashCode {
return id.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
ownerId.hashCode ^
primaryAssetId.hashCode;
}
}

View File

@@ -154,6 +154,25 @@ class SyncStreamService {
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
case SyncEntityType.memoryToAssetDeleteV1:
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
case SyncEntityType.stackV1:
return _syncStreamRepository.updateStacksV1(data.cast());
case SyncEntityType.stackDeleteV1:
return _syncStreamRepository.deleteStacksV1(data.cast());
case SyncEntityType.partnerStackV1:
return _syncStreamRepository.updateStacksV1(
data.cast(),
debugLabel: 'partner',
);
case SyncEntityType.partnerStackBackfillV1:
return _syncStreamRepository.updateStacksV1(
data.cast(),
debugLabel: 'partner backfill',
);
case SyncEntityType.partnerStackDeleteV1:
return _syncStreamRepository.deleteStacksV1(
data.cast(),
debugLabel: 'partner',
);
default:
_logger.warning("Unknown sync data type: $type");
}

View File

@@ -53,6 +53,43 @@ class TimelineFactory {
bucketSource: () =>
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
);
TimelineService favorite(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getFavoriteBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy),
);
TimelineService trash(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getTrashBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
);
TimelineService archive(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getArchiveBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
);
TimelineService lockedFolder(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
userId,
groupBy: groupBy,
),
);
TimelineService video(String userId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getVideoBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchVideoBucket(userId, groupBy: groupBy),
);
}
class TimelineService {

View File

@@ -0,0 +1,22 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class StackEntity extends Table with DriftDefaultsMixin {
const StackEntity();
TextColumn get id => text()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get ownerId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)();
@override
Set<Column> get primaryKey => {id};
}

View File

@@ -0,0 +1,706 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart' as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i4;
import 'package:drift/internal/modular.dart' as i5;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i6;
typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion
Function({
required String id,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
required String ownerId,
required String primaryAssetId,
});
typedef $$StackEntityTableUpdateCompanionBuilder = i1.StackEntityCompanion
Function({
i0.Value<String> id,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<String> ownerId,
i0.Value<String> primaryAssetId,
});
final class $$StackEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase, i1.$StackEntityTable, i1.StackEntityData> {
$$StackEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$StackEntityTable>('stack_entity')
.ownerId,
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i4.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
static i6.$RemoteAssetEntityTable _primaryAssetIdTable(
i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$StackEntityTable>('stack_entity')
.primaryAssetId,
i5.ReadDatabaseContainer(db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
.id));
i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId {
final $_column = $_itemColumn<String>('primary_asset_id')!;
final manager = i6
.$$RemoteAssetEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer($_db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$StackEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
$$StackEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
i4.$$UserEntityTableFilterComposer get ownerId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId {
final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.primaryAssetId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i6.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$StackEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
$$StackEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column));
i4.$$UserEntityTableOrderingComposer get ownerId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId {
final i6.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.primaryAssetId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i6.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$StackEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
$$StackEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i4.$$UserEntityTableAnnotationComposer get ownerId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.ownerId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId {
final i6.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.primaryAssetId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i6.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i6.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$StackEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$StackEntityTable,
i1.StackEntityData,
i1.$$StackEntityTableFilterComposer,
i1.$$StackEntityTableOrderingComposer,
i1.$$StackEntityTableAnnotationComposer,
$$StackEntityTableCreateCompanionBuilder,
$$StackEntityTableUpdateCompanionBuilder,
(i1.StackEntityData, i1.$$StackEntityTableReferences),
i1.StackEntityData,
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> {
$$StackEntityTableTableManager(
i0.GeneratedDatabase db, i1.$StackEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$StackEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$StackEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$StackEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<String> ownerId = const i0.Value.absent(),
i0.Value<String> primaryAssetId = const i0.Value.absent(),
}) =>
i1.StackEntityCompanion(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
primaryAssetId: primaryAssetId,
),
createCompanionCallback: ({
required String id,
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
required String ownerId,
required String primaryAssetId,
}) =>
i1.StackEntityCompanion.insert(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
primaryAssetId: primaryAssetId,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$StackEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (ownerId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.ownerId,
referencedTable:
i1.$$StackEntityTableReferences._ownerIdTable(db),
referencedColumn:
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
) as T;
}
if (primaryAssetId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.primaryAssetId,
referencedTable: i1.$$StackEntityTableReferences
._primaryAssetIdTable(db),
referencedColumn: i1.$$StackEntityTableReferences
._primaryAssetIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$StackEntityTable,
i1.StackEntityData,
i1.$$StackEntityTableFilterComposer,
i1.$$StackEntityTableOrderingComposer,
i1.$$StackEntityTableAnnotationComposer,
$$StackEntityTableCreateCompanionBuilder,
$$StackEntityTableUpdateCompanionBuilder,
(i1.StackEntityData, i1.$$StackEntityTableReferences),
i1.StackEntityData,
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>;
class $StackEntityTable extends i2.StackEntity
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$StackEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _createdAtMeta =
const i0.VerificationMeta('createdAt');
@override
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime);
static const i0.VerificationMeta _updatedAtMeta =
const i0.VerificationMeta('updatedAt');
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime);
static const i0.VerificationMeta _ownerIdMeta =
const i0.VerificationMeta('ownerId');
@override
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
'owner_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _primaryAssetIdMeta =
const i0.VerificationMeta('primaryAssetId');
@override
late final i0.GeneratedColumn<String> primaryAssetId =
i0.GeneratedColumn<String>(
'primary_asset_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id)'));
@override
List<i0.GeneratedColumn> get $columns =>
[id, createdAt, updatedAt, ownerId, primaryAssetId];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'stack_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.StackEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
if (data.containsKey('updated_at')) {
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('owner_id')) {
context.handle(_ownerIdMeta,
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
} else if (isInserting) {
context.missing(_ownerIdMeta);
}
if (data.containsKey('primary_asset_id')) {
context.handle(
_primaryAssetIdMeta,
primaryAssetId.isAcceptableOrUnknown(
data['primary_asset_id']!, _primaryAssetIdMeta));
} else if (isInserting) {
context.missing(_primaryAssetIdMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.StackEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.StackEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
ownerId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
primaryAssetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!,
);
}
@override
$StackEntityTable createAlias(String alias) {
return $StackEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class StackEntityData extends i0.DataClass
implements i0.Insertable<i1.StackEntityData> {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
final String ownerId;
final String primaryAssetId;
const StackEntityData(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.ownerId,
required this.primaryAssetId});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['created_at'] = i0.Variable<DateTime>(createdAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
map['owner_id'] = i0.Variable<String>(ownerId);
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId);
return map;
}
factory StackEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return StackEntityData(
id: serializer.fromJson<String>(json['id']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
ownerId: serializer.fromJson<String>(json['ownerId']),
primaryAssetId: serializer.fromJson<String>(json['primaryAssetId']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'createdAt': serializer.toJson<DateTime>(createdAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'ownerId': serializer.toJson<String>(ownerId),
'primaryAssetId': serializer.toJson<String>(primaryAssetId),
};
}
i1.StackEntityData copyWith(
{String? id,
DateTime? createdAt,
DateTime? updatedAt,
String? ownerId,
String? primaryAssetId}) =>
i1.StackEntityData(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
);
StackEntityData copyWithCompanion(i1.StackEntityCompanion data) {
return StackEntityData(
id: data.id.present ? data.id.value : this.id,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
primaryAssetId: data.primaryAssetId.present
? data.primaryAssetId.value
: this.primaryAssetId,
);
}
@override
String toString() {
return (StringBuffer('StackEntityData(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('ownerId: $ownerId, ')
..write('primaryAssetId: $primaryAssetId')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.StackEntityData &&
other.id == this.id &&
other.createdAt == this.createdAt &&
other.updatedAt == this.updatedAt &&
other.ownerId == this.ownerId &&
other.primaryAssetId == this.primaryAssetId);
}
class StackEntityCompanion extends i0.UpdateCompanion<i1.StackEntityData> {
final i0.Value<String> id;
final i0.Value<DateTime> createdAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<String> ownerId;
final i0.Value<String> primaryAssetId;
const StackEntityCompanion({
this.id = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.ownerId = const i0.Value.absent(),
this.primaryAssetId = const i0.Value.absent(),
});
StackEntityCompanion.insert({
required String id,
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
required String ownerId,
required String primaryAssetId,
}) : id = i0.Value(id),
ownerId = i0.Value(ownerId),
primaryAssetId = i0.Value(primaryAssetId);
static i0.Insertable<i1.StackEntityData> custom({
i0.Expression<String>? id,
i0.Expression<DateTime>? createdAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<String>? ownerId,
i0.Expression<String>? primaryAssetId,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (createdAt != null) 'created_at': createdAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (ownerId != null) 'owner_id': ownerId,
if (primaryAssetId != null) 'primary_asset_id': primaryAssetId,
});
}
i1.StackEntityCompanion copyWith(
{i0.Value<String>? id,
i0.Value<DateTime>? createdAt,
i0.Value<DateTime>? updatedAt,
i0.Value<String>? ownerId,
i0.Value<String>? primaryAssetId}) {
return i1.StackEntityCompanion(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
ownerId: ownerId ?? this.ownerId,
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (ownerId.present) {
map['owner_id'] = i0.Variable<String>(ownerId.value);
}
if (primaryAssetId.present) {
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('StackEntityCompanion(')
..write('id: $id, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('ownerId: $ownerId, ')
..write('primaryAssetId: $primaryAssetId')
..write(')'))
.toString();
}
}

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:isar/isar.dart';
@@ -50,6 +51,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
RemoteAlbumUserEntity,
MemoryEntity,
MemoryAssetEntity,
StackEntity,
],
include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift',

View File

@@ -27,9 +27,11 @@ import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i12;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i14;
import 'package:drift/internal/modular.dart' as i15;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i15;
import 'package:drift/internal/modular.dart' as i16;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -58,8 +60,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
i13.$MemoryAssetEntityTable(this);
i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this)
.accessor<i14.MergedAssetDrift>(i14.MergedAssetDrift.new);
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -80,7 +83,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity
memoryAssetEntity,
stackEntity
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
@@ -205,6 +209,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
],
),
],
);
@override
@@ -242,4 +253,6 @@ class $DriftManager {
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i14.$$StackEntityTableTableManager get stackEntity =>
i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
}

View File

@@ -0,0 +1,30 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftStackRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftStackRepository(this._db) : super(_db);
Future<List<Stack>> getAll(String userId) {
final query = _db.stackEntity.select()
..where((e) => e.ownerId.equals(userId));
return query.map((stack) {
return stack.toDto();
}).get();
}
}
extension on StackEntityData {
Stack toDto() {
return Stack(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
ownerId: ownerId,
primaryAssetId: primaryAssetId,
);
}
}

View File

@@ -54,6 +54,8 @@ class SyncApiRepository {
SyncRequestType.albumToAssetsV1,
SyncRequestType.memoriesV1,
SyncRequestType.memoryToAssetsV1,
SyncRequestType.stacksV1,
SyncRequestType.partnerStacksV1,
],
).toJson(),
);
@@ -163,6 +165,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
SyncEntityType.stackV1: SyncStackV1.fromJson,
SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson,
SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
};
class _SyncAckV1 {

View File

@@ -12,6 +12,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
@@ -69,8 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: SyncPartnerDeleteV1', error, stack);
rethrow;
}
}
@@ -92,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: SyncPartnerV1', error, stack);
rethrow;
}
}
@@ -104,10 +105,10 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}) async {
try {
await _db.remoteAssetEntity.deleteWhere(
(row) => row.id.isIn(data.map((error) => error.assetId)),
(row) => row.id.isIn(data.map((e) => e.assetId)),
);
} catch (error, stackTrace) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
rethrow;
}
}
@@ -142,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack);
rethrow;
}
}
@@ -186,11 +187,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
} catch (error, stack) {
_logger.severe(
'Error: updateAssetsExifV1 - $debugLabel',
error,
stackTrace,
stack,
);
rethrow;
}
@@ -201,8 +202,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
await _db.remoteAlbumEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.albumId)),
);
} catch (error, stackTrace) {
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: deleteAlbumsV1', error, stack);
rethrow;
}
}
@@ -229,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: updateAlbumsV1', error, stack);
rethrow;
}
}
@@ -248,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: deleteAlbumUsersV1', error, stack);
rethrow;
}
}
@@ -275,11 +276,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
} catch (error, stack) {
_logger.severe(
'Error: updateAlbumUsersV1 - $debugLabel',
error,
stackTrace,
stack,
);
rethrow;
}
@@ -300,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: deleteAlbumToAssetsV1', error, stack);
rethrow;
}
}
@@ -325,11 +326,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
} catch (error, stack) {
_logger.severe(
'Error: updateAlbumToAssetsV1 - $debugLabel',
error,
stackTrace,
stack,
);
rethrow;
}
@@ -359,8 +360,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: updateMemoriesV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: updateMemoriesV1', error, stack);
rethrow;
}
}
@@ -370,8 +371,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
await _db.memoryEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.memoryId)),
);
} catch (error, stackTrace) {
_logger.severe('Error: deleteMemoriesV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: deleteMemoriesV1', error, stack);
rethrow;
}
}
@@ -392,8 +393,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: updateMemoryAssetsV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: updateMemoryAssetsV1', error, stack);
rethrow;
}
}
@@ -413,8 +414,49 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace);
} catch (error, stack) {
_logger.severe('Error: deleteMemoryAssetsV1', error, stack);
rethrow;
}
}
Future<void> updateStacksV1(
Iterable<SyncStackV1> data, {
String debugLabel = 'user',
}) async {
try {
await _db.batch((batch) {
for (final stack in data) {
final companion = StackEntityCompanion(
createdAt: Value(stack.createdAt),
updatedAt: Value(stack.updatedAt),
ownerId: Value(stack.ownerId),
primaryAssetId: Value(stack.primaryAssetId),
);
batch.insert(
_db.stackEntity,
companion.copyWith(id: Value(stack.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: updateStacksV1 - $debugLabel', error, stack);
rethrow;
}
}
Future<void> deleteStacksV1(
Iterable<SyncStackDeleteV1> data, {
String debugLabel = 'user',
}) async {
try {
await _db.stackEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.stackId)),
);
} catch (error, stack) {
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
rethrow;
}
}
@@ -467,7 +509,7 @@ extension on String {
Duration? toDuration() {
try {
final parts = split(':')
.map((error) => double.parse(error).toInt())
.map((e) => double.parse(e).toInt())
.toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);

View File

@@ -213,6 +213,262 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
}
Stream<List<Bucket>> watchFavoriteBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.isFavorite.equals(true) & row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.isFavorite.equals(true),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getFavoriteBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.isFavorite.equals(true) & row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchTrashBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNotNull() & row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNotNull(),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getTrashBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchArchiveBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.visibility.equalsValue(AssetVisibility.archive) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.archive),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getArchiveBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.archive),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchLockedFolderBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.locked),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getLockedFolderBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchVideoBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getVideoBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
}
extension on Expression<DateTime> {

View File

@@ -118,7 +118,7 @@ class TabShellPage extends ConsumerWidget {
const MainTimelineRoute(),
SearchRoute(),
const DriftAlbumsRoute(),
const LibraryRoute(),
const DriftLibraryRoute(),
],
duration: const Duration(milliseconds: 600),
transitionBuilder: (context, child, animation) => FadeTransition(

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftArchivePage extends StatelessWidget {
const DriftArchivePage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access archive');
}
final timelineService =
ref.watch(timelineFactoryProvider).archive(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftFavoritePage extends StatelessWidget {
const DriftFavoritePage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access favorite');
}
final timelineService =
ref.watch(timelineFactoryProvider).favorite(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftLockedFolderPage extends StatelessWidget {
const DriftLockedFolderPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access locked folder');
}
final timelineService =
ref.watch(timelineFactoryProvider).lockedFolder(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftTrashPage extends StatelessWidget {
const DriftTrashPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access trash');
}
final timelineService =
ref.watch(timelineFactoryProvider).trash(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftVideoPage extends StatelessWidget {
const DriftVideoPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to video');
}
final timelineService =
ref.watch(timelineFactoryProvider).video(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}

View File

@@ -66,6 +66,9 @@ final _features = [
await db.remoteAlbumEntity.deleteAll();
await db.remoteAlbumUserEntity.deleteAll();
await db.remoteAlbumAssetEntity.deleteAll();
await db.memoryEntity.deleteAll();
await db.memoryAssetEntity.deleteAll();
await db.stackEntity.deleteAll();
},
),
_Feature(
@@ -96,6 +99,11 @@ final _features = [
icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
),
_Feature(
name: 'Video',
icon: Icons.video_collection_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
),
];
@RoutePage()

View File

@@ -162,6 +162,10 @@ final _remoteStats = [
name: 'Memories Assets',
load: (db) => db.managers.memoryAssetEntity.count(),
),
_Stat(
name: 'Stacks',
load: (db) => db.managers.stackEntity.count(),
),
];
@RoutePage()

View File

@@ -0,0 +1,501 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
class DriftLibraryPage extends ConsumerWidget {
const DriftLibraryPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return const Scaffold(
body: CustomScrollView(
slivers: [
ImmichSliverAppBar(),
_ActionButtonGrid(),
_CollectionCards(),
_QuickAccessButtonList(),
],
),
);
}
}
class _ActionButtonGrid extends ConsumerWidget {
const _ActionButtonGrid();
@override
Widget build(BuildContext context, WidgetRef ref) {
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
return SliverPadding(
padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 12),
sliver: SliverToBoxAdapter(
child: Column(
children: [
Row(
children: [
_ActionButton(
icon: Icons.favorite_outline_rounded,
onTap: () => context.pushRoute(const DriftFavoriteRoute()),
label: 'favorites'.t(context: context),
),
const SizedBox(width: 8),
_ActionButton(
icon: Icons.archive_outlined,
onTap: () => context.pushRoute(const DriftArchiveRoute()),
label: 'archived'.t(context: context),
),
],
),
const SizedBox(height: 8),
Row(
children: [
_ActionButton(
icon: Icons.link_outlined,
onTap: () => context.pushRoute(const SharedLinkRoute()),
label: 'shared_links'.t(context: context),
),
isTrashEnable
? const SizedBox(width: 8)
: const SizedBox.shrink(),
isTrashEnable
? _ActionButton(
icon: Icons.delete_outline_rounded,
onTap: () => context.pushRoute(const DriftTrashRoute()),
label: 'trash'.t(context: context),
)
: const SizedBox.shrink(),
],
),
],
),
),
);
}
}
class _ActionButton extends StatelessWidget {
const _ActionButton({
required this.icon,
required this.onTap,
required this.label,
});
final IconData icon;
final VoidCallback onTap;
final String label;
@override
Widget build(BuildContext context) {
return Expanded(
child: FilledButton.icon(
onPressed: onTap,
label: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
label,
style: TextStyle(
color: context.colorScheme.onSurface,
fontSize: 15,
),
),
),
style: FilledButton.styleFrom(
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
backgroundColor: context.colorScheme.surfaceContainerLow,
alignment: Alignment.centerLeft,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(25)),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
),
),
icon: Icon(
icon,
color: context.primaryColor,
),
),
);
}
}
class _CollectionCards extends StatelessWidget {
const _CollectionCards();
@override
Widget build(BuildContext context) {
return const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_PeopleCollectionCard(),
_PlacesCollectionCard(),
_LocalAlbumsCollectionCard(),
],
),
),
);
}
}
class _PeopleCollectionCard extends ConsumerWidget {
const _PeopleCollectionCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final people = ref.watch(getAllPeopleProvider);
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: people.widgetWhen(
onLoading: () => const Center(
child: CircularProgressIndicator(),
),
onData: (people) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: people.take(4).map((person) {
return CircleAvatar(
backgroundImage: NetworkImage(
getFaceThumbnailUrl(person.id),
headers: ApiService.getRequestHeaders(),
),
);
}).toList(),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'people'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _PlacesCollectionCard extends StatelessWidget {
const _PlacesCollectionCard();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(
PlacesCollectionRoute(
currentLocation: null,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
color:
context.colorScheme.secondaryContainer.withAlpha(100),
),
child: IgnorePointer(
child: MapThumbnail(
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode: context.isDarkTheme
? ThemeMode.dark
: ThemeMode.light,
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'places'.t(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _LocalAlbumsCollectionCard extends ConsumerWidget {
const _LocalAlbumsCollectionCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Migrate to the drift after local album page
final albums = ref.watch(localAlbumsProvider);
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(
const LocalAlbumsRoute(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: albums.take(4).map((album) {
return AlbumThumbnailCard(
album: album,
showTitle: false,
);
}).toList(),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'on_this_device'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _QuickAccessButtonList extends ConsumerWidget {
const _QuickAccessButtonList();
@override
Widget build(BuildContext context, WidgetRef ref) {
final partners = ref.watch(partnerSharedWithProvider);
return SliverPadding(
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(10),
context.colorScheme.primary.withAlpha(15),
context.colorScheme.primary.withAlpha(20),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
),
),
leading: const Icon(
Icons.folder_outlined,
size: 26,
),
title: Text(
'folders'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
onTap: () => context.pushRoute(FolderRoute()),
),
ListTile(
leading: const Icon(
Icons.lock_outline_rounded,
size: 26,
),
title: Text(
'locked_folder'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
// TODO: PIN code is needed
onTap: () => context.pushRoute(const DriftLockedFolderRoute()),
),
ListTile(
leading: const Icon(
Icons.group_outlined,
size: 26,
),
title: Text(
'partners'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
onTap: () => context.pushRoute(const PartnerRoute()),
),
_PartnerList(partners: partners),
],
),
),
),
);
}
}
class _PartnerList extends StatelessWidget {
const _PartnerList({required this.partners});
final List<UserDto> partners;
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
itemCount: partners.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final partner = partners[index];
final isLastItem = index == partners.length - 1;
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
bottomRight: Radius.circular(isLastItem ? 20 : 0),
),
),
contentPadding: const EdgeInsets.only(
left: 12.0,
right: 18.0,
),
leading: userAvatar(context, partner, radius: 16),
title: const Text(
"partner_list_user_photos",
style: TextStyle(
fontWeight: FontWeight.w500,
),
).t(context: context, args: {'user': partner.name}),
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
);
},
);
}
}

View File

@@ -0,0 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final driftStackProvider = Provider<DriftStackRepository>(
(ref) => DriftStackRepository(ref.watch(driftProvider)),
);

View File

@@ -40,6 +40,9 @@ class AuthRepository extends DatabaseRepository {
_drift.remoteAlbumEntity.deleteAll(),
_drift.remoteAlbumAssetEntity.deleteAll(),
_drift.remoteAlbumUserEntity.deleteAll(),
_drift.memoryEntity.deleteAll(),
_drift.memoryAssetEntity.deleteAll(),
_drift.stackEntity.deleteAll(),
]);
});
}

View File

@@ -66,12 +66,18 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_locked_folder.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -174,7 +180,7 @@ class AppRouter extends RootStackRouter {
maintainState: false,
),
AutoRoute(
page: LibraryRoute.page,
page: DriftLibraryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
@@ -392,7 +398,30 @@ class AppRouter extends RootStackRouter {
page: DriftMemoryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftFavoriteRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftTrashRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftArchiveRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftLockedFolderRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftVideoRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftLibraryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),

View File

@@ -618,6 +618,70 @@ class DriftAlbumsRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [DriftArchivePage]
class DriftArchiveRoute extends PageRouteInfo<void> {
const DriftArchiveRoute({List<PageRouteInfo>? children})
: super(DriftArchiveRoute.name, initialChildren: children);
static const String name = 'DriftArchiveRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftArchivePage();
},
);
}
/// generated route for
/// [DriftFavoritePage]
class DriftFavoriteRoute extends PageRouteInfo<void> {
const DriftFavoriteRoute({List<PageRouteInfo>? children})
: super(DriftFavoriteRoute.name, initialChildren: children);
static const String name = 'DriftFavoriteRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftFavoritePage();
},
);
}
/// generated route for
/// [DriftLibraryPage]
class DriftLibraryRoute extends PageRouteInfo<void> {
const DriftLibraryRoute({List<PageRouteInfo>? children})
: super(DriftLibraryRoute.name, initialChildren: children);
static const String name = 'DriftLibraryRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftLibraryPage();
},
);
}
/// generated route for
/// [DriftLockedFolderPage]
class DriftLockedFolderRoute extends PageRouteInfo<void> {
const DriftLockedFolderRoute({List<PageRouteInfo>? children})
: super(DriftLockedFolderRoute.name, initialChildren: children);
static const String name = 'DriftLockedFolderRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftLockedFolderPage();
},
);
}
/// generated route for
/// [DriftMemoryPage]
class DriftMemoryRoute extends PageRouteInfo<DriftMemoryRouteArgs> {
@@ -670,6 +734,38 @@ class DriftMemoryRouteArgs {
}
}
/// generated route for
/// [DriftTrashPage]
class DriftTrashRoute extends PageRouteInfo<void> {
const DriftTrashRoute({List<PageRouteInfo>? children})
: super(DriftTrashRoute.name, initialChildren: children);
static const String name = 'DriftTrashRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftTrashPage();
},
);
}
/// generated route for
/// [DriftVideoPage]
class DriftVideoRoute extends PageRouteInfo<void> {
const DriftVideoRoute({List<PageRouteInfo>? children})
: super(DriftVideoRoute.name, initialChildren: children);
static const String name = 'DriftVideoRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftVideoPage();
},
);
}
/// generated route for
/// [EditImagePage]
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {

View File

@@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/widgets/common/transparent_image.dart';
@@ -26,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
bool isDarkTheme = context.themeData.brightness == Brightness.dark;
final userAvatarColor = user.avatarColor.toColor();
final profileImageUrl =
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
@@ -34,14 +32,14 @@ class UserCircleAvatar extends ConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: isDarkTheme && user.avatarColor == AvatarColor.primary
color: userAvatarColor.computeLuminance() > 0.5
? Colors.black
: Colors.white,
),
child: Text(user.name[0].toUpperCase()),
);
return CircleAvatar(
backgroundColor: user.avatarColor.toColor(),
backgroundColor: userAvatarColor,
radius: radius,
child: user.profileImagePath == null
? textIcon

View File

@@ -24,6 +24,7 @@ class SystemConfigOAuthDto {
required this.mobileOverrideEnabled,
required this.mobileRedirectUri,
required this.profileSigningAlgorithm,
required this.roleClaim,
required this.scope,
required this.signingAlgorithm,
required this.storageLabelClaim,
@@ -55,6 +56,8 @@ class SystemConfigOAuthDto {
String profileSigningAlgorithm;
String roleClaim;
String scope;
String signingAlgorithm;
@@ -81,6 +84,7 @@ class SystemConfigOAuthDto {
other.mobileOverrideEnabled == mobileOverrideEnabled &&
other.mobileRedirectUri == mobileRedirectUri &&
other.profileSigningAlgorithm == profileSigningAlgorithm &&
other.roleClaim == roleClaim &&
other.scope == scope &&
other.signingAlgorithm == signingAlgorithm &&
other.storageLabelClaim == storageLabelClaim &&
@@ -102,6 +106,7 @@ class SystemConfigOAuthDto {
(mobileOverrideEnabled.hashCode) +
(mobileRedirectUri.hashCode) +
(profileSigningAlgorithm.hashCode) +
(roleClaim.hashCode) +
(scope.hashCode) +
(signingAlgorithm.hashCode) +
(storageLabelClaim.hashCode) +
@@ -110,7 +115,7 @@ class SystemConfigOAuthDto {
(tokenEndpointAuthMethod.hashCode);
@override
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -129,6 +134,7 @@ class SystemConfigOAuthDto {
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
json[r'roleClaim'] = this.roleClaim;
json[r'scope'] = this.scope;
json[r'signingAlgorithm'] = this.signingAlgorithm;
json[r'storageLabelClaim'] = this.storageLabelClaim;
@@ -158,6 +164,7 @@ class SystemConfigOAuthDto {
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
roleClaim: mapValueOfType<String>(json, r'roleClaim')!,
scope: mapValueOfType<String>(json, r'scope')!,
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
@@ -222,6 +229,7 @@ class SystemConfigOAuthDto {
'mobileOverrideEnabled',
'mobileRedirectUri',
'profileSigningAlgorithm',
'roleClaim',
'scope',
'signingAlgorithm',
'storageLabelClaim',

View File

@@ -89,6 +89,18 @@ void main() {
.thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()))
.thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.updateStacksV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
).thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.deleteStacksV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
).thenAnswer(successHandler);
sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo,