refactor: reduce timeline rebuilds (#19704)

* reduce timeline rebuilds

* feat: adds bottom sheet map and actions (#19692)

* adds bottom sheet map and actions

* PR feedbacks

* only reload the asset viewer if asset is changed

* styling tweak

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

* rename singleton and remove event prefix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2025-07-04 21:00:34 +05:30 committed by GitHub
parent b00d44a00c
commit 181efb9010
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 557 additions and 233 deletions

View file

@ -9,11 +9,13 @@ import 'package:url_launcher/url_launcher.dart';
class ExifMap extends StatelessWidget {
final ExifInfo exifInfo;
final String? markerId;
final MapCreatedCallback? onMapCreated;
const ExifMap({
super.key,
required this.exifInfo,
this.markerId = 'marker',
this.onMapCreated,
});
@override
@ -82,6 +84,7 @@ class ExifMap extends StatelessWidget {
debugPrint('Opening Map Uri: $uri');
launchUrl(uri);
},
onCreated: onMapCreated,
);
},
);

View file

@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
@ -39,64 +40,70 @@ class ImmichSliverAppBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final isMultiSelectEnabled =
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
return SliverAppBar(
floating: floating,
pinned: pinned,
snap: snap,
expandedHeight: expandedHeight,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
automaticallyImplyLeading: false,
centerTitle: false,
title: title ?? const _ImmichLogoWithText(),
actions: [
if (actions != null)
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(right: 16),
child: action,
),
),
IconButton(
icon: const Icon(Icons.swipe_left_alt_rounded),
onPressed: () => context.pop(),
),
IconButton(
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
icon: const Icon(
Icons.sync,
return SliverAnimatedOpacity(
duration: Durations.medium1,
opacity: isMultiSelectEnabled ? 0 : 1,
sliver: SliverAppBar(
floating: floating,
pinned: pinned,
snap: snap,
expandedHeight: expandedHeight,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
if (isCasting)
Padding(
padding: const EdgeInsets.only(right: 12),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
},
icon: Icon(
isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
automaticallyImplyLeading: false,
centerTitle: false,
title: title ?? const _ImmichLogoWithText(),
actions: [
if (actions != null)
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(right: 16),
child: action,
),
),
IconButton(
icon: const Icon(Icons.swipe_left_alt_rounded),
onPressed: () => context.pop(),
),
if (showUploadButton)
IconButton(
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
icon: const Icon(
Icons.sync,
),
),
if (isCasting)
Padding(
padding: const EdgeInsets.only(right: 12),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
},
icon: Icon(
isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
),
),
),
if (showUploadButton)
const Padding(
padding: EdgeInsets.only(right: 20),
child: _BackupIndicator(),
),
const Padding(
padding: EdgeInsets.only(right: 20),
child: _BackupIndicator(),
child: _ProfileIndicator(),
),
const Padding(
padding: EdgeInsets.only(right: 20),
child: _ProfileIndicator(),
),
],
],
),
);
}
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
@ -24,6 +25,7 @@ class MapThumbnail extends HookConsumerWidget {
final double width;
final ThemeMode? themeMode;
final bool showAttribution;
final MapCreatedCallback? onCreated;
const MapThumbnail({
super.key,
@ -36,16 +38,19 @@ class MapThumbnail extends HookConsumerWidget {
this.showMarkerPin = false,
this.themeMode,
this.showAttribution = true,
this.onCreated,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
final controller = useRef<MapLibreMapController?>(null);
final styleLoaded = useState(false);
final position = useValueNotifier<Point<num>?>(null);
Future<void> onMapCreated(MapLibreMapController mapController) async {
controller.value = mapController;
styleLoaded.value = false;
if (assetMarkerRemoteId != null) {
// The iOS impl returns wrong toScreenLocation without the delay
Future.delayed(
@ -54,17 +59,26 @@ class MapThumbnail extends HookConsumerWidget {
position.value = await mapController.toScreenLocation(centre),
);
}
onCreated?.call(mapController);
}
Future<void> onStyleLoaded() async {
if (showMarkerPin && controller.value != null) {
await controller.value?.addMarkerAtLatLng(centre);
}
styleLoaded.value = true;
}
return MapThemeOverride(
themeMode: themeMode,
mapBuilder: (style) => SizedBox(
mapBuilder: (style) => AnimatedContainer(
duration: Durations.medium2,
curve: Curves.easeOut,
foregroundDecoration: BoxDecoration(
color: context.colorScheme.inverseSurface
.withAlpha(styleLoaded.value ? 0 : 200),
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
height: height,
width: width,
child: ClipRRect(

View file

@ -660,7 +660,7 @@ typedef PhotoViewImageTapDownCallback = Function(
typedef PhotoViewImageDragStartCallback = Function(
BuildContext context,
DragStartDetails details,
PhotoViewControllerValue controllerValue,
PhotoViewControllerBase controllerValue,
PhotoViewScaleStateController scaleStateController,
);

View file

@ -271,7 +271,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
final PhotoView photoView = isCustomChild
? PhotoView.customChild(
key: ObjectKey(index),
key: pageOption.key ?? ObjectKey(index),
childSize: pageOption.childSize,
backgroundDecoration: widget.backgroundDecoration,
wantKeepAlive: widget.wantKeepAlive,
@ -304,7 +304,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
child: pageOption.child,
)
: PhotoView(
key: ObjectKey(index),
key: pageOption.key ?? ObjectKey(index),
index: index,
imageProvider: pageOption.imageProvider,
loadingBuilder: widget.loadingBuilder,
@ -363,7 +363,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
///
class PhotoViewGalleryPageOptions {
PhotoViewGalleryPageOptions({
Key? key,
this.key,
required this.imageProvider,
this.heroAttributes,
this.semanticLabel,
@ -392,6 +392,7 @@ class PhotoViewGalleryPageOptions {
assert(imageProvider != null);
const PhotoViewGalleryPageOptions.customChild({
this.key,
required this.child,
this.childSize,
this.semanticLabel,
@ -418,6 +419,8 @@ class PhotoViewGalleryPageOptions {
}) : errorBuilder = null,
imageProvider = null;
final Key? key;
/// Mirror to [PhotoView.imageProvider]
final ImageProvider? imageProvider;

View file

@ -416,7 +416,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
? (details) => widget.onDragStart!(
context,
details,
widget.controller.value,
widget.controller,
widget.scaleStateController,
)
: null,