mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
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:
parent
b00d44a00c
commit
181efb9010
29 changed files with 557 additions and 233 deletions
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -660,7 +660,7 @@ typedef PhotoViewImageTapDownCallback = Function(
|
|||
typedef PhotoViewImageDragStartCallback = Function(
|
||||
BuildContext context,
|
||||
DragStartDetails details,
|
||||
PhotoViewControllerValue controllerValue,
|
||||
PhotoViewControllerBase controllerValue,
|
||||
PhotoViewScaleStateController scaleStateController,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||
? (details) => widget.onDragStart!(
|
||||
context,
|
||||
details,
|
||||
widget.controller.value,
|
||||
widget.controller,
|
||||
widget.scaleStateController,
|
||||
)
|
||||
: null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue