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

feat: sliver appbar and snap scrubbing (#19446)

This commit is contained in:
Alex
2025-06-24 20:02:46 -05:00
committed by GitHub
parent 522cdbac99
commit 05064f87f0
9 changed files with 780 additions and 58 deletions

View File

@ -13,6 +13,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
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';
class Timeline extends StatelessWidget {
const Timeline({super.key});
@ -63,38 +65,68 @@ class _SliverTimelineState extends State<_SliverTimeline> {
final asyncSegments = ref.watch(timelineSegmentProvider);
final maxHeight =
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
final isMultiSelectEnabled =
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
return asyncSegments.widgetWhen(
onData: (segments) {
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
final statusBarHeight = context.padding.top;
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
const scrubberBottomPadding = 100.0;
return PrimaryScrollController(
controller: _scrollController,
child: Scrubber(
layoutSegments: segments,
timelineHeight: maxHeight,
topPadding: context.padding.top + 10,
bottomPadding: context.padding.bottom + 10,
child: CustomScrollView(
primary: true,
cacheExtent: maxHeight * 2,
slivers: [
_SliverSegmentedList(
segments: segments,
delegate: SliverChildBuilderDelegate(
(ctx, index) {
if (index >= childCount) return null;
final segment = segments.findByIndex(index);
return segment?.builder(ctx, index) ??
const SizedBox.shrink();
},
childCount: childCount,
addAutomaticKeepAlives: false,
// We add repaint boundary around tiles, so skip the auto boundaries
addRepaintBoundaries: false,
),
child: Stack(
children: [
Scrubber(
layoutSegments: segments,
timelineHeight: maxHeight,
topPadding: totalAppBarHeight + 10,
bottomPadding:
context.padding.bottom + scrubberBottomPadding,
child: CustomScrollView(
primary: true,
cacheExtent: maxHeight * 2,
slivers: [
SliverAnimatedOpacity(
duration: Durations.medium1,
opacity: isMultiSelectEnabled ? 0 : 1,
sliver: const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
),
_SliverSegmentedList(
segments: segments,
delegate: SliverChildBuilderDelegate(
(ctx, index) {
if (index >= childCount) return null;
final segment = segments.findByIndex(index);
return segment?.builder(ctx, index) ??
const SizedBox.shrink();
},
childCount: childCount,
addAutomaticKeepAlives: false,
// We add repaint boundary around tiles, so skip the auto boundaries
addRepaintBoundaries: false,
),
),
const SliverPadding(
padding: EdgeInsets.only(
bottom: scrubberBottomPadding,
),
),
],
),
],
),
),
if (isMultiSelectEnabled)
const Positioned(
top: 60,
left: 25,
child: _MultiSelectStatusButton(),
),
],
),
);
},
@ -363,3 +395,27 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
childManager.didFinishLayout();
}
}
class _MultiSelectStatusButton extends ConsumerWidget {
const _MultiSelectStatusButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectCount =
ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
return ElevatedButton.icon(
onPressed: () => ref.read(multiSelectProvider.notifier).clearSelection(),
icon: Icon(
Icons.close_rounded,
color: context.colorScheme.onPrimary,
),
label: Text(
selectCount.toString(),
style: context.textTheme.titleMedium?.copyWith(
height: 2.5,
color: context.colorScheme.onPrimary,
),
),
);
}
}