mirror of
https://github.com/immich-app/immich.git
synced 2026-03-03 02:57:01 +00:00
fix: simplify timeline rebuild on orientation (#26408)
* revert: current fix # Conflicts: # mobile/lib/presentation/widgets/timeline/timeline.widget.dart * fix: simpler fix * rebase --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -29,38 +29,7 @@ import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
||||||
|
|
||||||
class _TimelineRestorationState extends ChangeNotifier {
|
class Timeline extends StatelessWidget {
|
||||||
int? _restoreAssetIndex;
|
|
||||||
bool _shouldRestoreAssetPosition = false;
|
|
||||||
|
|
||||||
int? get restoreAssetIndex => _restoreAssetIndex;
|
|
||||||
bool get shouldRestoreAssetPosition => _shouldRestoreAssetPosition;
|
|
||||||
|
|
||||||
void setRestoreAssetIndex(int? index) {
|
|
||||||
_restoreAssetIndex = index;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setShouldRestoreAssetPosition(bool should) {
|
|
||||||
_shouldRestoreAssetPosition = should;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearRestoreAssetIndex() {
|
|
||||||
_restoreAssetIndex = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineRestorationProvider extends InheritedNotifier<_TimelineRestorationState> {
|
|
||||||
const _TimelineRestorationProvider({required super.notifier, required super.child});
|
|
||||||
|
|
||||||
static _TimelineRestorationState of(BuildContext context) {
|
|
||||||
return context.dependOnInheritedWidgetOfExactType<_TimelineRestorationProvider>()!.notifier!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Timeline extends StatefulWidget {
|
|
||||||
const Timeline({
|
const Timeline({
|
||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
@@ -90,68 +59,38 @@ class Timeline extends StatefulWidget {
|
|||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
final bool persistentBottomBar;
|
final bool persistentBottomBar;
|
||||||
|
|
||||||
@override
|
|
||||||
State<Timeline> createState() => _TimelineState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineState extends State<Timeline> {
|
|
||||||
double? _lastWidth;
|
|
||||||
late final _TimelineRestorationState _restorationState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_restorationState = _TimelineRestorationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_restorationState.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (_, constraints) {
|
builder: (_, constraints) => ProviderScope(
|
||||||
if (_lastWidth != null && _lastWidth != constraints.maxWidth) {
|
overrides: [
|
||||||
_restorationState.setShouldRestoreAssetPosition(true);
|
timelineArgsProvider.overrideWith(
|
||||||
}
|
(ref) => TimelineArgs(
|
||||||
_lastWidth = constraints.maxWidth;
|
maxWidth: constraints.maxWidth,
|
||||||
return _TimelineRestorationProvider(
|
maxHeight: constraints.maxHeight,
|
||||||
notifier: _restorationState,
|
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
||||||
child: ProviderScope(
|
showStorageIndicator: showStorageIndicator,
|
||||||
key: ValueKey(_lastWidth),
|
withStack: withStack,
|
||||||
overrides: [
|
groupBy: groupBy,
|
||||||
timelineArgsProvider.overrideWith(
|
|
||||||
(ref) => TimelineArgs(
|
|
||||||
maxWidth: constraints.maxWidth,
|
|
||||||
maxHeight: constraints.maxHeight,
|
|
||||||
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
|
||||||
showStorageIndicator: widget.showStorageIndicator,
|
|
||||||
withStack: widget.withStack,
|
|
||||||
groupBy: widget.groupBy,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
|
||||||
],
|
|
||||||
child: _SliverTimeline(
|
|
||||||
key: const ValueKey('_sliver_timeline'),
|
|
||||||
topSliverWidget: widget.topSliverWidget,
|
|
||||||
topSliverWidgetHeight: widget.topSliverWidgetHeight,
|
|
||||||
appBar: widget.appBar,
|
|
||||||
bottomSheet: widget.bottomSheet,
|
|
||||||
withScrubber: widget.withScrubber,
|
|
||||||
persistentBottomBar: widget.persistentBottomBar,
|
|
||||||
snapToMonth: widget.snapToMonth,
|
|
||||||
initialScrollOffset: widget.initialScrollOffset,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
||||||
},
|
],
|
||||||
|
child: _SliverTimeline(
|
||||||
|
topSliverWidget: topSliverWidget,
|
||||||
|
topSliverWidgetHeight: topSliverWidgetHeight,
|
||||||
|
appBar: appBar,
|
||||||
|
bottomSheet: bottomSheet,
|
||||||
|
withScrubber: withScrubber,
|
||||||
|
persistentBottomBar: persistentBottomBar,
|
||||||
|
snapToMonth: snapToMonth,
|
||||||
|
initialScrollOffset: initialScrollOffset,
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -170,7 +109,6 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier {
|
|||||||
|
|
||||||
class _SliverTimeline extends ConsumerStatefulWidget {
|
class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
const _SliverTimeline({
|
const _SliverTimeline({
|
||||||
super.key,
|
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
this.appBar,
|
this.appBar,
|
||||||
@@ -179,6 +117,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
this.persistentBottomBar = false,
|
this.persistentBottomBar = false,
|
||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
|
this.maxWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
@@ -189,6 +128,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
final bool persistentBottomBar;
|
final bool persistentBottomBar;
|
||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
final double? initialScrollOffset;
|
final double? initialScrollOffset;
|
||||||
|
final double? maxWidth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
@@ -207,6 +147,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
int _perRow = 4;
|
int _perRow = 4;
|
||||||
double _scaleFactor = 3.0;
|
double _scaleFactor = 3.0;
|
||||||
double _baseScaleFactor = 3.0;
|
double _baseScaleFactor = 3.0;
|
||||||
|
int? _restoreAssetIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -225,6 +166,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ScrollToTopEvent():
|
case ScrollToTopEvent():
|
||||||
@@ -242,21 +197,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMultiSelectionToggled(_, bool isEnabled) {
|
|
||||||
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _restoreAssetPosition(_) {
|
void _restoreAssetPosition(_) {
|
||||||
final restorationState = _TimelineRestorationProvider.of(context);
|
if (_restoreAssetIndex == null) return;
|
||||||
if (!restorationState.shouldRestoreAssetPosition || restorationState.restoreAssetIndex == null) return;
|
|
||||||
|
|
||||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
asyncSegments.whenData((segments) {
|
asyncSegments.whenData((segments) {
|
||||||
final targetSegment = segments.lastWhereOrNull(
|
final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!);
|
||||||
(segment) => segment.firstAssetIndex <= restorationState.restoreAssetIndex!,
|
|
||||||
);
|
|
||||||
if (targetSegment != null) {
|
if (targetSegment != null) {
|
||||||
final assetIndexInSegment = restorationState.restoreAssetIndex! - targetSegment.firstAssetIndex;
|
final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex;
|
||||||
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
||||||
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
||||||
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
||||||
@@ -268,7 +216,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
restorationState.clearRestoreAssetIndex();
|
_restoreAssetIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMultiSelectionToggled(_, bool isEnabled) {
|
||||||
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _getCurrentAssetIndex(List<Segment> segments) {
|
int? _getCurrentAssetIndex(List<Segment> segments) {
|
||||||
@@ -478,67 +430,56 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: NotificationListener<ScrollEndNotification>(
|
child: RawGestureDetector(
|
||||||
onNotification: (notification) {
|
gestures: {
|
||||||
final currentIndex = _getCurrentAssetIndex(segments);
|
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
||||||
if (currentIndex != null && mounted) {
|
() => CustomScaleGestureRecognizer(),
|
||||||
_TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex);
|
(CustomScaleGestureRecognizer scale) {
|
||||||
}
|
scale.onStart = (details) {
|
||||||
return false;
|
_baseScaleFactor = _scaleFactor;
|
||||||
},
|
};
|
||||||
child: RawGestureDetector(
|
|
||||||
gestures: {
|
|
||||||
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
|
||||||
() => CustomScaleGestureRecognizer(),
|
|
||||||
(CustomScaleGestureRecognizer scale) {
|
|
||||||
scale.onStart = (details) {
|
|
||||||
_baseScaleFactor = _scaleFactor;
|
|
||||||
};
|
|
||||||
|
|
||||||
scale.onUpdate = (details) {
|
scale.onUpdate = (details) {
|
||||||
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
||||||
final newPerRow = 7 - newScaleFactor.toInt();
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
|
|
||||||
|
if (newPerRow != _perRow) {
|
||||||
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
||||||
|
setState(() {
|
||||||
|
_scaleFactor = newScaleFactor;
|
||||||
|
_perRow = newPerRow;
|
||||||
|
_restoreAssetIndex = targetAssetIndex;
|
||||||
|
});
|
||||||
|
|
||||||
if (newPerRow != _perRow) {
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
final restorationState = _TimelineRestorationProvider.of(context);
|
}
|
||||||
setState(() {
|
};
|
||||||
_scaleFactor = newScaleFactor;
|
|
||||||
_perRow = newPerRow;
|
|
||||||
});
|
|
||||||
|
|
||||||
restorationState.setRestoreAssetIndex(targetAssetIndex);
|
|
||||||
restorationState.setShouldRestoreAssetPosition(true);
|
|
||||||
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
child: TimelineDragRegion(
|
|
||||||
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
|
||||||
onAssetEnter: _handleDragAssetEnter,
|
|
||||||
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
|
||||||
onScroll: _dragScroll,
|
|
||||||
onScrollStart: () {
|
|
||||||
// Minimize the bottom sheet when drag selection starts
|
|
||||||
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
|
||||||
},
|
},
|
||||||
child: Stack(
|
),
|
||||||
children: [
|
},
|
||||||
timeline,
|
child: TimelineDragRegion(
|
||||||
if (isMultiSelectStatusVisible)
|
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
||||||
Positioned(
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
top: MediaQuery.paddingOf(context).top,
|
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
||||||
left: 25,
|
onScroll: _dragScroll,
|
||||||
child: const SizedBox(
|
onScrollStart: () {
|
||||||
height: kToolbarHeight,
|
// Minimize the bottom sheet when drag selection starts
|
||||||
child: Center(child: _MultiSelectStatusButton()),
|
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
||||||
),
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
timeline,
|
||||||
|
if (isBottomWidgetVisible)
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.paddingOf(context).top,
|
||||||
|
left: 25,
|
||||||
|
child: const SizedBox(
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: Center(child: _MultiSelectStatusButton()),
|
||||||
),
|
),
|
||||||
if (isBottomWidgetVisible) widget.bottomSheet!,
|
),
|
||||||
],
|
if (isBottomWidgetVisible) widget.bottomSheet!,
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user