mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
fix(mobile): timeline orientation & foldable phones handling (#25088)
* [mobile]: Fix timeline handling on foldable phones + ensuring that images are not cut off This fixes the handling of unfolding the phone while having the application opened. So, the timeline is correctly rescaled and the current position is kept. Besides that it fixes a bug with the ordering which lead to images being "cut off" at the right side of the screen. * refactor + cleanup --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
b85f6f3fce
commit
730b770e67
1 changed files with 87 additions and 75 deletions
|
|
@ -29,7 +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 Timeline extends StatelessWidget {
|
class Timeline extends ConsumerWidget {
|
||||||
const Timeline({
|
const Timeline({
|
||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
|
|
@ -58,15 +58,15 @@ class Timeline extends StatelessWidget {
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (_, constraints) => ProviderScope(
|
builder: (_, constraints) => ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineArgsProvider.overrideWith(
|
timelineArgsProvider.overrideWithValue(
|
||||||
(ref) => TimelineArgs(
|
TimelineArgs(
|
||||||
maxWidth: constraints.maxWidth,
|
maxWidth: constraints.maxWidth,
|
||||||
maxHeight: constraints.maxHeight,
|
maxHeight: constraints.maxHeight,
|
||||||
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
||||||
|
|
@ -78,6 +78,7 @@ class Timeline extends StatelessWidget {
|
||||||
if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
||||||
],
|
],
|
||||||
child: _SliverTimeline(
|
child: _SliverTimeline(
|
||||||
|
key: const ValueKey('_sliver_timeline'),
|
||||||
topSliverWidget: topSliverWidget,
|
topSliverWidget: topSliverWidget,
|
||||||
topSliverWidgetHeight: topSliverWidgetHeight,
|
topSliverWidgetHeight: topSliverWidgetHeight,
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
|
|
@ -105,6 +106,7 @@ 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,
|
||||||
|
|
@ -139,14 +141,14 @@ 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? _scaleRestoreAssetIndex;
|
int? _restoreAssetIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController = ScrollController(
|
_scrollController = ScrollController(
|
||||||
initialScrollOffset: widget.initialScrollOffset ?? 0.0,
|
initialScrollOffset: widget.initialScrollOffset ?? 0.0,
|
||||||
onAttach: _restoreScalePosition,
|
onAttach: _restoreAssetPosition,
|
||||||
);
|
);
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
|
|
@ -179,14 +181,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _restoreScalePosition(_) {
|
void _restoreAssetPosition(_) {
|
||||||
if (_scaleRestoreAssetIndex == null) return;
|
if (_restoreAssetIndex == null) return;
|
||||||
|
|
||||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
asyncSegments.whenData((segments) {
|
asyncSegments.whenData((segments) {
|
||||||
final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _scaleRestoreAssetIndex!);
|
final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!);
|
||||||
if (targetSegment != null) {
|
if (targetSegment != null) {
|
||||||
final assetIndexInSegment = _scaleRestoreAssetIndex! - 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;
|
||||||
|
|
@ -198,7 +200,25 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_scaleRestoreAssetIndex = null;
|
_restoreAssetIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _getCurrentAssetIndex(List<Segment> segments) {
|
||||||
|
final currentOffset = _scrollController.offset.clamp(0.0, _scrollController.position.maxScrollExtent);
|
||||||
|
final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull;
|
||||||
|
int? targetAssetIndex;
|
||||||
|
if (segment != null) {
|
||||||
|
final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset);
|
||||||
|
if (rowIndex > segment.firstIndex) {
|
||||||
|
final rowIndexInSegment = rowIndex - (segment.firstIndex + 1);
|
||||||
|
final assetsPerRow = ref.read(timelineArgsProvider).columnCount;
|
||||||
|
final assetIndexInSegment = rowIndexInSegment * assetsPerRow;
|
||||||
|
targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment;
|
||||||
|
} else {
|
||||||
|
targetAssetIndex = segment.firstAssetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targetAssetIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -387,74 +407,66 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: RawGestureDetector(
|
child: NotificationListener<ScrollEndNotification>(
|
||||||
gestures: {
|
onNotification: (notification) {
|
||||||
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
final currentIndex = _getCurrentAssetIndex(segments);
|
||||||
() => CustomScaleGestureRecognizer(),
|
if (currentIndex != null && mounted) {
|
||||||
(CustomScaleGestureRecognizer scale) {
|
_restoreAssetIndex = currentIndex;
|
||||||
scale.onStart = (details) {
|
}
|
||||||
_baseScaleFactor = _scaleFactor;
|
return false;
|
||||||
};
|
|
||||||
|
|
||||||
scale.onUpdate = (details) {
|
|
||||||
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
|
||||||
final newPerRow = 7 - newScaleFactor.toInt();
|
|
||||||
|
|
||||||
if (newPerRow != _perRow) {
|
|
||||||
final currentOffset = _scrollController.offset.clamp(
|
|
||||||
0.0,
|
|
||||||
_scrollController.position.maxScrollExtent,
|
|
||||||
);
|
|
||||||
final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull;
|
|
||||||
int? targetAssetIndex;
|
|
||||||
if (segment != null) {
|
|
||||||
final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset);
|
|
||||||
if (rowIndex > segment.firstIndex) {
|
|
||||||
final rowIndexInSegment = rowIndex - (segment.firstIndex + 1);
|
|
||||||
final assetsPerRow = ref.read(timelineArgsProvider).columnCount;
|
|
||||||
final assetIndexInSegment = rowIndexInSegment * assetsPerRow;
|
|
||||||
targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment;
|
|
||||||
} else {
|
|
||||||
targetAssetIndex = segment.firstAssetIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_scaleFactor = newScaleFactor;
|
|
||||||
_perRow = newPerRow;
|
|
||||||
_scaleRestoreAssetIndex = targetAssetIndex;
|
|
||||||
});
|
|
||||||
|
|
||||||
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
child: TimelineDragRegion(
|
child: RawGestureDetector(
|
||||||
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
gestures: {
|
||||||
onAssetEnter: _handleDragAssetEnter,
|
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
||||||
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
() => CustomScaleGestureRecognizer(),
|
||||||
onScroll: _dragScroll,
|
(CustomScaleGestureRecognizer scale) {
|
||||||
onScrollStart: () {
|
scale.onStart = (details) {
|
||||||
// Minimize the bottom sheet when drag selection starts
|
_baseScaleFactor = _scaleFactor;
|
||||||
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
};
|
||||||
|
|
||||||
|
scale.onUpdate = (details) {
|
||||||
|
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
||||||
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
|
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
||||||
|
|
||||||
|
if (newPerRow != _perRow) {
|
||||||
|
setState(() {
|
||||||
|
_scaleFactor = newScaleFactor;
|
||||||
|
_perRow = newPerRow;
|
||||||
|
_restoreAssetIndex = targetAssetIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: TimelineDragRegion(
|
||||||
children: [
|
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
|
||||||
timeline,
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
|
||||||
Positioned(
|
onScroll: _dragScroll,
|
||||||
top: MediaQuery.paddingOf(context).top,
|
onScrollStart: () {
|
||||||
left: 25,
|
// Minimize the bottom sheet when drag selection starts
|
||||||
child: const SizedBox(
|
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
||||||
height: kToolbarHeight,
|
},
|
||||||
child: Center(child: _MultiSelectStatusButton()),
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
timeline,
|
||||||
|
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.paddingOf(context).top,
|
||||||
|
left: 25,
|
||||||
|
child: const SizedBox(
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: Center(child: _MultiSelectStatusButton()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.bottomSheet != null) widget.bottomSheet!,
|
||||||
if (widget.bottomSheet != null) widget.bottomSheet!,
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue