refactor(mobile): sqlite-based map view (#20665)

* feat(mobile): drift map page

* refactor: map query

* perf: do not filter markers

* fix: refresh timeline by key

* chore: rename

* remove ref listen and global key

* clean code

* remove locked and favorite

* temporary change for stress test

* optimizations

* fix bottom sheet

* cleaner bounds check

* cleanup

* feat: back button

---------

Co-authored-by: wuzihao051119 <wuzihao051119@outlook.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Mert 2025-08-06 11:05:49 -04:00 committed by GitHub
parent 1ca46fbd98
commit 0121043d7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 7573 additions and 34 deletions

View file

@ -69,7 +69,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
builder: (BuildContext context, ScrollController scrollController) {
return Card(
color: widget.backgroundColor ?? context.colorScheme.surfaceContainerHigh,
color: widget.backgroundColor ?? context.colorScheme.surface,
borderOnForeground: false,
clipBehavior: Clip.antiAlias,
elevation: 6.0,
@ -78,25 +78,21 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
const SizedBox(height: 10),
const _DragHandle(),
const SizedBox(height: 14),
if (widget.actions.isNotEmpty)
const SliverPersistentHeader(delegate: _DragHandleDelegate(), pinned: true),
if (widget.actions.isNotEmpty)
SliverToBoxAdapter(
child: Column(
children: [
SizedBox(
height: 115,
child: ListView(shrinkWrap: true, scrollDirection: Axis.horizontal, children: widget.actions),
),
if (widget.actions.isNotEmpty) ...[
const Divider(indent: 16, endIndent: 16),
const SizedBox(height: 16),
],
],
),
),
),
...(widget.slivers ?? []),
if (widget.slivers != null) ...widget.slivers!,
],
),
);
@ -105,17 +101,42 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
}
}
class _DragHandleDelegate extends SliverPersistentHeaderDelegate {
const _DragHandleDelegate();
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return const _DragHandle();
}
@override
bool shouldRebuild(_DragHandleDelegate oldDelegate) => false;
@override
double get minExtent => 50.0;
@override
double get maxExtent => 50.0;
}
class _DragHandle extends StatelessWidget {
const _DragHandle();
@override
Widget build(BuildContext context) {
return Container(
height: 6,
width: 32,
decoration: BoxDecoration(
color: context.themeData.dividerColor.lighten(amount: 0.6),
borderRadius: const BorderRadius.all(Radius.circular(20)),
return SizedBox(
height: 50,
child: Center(
child: SizedBox(
width: 32,
height: 6,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
color: context.themeData.dividerColor.lighten(amount: 0.6),
),
),
),
),
);
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class MapBottomSheet extends StatelessWidget {
const MapBottomSheet({super.key});
@override
Widget build(BuildContext context) {
return const BaseBottomSheet(
initialChildSize: 0.25,
maxChildSize: 0.9,
shouldCloseOnMinExtent: false,
resizeOnScroll: false,
actions: [],
slivers: [SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())],
);
}
}
class _ScopedMapTimeline extends StatelessWidget {
const _ScopedMapTimeline();
@override
Widget build(BuildContext context) {
// TODO: this causes the timeline to switch to flicker to "loading" state and back. This is both janky and inefficient.
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final bounds = ref.watch(mapStateProvider).bounds;
final timelineService = ref.watch(timelineFactoryProvider).map(bounds);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: const Timeline(appBar: null, bottomSheet: null),
);
}
}