mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix timeline not updated on orientation change
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user