diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 37b48a2891..3f3d2f6bd7 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -55,18 +55,10 @@ void main() async { await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); await migrateDatabaseIfNeeded(); - runApp( - ProviderScope( - overrides: [driftProvider.overrideWith(driftOverride(drift))], - // Never retry any provider - child: const MainWidget(), - ), - ); + runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); } catch (error, stack) { runApp( ProviderScope( - // Never retry any provider - retry: (retryCount, error) => null, child: BootstrapErrorWidget(error: error.toString(), stack: stack.toString()), ), ); diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index c68a7273e0..81a5ab2297 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -57,6 +57,12 @@ class _AlbumSelectorState extends ConsumerState { void initState() { super.initState(); + ref.listenManual( + remoteAlbumProvider.select((state) => state.albums), + (_, _) => sortAlbums(), + fireImmediately: true, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { final appSettings = ref.read(appSettingsServiceProvider); final savedSortMode = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortOrder); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index 1e1d4130f7..78eeebd1fe 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; @@ -48,6 +49,26 @@ class TimelineArgs { showStorageIndicator.hashCode ^ withStack.hashCode ^ groupBy.hashCode; + + TimelineArgs copyWith({ + double? maxWidth, + double? maxHeight, + double? spacing, + int? columnCount, + bool? showStorageIndicator, + bool? withStack, + GroupAssetsBy? groupBy, + }) { + return TimelineArgs( + maxWidth: maxWidth ?? this.maxWidth, + maxHeight: maxHeight ?? this.maxHeight, + spacing: spacing ?? this.spacing, + columnCount: columnCount ?? this.columnCount, + showStorageIndicator: showStorageIndicator ?? this.showStorageIndicator, + withStack: withStack ?? this.withStack, + groupBy: groupBy ?? this.groupBy, + ); + } } class TimelineState { @@ -86,25 +107,37 @@ class TimelineStateNotifier extends Notifier { // This provider watches the buckets from the timeline service & args and serves the segments. // It should be used only after the timeline service and timeline args provider is overridden -final timelineSegmentProvider = StreamProvider.autoDispose>((ref) async* { - final args = ref.watch(timelineArgsProvider); - final columnCount = args.columnCount; - final spacing = args.spacing; - final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1)); - final tileExtent = math.max(0, availableTileWidth) / columnCount; +final timelineSegmentProvider = StreamNotifierProvider<_TimelineSegmentNotifier, List>( + _TimelineSegmentNotifier.new, + dependencies: [timelineServiceProvider, timelineArgsProvider], +); - final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)]; +class _TimelineSegmentNotifier extends StreamNotifier> { + @override + Stream> build() async* { + final args = ref.watch(timelineArgsProvider); + final columnCount = args.columnCount; + final spacing = args.spacing; + final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1)); + final tileExtent = math.max(0, availableTileWidth) / columnCount; + final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)]; + final timelineService = ref.watch(timelineServiceProvider); + yield* timelineService.watchBuckets().map((buckets) { + return FixedSegmentBuilder( + buckets: buckets, + tileHeight: tileExtent, + columnCount: columnCount, + spacing: spacing, + groupBy: groupBy, + ).generate(); + }); + } - final timelineService = ref.watch(timelineServiceProvider); - yield* timelineService.watchBuckets().map((buckets) { - return FixedSegmentBuilder( - buckets: buckets, - tileHeight: tileExtent, - columnCount: columnCount, - spacing: spacing, - groupBy: groupBy, - ).generate(); - }); -}, dependencies: [timelineServiceProvider, timelineArgsProvider]); + @override + bool updateShouldNotify(AsyncValue> previous, AsyncValue> next) { + final listEquals = const DeepCollectionEquality().equals; + return !listEquals(previous.value, next.value); + } +} final timelineStateProvider = NotifierProvider(TimelineStateNotifier.new); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 578bd37a23..217cfcc9e6 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -71,10 +71,9 @@ class Timeline extends StatelessWidget { builder: (_, constraints) => ProviderScope( overrides: [ timelineArgsProvider.overrideWith( - (ref) => TimelineArgs( - maxWidth: constraints.maxWidth, - maxHeight: constraints.maxHeight, - columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), + () => TimelineArgsNotifier( + initialMaxWidth: constraints.maxWidth, + initialMaxHeight: constraints.maxHeight, showStorageIndicator: showStorageIndicator, withStack: withStack, groupBy: groupBy, @@ -92,6 +91,7 @@ class Timeline extends StatelessWidget { persistentBottomBar: persistentBottomBar, snapToMonth: snapToMonth, maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, loadingWidget: loadingWidget, ), ), @@ -122,6 +122,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { this.persistentBottomBar = false, this.snapToMonth = true, this.maxWidth, + this.maxHeight, this.loadingWidget, }); @@ -134,6 +135,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { final bool persistentBottomBar; final bool snapToMonth; final double? maxWidth; + final double? maxHeight; final Widget? loadingWidget; @override @@ -172,13 +174,21 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { @override void didUpdateWidget(covariant _SliverTimeline oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.maxWidth != oldWidget.maxWidth) { - final asyncSegments = ref.read(timelineSegmentProvider); - asyncSegments.whenData((segments) { - final index = _getCurrentAssetIndex(segments); - // Refresh to wait for new segments to be generated with the updated width before restoring the scroll position - final _ = ref.refresh(timelineArgsProvider); - _restoreAssetIndex = index; + if (widget.maxWidth != oldWidget.maxWidth || widget.maxHeight != oldWidget.maxHeight) { + final segments = ref.read(timelineSegmentProvider).value; + int? restoreAssetIndex; + if (segments != null) { + restoreAssetIndex = _getCurrentAssetIndex(segments); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + ref + .read(timelineArgsProvider.notifier) + .updateConstraints(maxWidth: widget.maxWidth!, maxHeight: widget.maxHeight!); + final _ = ref.refresh(timelineSegmentProvider); + _restoreAssetIndex = restoreAssetIndex; + _restoreAssetPosition(null); + } }); } } @@ -200,26 +210,40 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { } } + void _restorePosition(List segments) { + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); + if (targetSegment == null) { + _restoreAssetIndex = null; + return; + } + final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; + final newColumnCount = ref.read(timelineArgsProvider).columnCount; + final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); + final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; + final targetOffset = targetSegment.indexToLayoutOffset(targetRowIndex); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _scrollController.hasClients) { + _scrollController.jumpTo(targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent)); + } + }); + _restoreAssetIndex = null; + } + void _restoreAssetPosition(_) { if (_restoreAssetIndex == null) return; final asyncSegments = ref.read(timelineSegmentProvider); - asyncSegments.whenData((segments) { - final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); - if (targetSegment != null) { - final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; - final newColumnCount = ref.read(timelineArgsProvider).columnCount; - final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); - final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; - final targetOffset = targetSegment.indexToLayoutOffset(targetRowIndex); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _scrollController.jumpTo(targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent)); - } - }); + if (asyncSegments is AsyncData>) { + _restorePosition(asyncSegments.value); + return; + } + late ProviderSubscription>> sub; + sub = ref.listenManual>>(timelineSegmentProvider, (_, next) { + if (next is AsyncData>) { + sub.close(); + _restorePosition(next.value); } }); - _restoreAssetIndex = null; } void _onMultiSelectionToggled(_, bool isEnabled) { diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index d3df6ce697..a7e4224815 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -1,4 +1,7 @@ +import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; @@ -10,10 +13,48 @@ final timelineRepositoryProvider = Provider( (ref) => DriftTimelineRepository(ref.watch(driftProvider)), ); -final timelineArgsProvider = Provider.autoDispose( - (ref) => throw UnimplementedError('Will be overridden through a ProviderScope.'), +final timelineArgsProvider = NotifierProvider.autoDispose( + TimelineArgsNotifier.new, + dependencies: const [], ); +class TimelineArgsNotifier extends Notifier { + TimelineArgsNotifier({ + double initialMaxWidth = 0, + double initialMaxHeight = 0, + this.showStorageIndicator = false, + this.withStack = false, + this.groupBy, + }) : _maxWidth = initialMaxWidth, + _maxHeight = initialMaxHeight; + + double _maxWidth; + double _maxHeight; + final bool showStorageIndicator; + final bool withStack; + final GroupAssetsBy? groupBy; + + @override + TimelineArgs build() { + final columnCount = ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))); + return TimelineArgs( + maxWidth: _maxWidth, + maxHeight: _maxHeight, + columnCount: columnCount, + showStorageIndicator: showStorageIndicator, + withStack: withStack, + groupBy: groupBy, + ); + } + + void updateConstraints({required double maxWidth, required double maxHeight}) { + if (_maxWidth == maxWidth && _maxHeight == maxHeight) return; + _maxWidth = maxWidth; + _maxHeight = maxHeight; + state = state.copyWith(maxWidth: maxWidth, maxHeight: maxHeight); + } +} + final timelineServiceProvider = Provider( (ref) { final timelineUsers = ref.watch(timelineUsersProvider).value ?? []; @@ -33,11 +74,22 @@ final timelineFactoryProvider = Provider( ), ); -final timelineUsersProvider = StreamProvider>((ref) { - final currentUserId = ref.watch(currentUserProvider.select((u) => u?.id)); - if (currentUserId == null) { - return Stream.value([]); +final timelineUsersProvider = StreamNotifierProvider<_TimelineUsersNotifier, List>(_TimelineUsersNotifier.new); + +class _TimelineUsersNotifier extends StreamNotifier> { + @override + Stream> build() { + final currentUserId = ref.watch(currentUserProvider.select((u) => u?.id)); + if (currentUserId == null) { + return Stream.value([]); + } + + return ref.watch(timelineRepositoryProvider).watchTimelineUserIds(currentUserId); } - return ref.watch(timelineRepositoryProvider).watchTimelineUserIds(currentUserId); -}); + @override + bool updateShouldNotify(AsyncValue> previous, AsyncValue> next) { + final listEquals = const DeepCollectionEquality().equals; + return !listEquals(previous.value, next.value); + } +}