mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
adapt to more pages
This commit is contained in:
parent
8987b2de17
commit
596a3bd689
@ -11,7 +11,7 @@ enum AlbumUserRole {
|
||||
}
|
||||
|
||||
// Model for an album stored in the server
|
||||
class Album {
|
||||
class RemoteAlbum {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ownerId;
|
||||
@ -24,7 +24,7 @@ class Album {
|
||||
final int assetCount;
|
||||
final String ownerName;
|
||||
|
||||
const Album({
|
||||
const RemoteAlbum({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ownerId,
|
||||
@ -57,7 +57,7 @@ class Album {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! Album) return false;
|
||||
if (other is! RemoteAlbum) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return id == other.id &&
|
||||
name == other.name &&
|
||||
|
@ -8,26 +8,26 @@ class RemoteAlbumService {
|
||||
|
||||
const RemoteAlbumService(this._repository);
|
||||
|
||||
Future<List<Album>> getAll() {
|
||||
Future<List<RemoteAlbum>> getAll() {
|
||||
return _repository.getAll();
|
||||
}
|
||||
|
||||
List<Album> sortAlbums(
|
||||
List<Album> albums,
|
||||
List<RemoteAlbum> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
RemoteAlbumSortMode sortMode, {
|
||||
bool isReverse = false,
|
||||
}) {
|
||||
return sortMode.sortFn(albums, isReverse);
|
||||
}
|
||||
|
||||
List<Album> searchAlbums(
|
||||
List<Album> albums,
|
||||
List<RemoteAlbum> searchAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
String query,
|
||||
String? userId, [
|
||||
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||
]) {
|
||||
final lowerQuery = query.toLowerCase();
|
||||
List<Album> filtered = albums;
|
||||
List<RemoteAlbum> filtered = albums;
|
||||
|
||||
// Apply text search filter
|
||||
if (query.isNotEmpty) {
|
||||
|
@ -9,57 +9,8 @@ class BackgroundSyncManager {
|
||||
Cancelable<void>? _deviceAlbumSyncTask;
|
||||
Cancelable<void>? _hashTask;
|
||||
|
||||
Completer<void>? _localSyncMutex;
|
||||
Completer<void>? _remoteSyncMutex;
|
||||
Completer<void>? _hashMutex;
|
||||
|
||||
BackgroundSyncManager();
|
||||
|
||||
Future<T> _withMutex<T>(
|
||||
Completer<void>? Function() getMutex,
|
||||
void Function(Completer<void>?) setMutex,
|
||||
Future<T> Function() operation,
|
||||
) async {
|
||||
while (getMutex() != null) {
|
||||
await getMutex()!.future;
|
||||
}
|
||||
|
||||
final mutex = Completer<void>();
|
||||
setMutex(mutex);
|
||||
|
||||
try {
|
||||
final result = await operation();
|
||||
return result;
|
||||
} finally {
|
||||
setMutex(null);
|
||||
mutex.complete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _withLocalSyncMutex<T>(Future<T> Function() operation) {
|
||||
return _withMutex(
|
||||
() => _localSyncMutex,
|
||||
(mutex) => _localSyncMutex = mutex,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> _withRemoteSyncMutex<T>(Future<T> Function() operation) {
|
||||
return _withMutex(
|
||||
() => _remoteSyncMutex,
|
||||
(mutex) => _remoteSyncMutex = mutex,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> _withHashMutex<T>(Future<T> Function() operation) {
|
||||
return _withMutex(
|
||||
() => _hashMutex,
|
||||
(mutex) => _hashMutex = mutex,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancel() {
|
||||
final futures = <Future>[];
|
||||
|
||||
@ -74,57 +25,51 @@ class BackgroundSyncManager {
|
||||
|
||||
// No need to cancel the task, as it can also be run when the user logs out
|
||||
Future<void> syncLocal({bool full = false}) {
|
||||
return _withLocalSyncMutex(() async {
|
||||
if (_deviceAlbumSyncTask != null) {
|
||||
return _deviceAlbumSyncTask!.future;
|
||||
}
|
||||
if (_deviceAlbumSyncTask != null) {
|
||||
return _deviceAlbumSyncTask!.future;
|
||||
}
|
||||
|
||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||
// captured by the closure passed to [runInIsolateGentle].
|
||||
_deviceAlbumSyncTask = full
|
||||
? runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: true),
|
||||
)
|
||||
: runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: false),
|
||||
);
|
||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||
// captured by the closure passed to [runInIsolateGentle].
|
||||
_deviceAlbumSyncTask = full
|
||||
? runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: true),
|
||||
)
|
||||
: runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: false),
|
||||
);
|
||||
|
||||
return _deviceAlbumSyncTask!.whenComplete(() {
|
||||
_deviceAlbumSyncTask = null;
|
||||
});
|
||||
return _deviceAlbumSyncTask!.whenComplete(() {
|
||||
_deviceAlbumSyncTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
// No need to cancel the task, as it can also be run when the user logs out
|
||||
Future<void> hashAssets() {
|
||||
return _withHashMutex(() async {
|
||||
if (_hashTask != null) {
|
||||
return _hashTask!.future;
|
||||
}
|
||||
if (_hashTask != null) {
|
||||
return _hashTask!.future;
|
||||
}
|
||||
|
||||
_hashTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||
);
|
||||
return _hashTask!.whenComplete(() {
|
||||
_hashTask = null;
|
||||
});
|
||||
_hashTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||
);
|
||||
return _hashTask!.whenComplete(() {
|
||||
_hashTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncRemote() {
|
||||
return _withRemoteSyncMutex(() async {
|
||||
if (_syncTask != null) {
|
||||
return _syncTask!.future;
|
||||
}
|
||||
if (_syncTask != null) {
|
||||
return _syncTask!.future;
|
||||
}
|
||||
|
||||
_syncTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||
);
|
||||
return _syncTask!.whenComplete(() {
|
||||
_syncTask = null;
|
||||
});
|
||||
_syncTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||
);
|
||||
return _syncTask!.whenComplete(() {
|
||||
_syncTask = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
||||
Future<List<RemoteAlbum>> getAll(
|
||||
{Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
||||
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
||||
|
||||
final query = _db.remoteAlbumEntity.select().join([
|
||||
@ -59,8 +60,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
extension on RemoteAlbumEntityData {
|
||||
Album toDto({int assetCount = 0, required String ownerName}) {
|
||||
return Album(
|
||||
RemoteAlbum toDto({int assetCount = 0, required String ownerName}) {
|
||||
return RemoteAlbum(
|
||||
id: id,
|
||||
name: name,
|
||||
ownerId: ownerId,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
@ -32,6 +32,7 @@ class DriftArchivePage extends StatelessWidget {
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(
|
||||
title: 'archive'.t(context: context),
|
||||
icon: Icons.archive_outlined, // Icon for the archive page
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -32,6 +32,7 @@ class DriftFavoritePage extends StatelessWidget {
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(
|
||||
title: 'favorites'.t(context: context),
|
||||
icon: Icons.favorite_outline,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -103,7 +103,7 @@ class _AlbumList extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
onTap: () =>
|
||||
context.pushRoute(LocalTimelineRoute(albumId: album.id)),
|
||||
context.pushRoute(LocalTimelineRoute(album: album)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.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';
|
||||
@ -27,7 +29,16 @@ class DriftTrashPage extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
child: const Timeline(),
|
||||
child: Timeline(
|
||||
appBar: SliverAppBar(
|
||||
title: Text('trash'.t(context: context)),
|
||||
floating: true,
|
||||
snap: true,
|
||||
pinned: true,
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LocalTimelinePage extends StatelessWidget {
|
||||
final String albumId;
|
||||
final LocalAlbum album;
|
||||
|
||||
const LocalTimelinePage({super.key, required this.albumId});
|
||||
const LocalTimelinePage({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -16,14 +18,17 @@ class LocalTimelinePage extends StatelessWidget {
|
||||
overrides: [
|
||||
timelineServiceProvider.overrideWith(
|
||||
(ref) {
|
||||
final timelineService =
|
||||
ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId);
|
||||
final timelineService = ref
|
||||
.watch(timelineFactoryProvider)
|
||||
.localAlbum(albumId: album.id);
|
||||
ref.onDispose(timelineService.dispose);
|
||||
return timelineService;
|
||||
},
|
||||
),
|
||||
],
|
||||
child: const Timeline(),
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
|
||||
name: album.name,
|
||||
countFuture: countFuture,
|
||||
onTap: () => context.router.push(
|
||||
LocalTimelineRoute(albumId: album.id),
|
||||
LocalTimelineRoute(album: album),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -226,7 +226,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
|
||||
name: album.name,
|
||||
countFuture: countFuture,
|
||||
onTap: () => context.router.push(
|
||||
RemoteTimelineRoute(albumId: album.id),
|
||||
RemoteTimelineRoute(album: album),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -1,14 +1,16 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||
|
||||
@RoutePage()
|
||||
class RemoteTimelinePage extends StatelessWidget {
|
||||
final String albumId;
|
||||
final RemoteAlbum album;
|
||||
|
||||
const RemoteTimelinePage({super.key, required this.albumId});
|
||||
const RemoteTimelinePage({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -18,13 +20,18 @@ class RemoteTimelinePage extends StatelessWidget {
|
||||
(ref) {
|
||||
final timelineService = ref
|
||||
.watch(timelineFactoryProvider)
|
||||
.remoteAlbum(albumId: albumId);
|
||||
.remoteAlbum(albumId: album.id);
|
||||
ref.onDispose(timelineService.dispose);
|
||||
return timelineService;
|
||||
},
|
||||
),
|
||||
],
|
||||
child: const Timeline(),
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(
|
||||
title: album.name,
|
||||
icon: Icons.photo_album_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -475,7 +475,7 @@ class _AlbumList extends StatelessWidget {
|
||||
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final List<Album> albums;
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
@ -555,7 +555,7 @@ class _AlbumList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () => context.router.push(
|
||||
RemoteTimelineRoute(albumId: album.id),
|
||||
RemoteTimelineRoute(album: album),
|
||||
),
|
||||
leadingPadding: const EdgeInsets.only(
|
||||
right: 16,
|
||||
@ -573,13 +573,24 @@ class _AlbumList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(
|
||||
: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline.withAlpha(50),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.photo_album_rounded,
|
||||
size: 24,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -599,7 +610,7 @@ class _AlbumGrid extends StatelessWidget {
|
||||
required this.error,
|
||||
});
|
||||
|
||||
final List<Album> albums;
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
@ -674,14 +685,14 @@ class _GridAlbumCard extends StatelessWidget {
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.router.push(
|
||||
RemoteTimelineRoute(albumId: album.id),
|
||||
RemoteTimelineRoute(album: album),
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
|
@ -8,21 +8,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'album.provider.dart';
|
||||
|
||||
class RemoteAlbumState {
|
||||
final List<Album> albums;
|
||||
final List<Album> filteredAlbums;
|
||||
final List<RemoteAlbum> albums;
|
||||
final List<RemoteAlbum> filteredAlbums;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const RemoteAlbumState({
|
||||
required this.albums,
|
||||
List<Album>? filteredAlbums,
|
||||
List<RemoteAlbum>? filteredAlbums,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
}) : filteredAlbums = filteredAlbums ?? albums;
|
||||
|
||||
RemoteAlbumState copyWith({
|
||||
List<Album>? albums,
|
||||
List<Album>? filteredAlbums,
|
||||
List<RemoteAlbum>? albums,
|
||||
List<RemoteAlbum>? filteredAlbums,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) {
|
||||
@ -66,7 +66,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
||||
}
|
||||
|
||||
Future<List<Album>> getAll() async {
|
||||
Future<List<RemoteAlbum>> getAll() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
|
@ -1,6 +1,8 @@
|
||||
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/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
|
@ -1211,11 +1211,11 @@ class LocalMediaSummaryRoute extends PageRouteInfo<void> {
|
||||
class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
|
||||
LocalTimelineRoute({
|
||||
Key? key,
|
||||
required String albumId,
|
||||
required LocalAlbum album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
LocalTimelineRoute.name,
|
||||
args: LocalTimelineRouteArgs(key: key, albumId: albumId),
|
||||
args: LocalTimelineRouteArgs(key: key, album: album),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@ -1225,21 +1225,21 @@ class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<LocalTimelineRouteArgs>();
|
||||
return LocalTimelinePage(key: args.key, albumId: args.albumId);
|
||||
return LocalTimelinePage(key: args.key, album: args.album);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class LocalTimelineRouteArgs {
|
||||
const LocalTimelineRouteArgs({this.key, required this.albumId});
|
||||
const LocalTimelineRouteArgs({this.key, required this.album});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String albumId;
|
||||
final LocalAlbum album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LocalTimelineRouteArgs{key: $key, albumId: $albumId}';
|
||||
return 'LocalTimelineRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1765,11 +1765,11 @@ class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
|
||||
class RemoteTimelineRoute extends PageRouteInfo<RemoteTimelineRouteArgs> {
|
||||
RemoteTimelineRoute({
|
||||
Key? key,
|
||||
required String albumId,
|
||||
required RemoteAlbum album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
RemoteTimelineRoute.name,
|
||||
args: RemoteTimelineRouteArgs(key: key, albumId: albumId),
|
||||
args: RemoteTimelineRouteArgs(key: key, album: album),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@ -1779,21 +1779,21 @@ class RemoteTimelineRoute extends PageRouteInfo<RemoteTimelineRouteArgs> {
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<RemoteTimelineRouteArgs>();
|
||||
return RemoteTimelinePage(key: args.key, albumId: args.albumId);
|
||||
return RemoteTimelinePage(key: args.key, album: args.album);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class RemoteTimelineRouteArgs {
|
||||
const RemoteTimelineRouteArgs({this.key, required this.albumId});
|
||||
const RemoteTimelineRouteArgs({this.key, required this.album});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String albumId;
|
||||
final RemoteAlbum album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RemoteTimelineRouteArgs{key: $key, albumId: $albumId}';
|
||||
return 'RemoteTimelineRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,38 +1,44 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
|
||||
typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
|
||||
typedef AlbumSortFn = List<RemoteAlbum> Function(
|
||||
List<RemoteAlbum> albums, bool isReverse);
|
||||
|
||||
class _RemoteAlbumSortHandlers {
|
||||
const _RemoteAlbumSortHandlers._();
|
||||
|
||||
static const AlbumSortFn created = _sortByCreated;
|
||||
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
|
||||
static List<RemoteAlbum> _sortByCreated(
|
||||
List<RemoteAlbum> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.createdAt);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn title = _sortByTitle;
|
||||
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
|
||||
static List<RemoteAlbum> _sortByTitle(
|
||||
List<RemoteAlbum> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.name);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn lastModified = _sortByLastModified;
|
||||
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
|
||||
static List<RemoteAlbum> _sortByLastModified(
|
||||
List<RemoteAlbum> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.updatedAt);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn assetCount = _sortByAssetCount;
|
||||
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
|
||||
static List<RemoteAlbum> _sortByAssetCount(
|
||||
List<RemoteAlbum> albums, bool isReverse) {
|
||||
final sorted =
|
||||
albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
static const AlbumSortFn mostRecent = _sortByMostRecent;
|
||||
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
|
||||
static List<RemoteAlbum> _sortByMostRecent(
|
||||
List<RemoteAlbum> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) {
|
||||
// For most recent, we sort by updatedAt in descending order
|
||||
return b.updatedAt.compareTo(a.updatedAt);
|
||||
@ -41,7 +47,8 @@ class _RemoteAlbumSortHandlers {
|
||||
}
|
||||
|
||||
static const AlbumSortFn mostOldest = _sortByMostOldest;
|
||||
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
|
||||
static List<RemoteAlbum> _sortByMostOldest(
|
||||
List<RemoteAlbum> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) {
|
||||
// For oldest, we sort by createdAt in ascending order
|
||||
return a.createdAt.compareTo(b.createdAt);
|
||||
|
@ -12,9 +12,11 @@ class MesmerizingSliverAppBar extends ConsumerWidget {
|
||||
const MesmerizingSliverAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.icon = Icons.camera,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
|
||||
double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) {
|
||||
if (settings?.maxExtent == null || settings?.minExtent == null) {
|
||||
@ -70,6 +72,7 @@ class MesmerizingSliverAppBar extends ConsumerWidget {
|
||||
assetCount: assetCount,
|
||||
scrollProgress: scrollProgress,
|
||||
title: title,
|
||||
icon: icon,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -83,11 +86,13 @@ class _ExpandedBackground extends ConsumerWidget {
|
||||
final int assetCount;
|
||||
final double scrollProgress;
|
||||
final String title;
|
||||
final IconData icon;
|
||||
|
||||
const _ExpandedBackground({
|
||||
required this.assetCount,
|
||||
required this.scrollProgress,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -101,7 +106,10 @@ class _ExpandedBackground extends ConsumerWidget {
|
||||
offset: Offset(0, scrollProgress * 50),
|
||||
child: Transform.scale(
|
||||
scale: 1.4 - (scrollProgress * 0.2),
|
||||
child: _RandomAssetBackground(timelineService: timelineService),
|
||||
child: _RandomAssetBackground(
|
||||
timelineService: timelineService,
|
||||
icon: icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@ -175,8 +183,12 @@ class _ExpandedBackground extends ConsumerWidget {
|
||||
|
||||
class _RandomAssetBackground extends StatefulWidget {
|
||||
final TimelineService timelineService;
|
||||
final IconData icon;
|
||||
|
||||
const _RandomAssetBackground({required this.timelineService});
|
||||
const _RandomAssetBackground({
|
||||
required this.timelineService,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
|
||||
@ -192,6 +204,18 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
BaseAsset? _currentAsset;
|
||||
BaseAsset? _nextAsset;
|
||||
|
||||
final LinearGradient gradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.pink.shade300.withValues(alpha: 0.9),
|
||||
Colors.purple.shade400.withValues(alpha: 0.8),
|
||||
Colors.indigo.shade400.withValues(alpha: 0.9),
|
||||
Colors.blue.shade500.withValues(alpha: 0.8),
|
||||
],
|
||||
stops: const [0.0, 0.3, 0.7, 1.0],
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -237,7 +261,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
);
|
||||
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
Durations.medium1,
|
||||
() => _loadRandomAsset(),
|
||||
);
|
||||
}
|
||||
@ -275,6 +299,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
: 0;
|
||||
|
||||
final assets = widget.timelineService.getAssets(randomIndex, 1);
|
||||
|
||||
if (assets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@ -330,119 +355,14 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: context.isDarkTheme
|
||||
? [
|
||||
Colors.deepPurple.withValues(alpha: 0.8),
|
||||
Colors.indigo.withValues(alpha: 0.9),
|
||||
Colors.purple.withValues(alpha: 0.8),
|
||||
Colors.pink.withValues(alpha: 0.7),
|
||||
]
|
||||
: [
|
||||
Colors.pink.shade300.withValues(alpha: 0.9),
|
||||
Colors.purple.shade400.withValues(alpha: 0.8),
|
||||
Colors.indigo.shade400.withValues(alpha: 0.9),
|
||||
Colors.blue.shade500.withValues(alpha: 0.8),
|
||||
],
|
||||
stops: const [0.0, 0.3, 0.7, 1.0],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 30,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.1)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 50,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.white.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 120,
|
||||
left: 20,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.06)
|
||||
: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Heart icon for empty favorites
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.favorite_outline,
|
||||
size: 100,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.15)
|
||||
: Colors.white.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return _EmptyPageExtendedBackground(
|
||||
gradient: gradient,
|
||||
icon: widget.icon,
|
||||
);
|
||||
}
|
||||
|
||||
if (_currentAsset == null) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: context.isDarkTheme
|
||||
? [
|
||||
Colors.deepPurple.withValues(alpha: 0.4),
|
||||
Colors.indigo.withValues(alpha: 0.5),
|
||||
Colors.purple.withValues(alpha: 0.4),
|
||||
]
|
||||
: [
|
||||
Colors.blue.shade200.withValues(alpha: 0.6),
|
||||
Colors.purple.shade300.withValues(alpha: 0.5),
|
||||
Colors.indigo.shade300.withValues(alpha: 0.6),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
@ -451,7 +371,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
_panAnimation.value.dx * 100, // Convert to pixel offset
|
||||
_panAnimation.value.dx * 100,
|
||||
_panAnimation.value.dy * 100,
|
||||
),
|
||||
child: Transform.scale(
|
||||
@ -469,47 +389,14 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
// Show a subtle loading state while the full image loads
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: context.isDarkTheme
|
||||
? [
|
||||
Colors.deepPurple.withValues(alpha: 0.3),
|
||||
Colors.indigo.withValues(alpha: 0.4),
|
||||
Colors.purple.withValues(alpha: 0.3),
|
||||
]
|
||||
: [
|
||||
Colors.blue.shade200.withValues(alpha: 0.5),
|
||||
Colors.purple.shade300.withValues(alpha: 0.4),
|
||||
Colors.indigo.shade300.withValues(alpha: 0.5),
|
||||
],
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(gradient: gradient),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// Fallback to a gradient if image fails to load
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: context.isDarkTheme
|
||||
? [
|
||||
Colors.deepPurple.withValues(alpha: 0.6),
|
||||
Colors.indigo.withValues(alpha: 0.7),
|
||||
Colors.purple.withValues(alpha: 0.6),
|
||||
]
|
||||
: [
|
||||
Colors.blue.shade300.withValues(alpha: 0.7),
|
||||
Colors.purple.shade400.withValues(alpha: 0.6),
|
||||
Colors.indigo.shade400.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(gradient: gradient),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -521,3 +408,75 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyPageExtendedBackground extends StatelessWidget {
|
||||
const _EmptyPageExtendedBackground({
|
||||
required this.gradient,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final LinearGradient gradient;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(gradient: gradient),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 30,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.1)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 50,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.white.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 120,
|
||||
left: 20,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.06)
|
||||
: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 100,
|
||||
color: context.isDarkTheme
|
||||
? Colors.white.withValues(alpha: 0.15)
|
||||
: Colors.white.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user