fix timeline not updated on orientation change

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