1
0
mirror of https://github.com/immich-app/immich.git synced 2025-08-07 23:03:36 +02:00

feat: expanded sliver app bar (#19827)

* use mutex

* feat: cool app bar

* animation

* adapt to more pages

* animation

* better animation

* fix: asset count

* Revert "fix: asset count"

This reverts commit 673a5b264b.

* fix: asset count

* fix: shaky animation on Android

* tunning

* offset SizedBox to fix scroll jump on multiselect

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
Alex
2025-07-10 10:13:46 -05:00
committed by GitHub
parent 977d6452f6
commit feff1899ee
21 changed files with 733 additions and 75 deletions

View File

@ -1,9 +1,11 @@
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';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage()
class DriftArchivePage extends StatelessWidget {
@ -27,7 +29,12 @@ class DriftArchivePage extends StatelessWidget {
},
),
],
child: const Timeline(),
child: Timeline(
appBar: MesmerizingSliverAppBar(
title: 'archive'.t(context: context),
icon: Icons.archive_outlined,
),
),
);
}
}

View File

@ -1,9 +1,11 @@
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';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage()
class DriftFavoritePage extends StatelessWidget {
@ -27,7 +29,12 @@ class DriftFavoritePage extends StatelessWidget {
},
),
],
child: const Timeline(),
child: Timeline(
appBar: MesmerizingSliverAppBar(
title: 'favorites'.t(context: context),
icon: Icons.favorite_outline,
),
),
);
}
}

View File

@ -103,7 +103,7 @@ class _AlbumList extends ConsumerWidget {
),
),
onTap: () =>
context.pushRoute(LocalTimelineRoute(albumId: album.id)),
context.pushRoute(LocalTimelineRoute(album: album)),
),
);
},

View File

@ -1,6 +1,7 @@
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';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@ -27,7 +28,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,
),
),
);
}
}

View File

@ -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),
),
);
}
}

View File

@ -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),
),
);
},

View File

@ -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,
),
),
);
}
}

View File

@ -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,

View File

@ -27,7 +27,12 @@ class DriftLibraryPage extends ConsumerWidget {
return const Scaffold(
body: CustomScrollView(
slivers: [
ImmichSliverAppBar(),
ImmichSliverAppBar(
snap: false,
floating: false,
pinned: true,
showUploadButton: false,
),
_ActionButtonGrid(),
_CollectionCards(),
_QuickAccessButtonList(),

View File

@ -335,7 +335,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
final isDraggingDown = currentExtent < previousExtent;
previousExtent = currentExtent;
// Closes the bottom sheet if the user is dragging down
if (isDraggingDown && delta.extent < 0.5) {
if (isDraggingDown && delta.extent < 0.55) {
if (dragInProgress) {
blockGestures = true;
}

View File

@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
class Timeline extends StatelessWidget {
@ -25,10 +26,12 @@ class Timeline extends StatelessWidget {
super.key,
this.topSliverWidget,
this.topSliverWidgetHeight,
this.appBar,
});
final Widget? topSliverWidget;
final double? topSliverWidgetHeight;
final Widget? appBar;
@override
Widget build(BuildContext context) {
@ -49,6 +52,7 @@ class Timeline extends StatelessWidget {
child: _SliverTimeline(
topSliverWidget: topSliverWidget,
topSliverWidgetHeight: topSliverWidgetHeight,
appBar: appBar,
),
),
),
@ -60,10 +64,12 @@ class _SliverTimeline extends ConsumerStatefulWidget {
const _SliverTimeline({
this.topSliverWidget,
this.topSliverWidgetHeight,
this.appBar,
});
final Widget? topSliverWidget;
final double? topSliverWidgetHeight;
final Widget? appBar;
@override
ConsumerState createState() => _SliverTimelineState();
@ -100,6 +106,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
onData: (segments) {
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
final statusBarHeight = context.padding.top;
final double appBarExpandedHeight =
widget.appBar != null && widget.appBar is MesmerizingSliverAppBar
? 200
: 0;
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
const scrubberBottomPadding = 100.0;
@ -112,7 +122,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
timelineHeight: maxHeight,
topPadding: totalAppBarHeight + 10,
bottomPadding: context.padding.bottom + scrubberBottomPadding,
monthSegmentSnappingOffset: widget.topSliverWidgetHeight,
monthSegmentSnappingOffset:
widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
child: CustomScrollView(
primary: true,
cacheExtent: maxHeight * 2,
@ -120,11 +131,12 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
if (isSelectionMode)
const SelectionSliverAppBar()
else
const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
widget.appBar ??
const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
if (widget.topSliverWidget != null) widget.topSliverWidget!,
_SliverSegmentedList(
segments: segments,