From 6311ecadd4ee3d15eb2af26abec14a7bc49d2588 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Wed, 19 Mar 2025 00:03:45 +0530 Subject: [PATCH] timeline go brrrrr --- mobile-v2/immich_lint/pubspec.lock | 14 +- .../domain/repositories/asset.repository.dart | 20 +- .../device_asset_hash.repository.dart | 9 +- .../domain/repositories/log.repository.dart | 6 +- .../lib/domain/services/login.service.dart | 4 + .../components/grid/asset_grid.widget.dart | 94 +++--- .../grid/asset_render_grid.widget.dart | 83 ++--- .../components/grid/draggable_scrollbar.dart | 287 +++++++++++------- .../components/image/immich_image.widget.dart | 42 ++- mobile-v2/lib/utils/constants/globals.dart | 4 +- mobile-v2/pubspec.lock | 8 + mobile-v2/pubspec.yaml | 1 + 12 files changed, 323 insertions(+), 249 deletions(-) diff --git a/mobile-v2/immich_lint/pubspec.lock b/mobile-v2/immich_lint/pubspec.lock index 6f3561b71e..6d9b0a6dd2 100644 --- a/mobile-v2/immich_lint/pubspec.lock +++ b/mobile-v2/immich_lint/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "73.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: "direct main" description: name: analyzer - sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.11.0" analyzer_plugin: dependency: "direct main" description: @@ -202,10 +202,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: diff --git a/mobile-v2/lib/domain/repositories/asset.repository.dart b/mobile-v2/lib/domain/repositories/asset.repository.dart index 5416130667..3aad559fb9 100644 --- a/mobile-v2/lib/domain/repositories/asset.repository.dart +++ b/mobile-v2/lib/domain/repositories/asset.repository.dart @@ -16,16 +16,16 @@ class AssetRepository with LogMixin implements IAssetRepository { @override Future upsertAll(Iterable assets) async { try { - await _db.txn(() async => await _db.batch((batch) { - final rows = assets.map(_toEntity); - for (final row in rows) { - batch.insert( - _db.asset, - row, - onConflict: DoUpdate((_) => row, target: [_db.asset.hash]), - ); - } - })); + await _db.batch((batch) { + final rows = assets.map(_toEntity); + for (final row in rows) { + batch.insert( + _db.asset, + row, + onConflict: DoUpdate((_) => row, target: [_db.asset.hash]), + ); + } + }); return true; } catch (e, s) { diff --git a/mobile-v2/lib/domain/repositories/device_asset_hash.repository.dart b/mobile-v2/lib/domain/repositories/device_asset_hash.repository.dart index 627a1375e2..d8230ce439 100644 --- a/mobile-v2/lib/domain/repositories/device_asset_hash.repository.dart +++ b/mobile-v2/lib/domain/repositories/device_asset_hash.repository.dart @@ -18,11 +18,10 @@ class DeviceAssetToHashRepository @override Future upsertAll(Iterable assetHash) async { try { - await _db.txn(() async => - await _db.batch((batch) => batch.insertAllOnConflictUpdate( - _db.deviceAssetToHash, - assetHash.map(_toEntity), - ))); + await _db.batch((batch) => batch.insertAllOnConflictUpdate( + _db.deviceAssetToHash, + assetHash.map(_toEntity), + )); return true; } catch (e, s) { diff --git a/mobile-v2/lib/domain/repositories/log.repository.dart b/mobile-v2/lib/domain/repositories/log.repository.dart index 213ad374eb..a48bd612dd 100644 --- a/mobile-v2/lib/domain/repositories/log.repository.dart +++ b/mobile-v2/lib/domain/repositories/log.repository.dart @@ -46,9 +46,9 @@ class LogRepository implements ILogRepository { @override Future createAll(Iterable logs) async { try { - await _db.txn(() async => await _db.batch((b) { - b.insertAll(_db.logs, logs.map(_toEntity)); - })); + await _db.batch((b) { + b.insertAll(_db.logs, logs.map(_toEntity)); + }); return true; } catch (e) { debugPrint("Error while adding a log to the DB - $e"); diff --git a/mobile-v2/lib/domain/services/login.service.dart b/mobile-v2/lib/domain/services/login.service.dart index f3d4d9e4d2..0ae91c9c7e 100644 --- a/mobile-v2/lib/domain/services/login.service.dart +++ b/mobile-v2/lib/domain/services/login.service.dart @@ -131,6 +131,10 @@ class LoginService with LogMixin { } ServiceLocator.registerCurrentUser(user); + await di.unregister(); + di.registerLazySingleton( + () => ServerInfoProvider(serverApiRepo: di()), + ); await di().fetchServerDisk(); // sync assets in background diff --git a/mobile-v2/lib/presentation/components/grid/asset_grid.widget.dart b/mobile-v2/lib/presentation/components/grid/asset_grid.widget.dart index 2a3ad45682..31d17ca8ea 100644 --- a/mobile-v2/lib/presentation/components/grid/asset_grid.widget.dart +++ b/mobile-v2/lib/presentation/components/grid/asset_grid.widget.dart @@ -1,7 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_list_view/flutter_list_view.dart'; import 'package:immich_mobile/domain/models/render_list_element.model.dart'; import 'package:immich_mobile/i18n/strings.g.dart'; import 'package:immich_mobile/presentation/components/common/page_empty.widget.dart'; @@ -13,6 +12,7 @@ import 'package:immich_mobile/utils/extensions/build_context.extension.dart'; import 'package:immich_mobile/utils/extensions/color.extension.dart'; import 'package:intl/intl.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; part 'asset_grid_header.widget.dart'; @@ -20,31 +20,18 @@ class ImAssetGrid extends StatefulWidget { /// The padding for the grid final double? topPadding; - final FlutterListViewController? controller; - - const ImAssetGrid({this.controller, this.topPadding, super.key}); + const ImAssetGrid({this.topPadding, super.key}); @override State createState() => _ImAssetGridState(); } class _ImAssetGridState extends State { - late final FlutterListViewController _controller; - - @override - void initState() { - super.initState(); - _controller = widget.controller ?? FlutterListViewController(); - } - - @override - void dispose() { - // Dispose controller if it was created here - if (widget.controller == null) { - _controller.dispose(); - } - super.dispose(); - } + final ItemScrollController _itemScrollController = ItemScrollController(); + final ScrollOffsetController _scrollOffsetController = + ScrollOffsetController(); + final ItemPositionsListener _itemPositionsListener = + ItemPositionsListener.create(); Text? _labelBuilder(List elements, int currentPosition) { final element = elements.elementAtOrNull(currentPosition); @@ -86,51 +73,46 @@ class _ImAssetGridState extends State { elements.removeAt(0); } - final grid = FlutterListView( - delegate: FlutterListViewDelegate( - (_, sectionIndex) { - // ignore: avoid-unsafe-collection-methods - final section = elements[sectionIndex]; + final EdgeInsets? padding = null; - return switch (section) { - RenderListPaddingElement() => Padding( - padding: EdgeInsets.only(top: section.topPadding), - ), - RenderListMonthHeaderElement() => - _MonthHeader(text: section.header), - RenderListDayHeaderElement() => Text(section.header), - RenderListAssetElement() => ImStaticGrid( - section: section, - isDragging: state.isDragScrolling, - ), - }; - }, - childCount: elements.length, - addAutomaticKeepAlives: false, - ), - controller: _controller, + final grid = ScrollablePositionedList.builder( + itemCount: state.renderList.elements.length, + itemBuilder: (_, sectionIndex) { + // ignore: avoid-unsafe-collection-methods + final section = elements[sectionIndex]; + + return switch (section) { + RenderListPaddingElement() => Padding( + padding: EdgeInsets.only(top: section.topPadding), + ), + RenderListMonthHeaderElement() => + _MonthHeader(text: section.header), + RenderListDayHeaderElement() => Text(section.header), + RenderListAssetElement() => ImStaticGrid(section: section), + }; + }, + itemScrollController: _itemScrollController, + itemPositionsListener: _itemPositionsListener, + scrollOffsetController: _scrollOffsetController, + padding: padding, + addRepaintBoundaries: true, ); - final EdgeInsetsGeometry? padding; - if (widget.topPadding == null) { - padding = null; - } else { - padding = EdgeInsets.only(top: widget.topPadding!); - } - - return DraggableScrollbar( - controller: _controller, - maxItemCount: elements.length, + return DraggableScrollbar.semicircle( + alwaysVisibleScrollThumb: true, + controller: _itemScrollController, + itemPositionsListener: _itemPositionsListener, scrollStateListener: context.read().setDragScrolling, backgroundColor: context.colorScheme.surfaceContainerHighest, foregroundColor: context.colorScheme.onSurface, - padding: padding, - scrollbarAnimationDuration: Durations.medium2, - scrollbarTimeToFade: Durations.extralong4, + padding: EdgeInsets.only(top: 120), + heightOffset: 100, + scrollbarAnimationDuration: const Duration(milliseconds: 300), + scrollbarTimeToFade: const Duration(milliseconds: 1000), labelTextBuilder: (int position) => _labelBuilder(elements, position), - labelConstraints: const BoxConstraints(maxHeight: 36), + labelConstraints: const BoxConstraints(maxHeight: 28), child: grid, ); }, diff --git a/mobile-v2/lib/presentation/components/grid/asset_render_grid.widget.dart b/mobile-v2/lib/presentation/components/grid/asset_render_grid.widget.dart index 1b428a828b..274eff79ed 100644 --- a/mobile-v2/lib/presentation/components/grid/asset_render_grid.widget.dart +++ b/mobile-v2/lib/presentation/components/grid/asset_render_grid.widget.dart @@ -8,49 +8,54 @@ import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart'; class ImStaticGrid extends StatelessWidget { final RenderListAssetElement section; - final bool isDragging; - const ImStaticGrid({ - super.key, - required this.section, - required this.isDragging, - }); + const ImStaticGrid({super.key, required this.section}); @override Widget build(BuildContext context) { - return FutureBuilder( - future: context.read().loadAssets( - section.assetOffset, - section.assetCount, - ), - builder: (_, assetsSnap) { - final assets = assetsSnap.data; - return GridView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: const EdgeInsets.all(0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - mainAxisSpacing: 3, - crossAxisSpacing: 3, - ), - itemBuilder: (_, i) { - final asset = assetsSnap.isWaiting || assets == null - ? null - : assets.elementAtOrNull(i); - return SizedBox.square( - dimension: 200, - // Show Placeholder when drag scrolled - child: asset == null || isDragging - ? const ImImagePlaceholder() - : ImThumbnail(asset), - ); - }, - itemCount: section.assetCount, - addAutomaticKeepAlives: false, - cacheExtent: 100, - ); - }, + return BlocSelector( + selector: (state) => state.isDragScrolling, + builder: (_, isDragging) => FutureBuilder( + future: isDragging + ? Future.value(null) + // ignore: avoid-async-call-in-sync-function + : context.read().loadAssets( + section.assetOffset, + section.assetCount, + ), + builder: (_, assetsSnap) { + final assets = assetsSnap.data; + return GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.all(0), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), + itemBuilder: (_, i) { + if (isDragging) { + return const ImImagePlaceholder(); + } + + final asset = assetsSnap.isWaiting || assets == null + ? null + : assets.elementAtOrNull(i); + return SizedBox.square( + dimension: 200, + // Show Placeholder when drag scrolled + child: asset == null + ? const ImImagePlaceholder() + : ImThumbnail(asset), + ); + }, + itemCount: section.assetCount, + addAutomaticKeepAlives: false, + cacheExtent: 100, + ); + }, + ), ); } } diff --git a/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart b/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart index c252ee0c96..38156b6383 100644 --- a/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart +++ b/mobile-v2/lib/presentation/components/grid/draggable_scrollbar.dart @@ -1,10 +1,7 @@ -// ignore_for_file: avoid-passing-self-as-argument - import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_list_view/flutter_list_view.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; /// Build the Scroll Thumb and label using the current configuration typedef ScrollThumbBuilder = Widget Function( @@ -24,12 +21,9 @@ typedef LabelTextBuilder = Text? Function(int item); /// for quick navigation of the BoxScrollView. class DraggableScrollbar extends StatefulWidget { /// The view that will be scrolled with the scroll thumb - final CustomScrollView child; + final ScrollablePositionedList child; - /// Total number of children in the list - final int maxItemCount; - - final FlutterListViewController controller; + final ItemPositionsListener itemPositionsListener; /// A function that builds a thumb using the current configuration final ScrollThumbBuilder scrollThumbBuilder; @@ -46,6 +40,9 @@ class DraggableScrollbar extends StatefulWidget { /// The amount of padding that should surround the thumb final EdgeInsetsGeometry? padding; + /// The height offset of the thumb/bar from the bottom of the page + final double? heightOffset; + /// Determines how quickly the scrollbar will animate in and out final Duration scrollbarAnimationDuration; @@ -58,25 +55,29 @@ class DraggableScrollbar extends StatefulWidget { /// Determines box constraints for Container displaying label final BoxConstraints? labelConstraints; + /// The ScrollController for the BoxScrollView + final ItemScrollController controller; + /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] final bool alwaysVisibleScrollThumb; final Function(bool scrolling) scrollStateListener; - DraggableScrollbar({ + DraggableScrollbar.semicircle({ super.key, Key? scrollThumbKey, this.alwaysVisibleScrollThumb = false, required this.child, required this.controller, - required this.maxItemCount, + required this.itemPositionsListener, required this.scrollStateListener, this.heightScrollThumb = 48.0, this.backgroundColor = Colors.white, this.foregroundColor = Colors.black, this.padding, - this.scrollbarAnimationDuration = Durations.medium2, - this.scrollbarTimeToFade = Durations.long4, + this.heightOffset, + this.scrollbarAnimationDuration = const Duration(milliseconds: 300), + this.scrollbarTimeToFade = const Duration(milliseconds: 600), this.labelTextBuilder, this.labelConstraints, }) : assert(child.scrollDirection == Axis.vertical), @@ -87,7 +88,7 @@ class DraggableScrollbar extends StatefulWidget { ); @override - State createState() => _DraggableScrollbarState(); + DraggableScrollbarState createState() => DraggableScrollbarState(); static buildScrollThumbAndLabel({ required Widget scrollThumb, @@ -98,13 +99,13 @@ class DraggableScrollbar extends StatefulWidget { required BoxConstraints? labelConstraints, required bool alwaysVisibleScrollThumb, }) { - Widget scrollThumbAndLabel = labelText == null + var scrollThumbAndLabel = labelText == null ? scrollThumb : Row( - mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ - _ScrollLabel( + ScrollLabel( animation: labelAnimation, backgroundColor: backgroundColor, constraints: labelConstraints, @@ -117,7 +118,7 @@ class DraggableScrollbar extends StatefulWidget { if (alwaysVisibleScrollThumb) { return scrollThumbAndLabel; } - return _SlideFadeTransition( + return SlideFadeTransition( animation: thumbAnimation!, child: scrollThumbAndLabel, ); @@ -139,7 +140,7 @@ class DraggableScrollbar extends StatefulWidget { }) { final scrollThumb = CustomPaint( key: scrollThumbKey, - foregroundPainter: _ArrowCustomPainter(foregroundColor), + foregroundPainter: ArrowCustomPainter(foregroundColor), child: Material( elevation: 4.0, color: backgroundColor, @@ -168,7 +169,7 @@ class DraggableScrollbar extends StatefulWidget { } } -class _ScrollLabel extends StatelessWidget { +class ScrollLabel extends StatelessWidget { final Animation? animation; final Color backgroundColor; final Text child; @@ -177,7 +178,8 @@ class _ScrollLabel extends StatelessWidget { static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - const _ScrollLabel({ + const ScrollLabel({ + super.key, required this.child, required this.animation, required this.backgroundColor, @@ -195,9 +197,9 @@ class _ScrollLabel extends StatelessWidget { color: backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(16.0)), child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 15), constraints: constraints ?? _defaultConstraints, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, child: child, ), ), @@ -206,7 +208,7 @@ class _ScrollLabel extends StatelessWidget { } } -class _DraggableScrollbarState extends State +class DraggableScrollbarState extends State with TickerProviderStateMixin { late double _barOffset; late bool _isDragInProcess; @@ -217,11 +219,6 @@ class _DraggableScrollbarState extends State late AnimationController _labelAnimationController; late Animation _labelAnimation; Timer? _fadeoutTimer; - List _positions = []; - - /// The controller can have only one active callback - /// cache the old one, invoke it in the new callback and restore it on dispose - FlutterSliverListControllerOnPaintItemPositionCallback? _oldCallback; @override void initState() { @@ -231,8 +228,8 @@ class _DraggableScrollbarState extends State _currentItem = 0; _thumbAnimationController = AnimationController( - duration: widget.scrollbarAnimationDuration, vsync: this, + duration: widget.scrollbarAnimationDuration, ); _thumbAnimation = CurvedAnimation( @@ -241,35 +238,33 @@ class _DraggableScrollbarState extends State ); _labelAnimationController = AnimationController( - duration: widget.scrollbarAnimationDuration, vsync: this, + duration: widget.scrollbarAnimationDuration, ); _labelAnimation = CurvedAnimation( parent: _labelAnimationController, curve: Curves.fastOutSlowIn, ); - - _oldCallback = - widget.controller.sliverController.onPaintItemPositionsCallback; - widget.controller.sliverController.onPaintItemPositionsCallback = - (height, pos) { - _positions = pos; - _oldCallback?.call(height, pos); - }; } @override void dispose() { - widget.controller.sliverController.onPaintItemPositionsCallback = - _oldCallback; _thumbAnimationController.dispose(); _labelAnimationController.dispose(); _fadeoutTimer?.cancel(); - _dragHaltTimer?.cancel(); super.dispose(); } + double get barMaxScrollExtent => + (context.size?.height ?? 0) - + widget.heightScrollThumb - + (widget.heightOffset ?? 0); + + double get barMinScrollExtent => 0; + + int get maxItemCount => widget.child.itemCount; + @override Widget build(BuildContext context) { Text? labelText; @@ -278,12 +273,19 @@ class _DraggableScrollbarState extends State } return LayoutBuilder( - builder: (BuildContext _, BoxConstraints constraints) { + builder: (BuildContext context, BoxConstraints constraints) { + //print("LayoutBuilder constraints=$constraints"); + return NotificationListener( - onNotification: _onScrollNotification, + onNotification: (ScrollNotification notification) { + changePosition(notification); + return false; + }, child: Stack( - children: [ - RepaintBoundary(child: widget.child), + children: [ + RepaintBoundary( + child: widget.child, + ), RepaintBoundary( child: GestureDetector( onVerticalDragStart: _onVerticalDragStart, @@ -291,16 +293,16 @@ class _DraggableScrollbarState extends State onVerticalDragEnd: _onVerticalDragEnd, child: Container( alignment: Alignment.topRight, - padding: widget.padding, margin: EdgeInsets.only(top: _barOffset), + padding: widget.padding, child: widget.scrollThumbBuilder( widget.backgroundColor, widget.foregroundColor, _thumbAnimation, _labelAnimation, widget.heightScrollThumb, - labelConstraints: widget.labelConstraints, labelText: labelText, + labelConstraints: widget.labelConstraints, ), ), ), @@ -312,42 +314,28 @@ class _DraggableScrollbarState extends State ); } - double get _barMaxScrollExtent => - (context.size?.height ?? 0) - - widget.heightScrollThumb - - (widget.padding?.vertical ?? 0); - - double get _maxScrollRatio => - _barMaxScrollExtent / widget.controller.position.maxScrollExtent; - - double get _barMinScrollExtent => 0; - - bool _onScrollNotification(ScrollNotification notification) { - _changePosition(notification); - return false; - } - - void _onScrollFade() { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - } - // scroll bar has received notification that it's view was scrolled // so it should also changes his position // but only if it isn't dragged - void _changePosition(ScrollNotification notification) { + changePosition(ScrollNotification notification) { if (_isDragInProcess) { return; } setState(() { try { - if (notification is ScrollUpdateNotification) { - _barOffset = widget.controller.offset * _maxScrollRatio; + int firstItemIndex = + widget.itemPositionsListener.itemPositions.value.first.index; - _barOffset = - clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent); + if (notification is ScrollUpdateNotification) { + _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; + + if (_barOffset < barMinScrollExtent) { + _barOffset = barMinScrollExtent; + } + if (_barOffset > barMaxScrollExtent) { + _barOffset = barMaxScrollExtent; + } } if (notification is ScrollUpdateNotification || @@ -356,13 +344,16 @@ class _DraggableScrollbarState extends State _thumbAnimationController.forward(); } - final lastItemPos = _itemPos; - if (lastItemPos < widget.maxItemCount) { - _currentItem = lastItemPos; + if (itemPosition < maxItemCount) { + _currentItem = itemPosition; } _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, _onScrollFade); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); } } catch (_) {} }); @@ -378,34 +369,35 @@ class _DraggableScrollbarState extends State widget.scrollStateListener(true); } - int get _itemIndex { - int index = 0; - double minDiff = 1000; - for (final pos in _positions) { - final diff = (_barOffset - pos.offset).abs(); - if (diff < minDiff) { - minDiff = diff; - index = pos.index; - } - } - return index; + int get itemPosition { + int numberOfItems = widget.child.itemCount; + return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt(); } - int get _itemPos => - ((_barOffset / (_barMaxScrollExtent)) * widget.maxItemCount).toInt(); - - void _jumpToBarPos() { - final lastItemPos = _itemPos; - if (lastItemPos > widget.maxItemCount - 1) { + void _jumpToBarPosition() { + if (itemPosition > maxItemCount - 1) { return; } - _currentItem = _itemIndex; - widget.controller.sliverController.jumpToIndex(lastItemPos); + _currentItem = itemPosition; + + /// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error) + /// jump to the end of the list + if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) { + widget.controller.jumpTo( + index: maxItemCount, + ); + + return; + } + + widget.controller.jumpTo( + index: itemPosition, + ); } - Timer? _dragHaltTimer; - int _lastTimerPos = 0; + Timer? dragHaltTimer; + int lastTimerPosition = 0; void _onVerticalDragUpdate(DragUpdateDetails details) { setState(() { @@ -415,31 +407,40 @@ class _DraggableScrollbarState extends State if (_isDragInProcess) { _barOffset += details.delta.dy; - _barOffset = - clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent); + if (_barOffset < barMinScrollExtent) { + _barOffset = barMinScrollExtent; + } + if (_barOffset > barMaxScrollExtent) { + _barOffset = barMaxScrollExtent; + } - final lastItemPos = _itemPos; - if (lastItemPos != _lastTimerPos) { - _lastTimerPos = lastItemPos; - _dragHaltTimer?.cancel(); + if (itemPosition != lastTimerPosition) { + lastTimerPosition = itemPosition; + dragHaltTimer?.cancel(); widget.scrollStateListener(true); - _dragHaltTimer = Timer( - Durations.long2, - () => widget.scrollStateListener(false), + dragHaltTimer = Timer( + const Duration(milliseconds: 500), + () { + widget.scrollStateListener(false); + }, ); } - _jumpToBarPos(); + _jumpToBarPosition(); } }); } void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, _onScrollFade); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); setState(() { - _jumpToBarPos(); + _jumpToBarPosition(); _isDragInProcess = false; }); @@ -448,10 +449,10 @@ class _DraggableScrollbarState extends State } /// Draws 2 triangles like arrow up and arrow down -class _ArrowCustomPainter extends CustomPainter { +class ArrowCustomPainter extends CustomPainter { Color color; - _ArrowCustomPainter(this.color); + ArrowCustomPainter(this.color); @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; @@ -483,23 +484,75 @@ class _ArrowCustomPainter extends CustomPainter { } } -class _SlideFadeTransition extends StatelessWidget { +///This cut 2 lines in arrow shape +class ArrowClipper extends CustomClipper { + @override + Path getClip(Size size) { + Path path = Path(); + path.lineTo(0.0, size.height); + path.lineTo(size.width, size.height); + path.lineTo(size.width, 0.0); + path.lineTo(0.0, 0.0); + path.close(); + + double arrowWidth = 8.0; + double startPointX = (size.width - arrowWidth) / 2; + double startPointY = size.height / 2 - arrowWidth / 2; + path.moveTo(startPointX, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); + path.lineTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth, startPointY + 1.0); + path.lineTo( + startPointX + arrowWidth / 2, + startPointY - arrowWidth / 2 + 1.0, + ); + path.lineTo(startPointX, startPointY + 1.0); + path.close(); + + startPointY = size.height / 2 + arrowWidth / 2; + path.moveTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); + path.lineTo(startPointX, startPointY); + path.lineTo(startPointX, startPointY - 1.0); + path.lineTo( + startPointX + arrowWidth / 2, + startPointY + arrowWidth / 2 - 1.0, + ); + path.lineTo(startPointX + arrowWidth, startPointY - 1.0); + path.close(); + + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +class SlideFadeTransition extends StatelessWidget { final Animation animation; final Widget child; - const _SlideFadeTransition({required this.animation, required this.child}); + const SlideFadeTransition({ + super.key, + required this.animation, + required this.child, + }); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: animation, - builder: (_, c) => animation.value == 0.0 ? const SizedBox() : c!, + builder: (context, child) => + animation.value == 0.0 ? const SizedBox() : child!, child: SlideTransition( position: Tween( begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0), ).animate(animation), - child: FadeTransition(opacity: animation, child: child), + child: FadeTransition( + opacity: animation, + child: child, + ), ), ); } diff --git a/mobile-v2/lib/presentation/components/image/immich_image.widget.dart b/mobile-v2/lib/presentation/components/image/immich_image.widget.dart index a1754d07c7..0caa1bbd21 100644 --- a/mobile-v2/lib/presentation/components/image/immich_image.widget.dart +++ b/mobile-v2/lib/presentation/components/image/immich_image.widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/presentation/components/image/provider/immich_local_image_provider.dart'; -import 'package:immich_mobile/presentation/components/image/provider/immich_remote_image_provider.dart'; +import 'package:immich_mobile/presentation/components/image/provider/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/utils/extensions/build_context.extension.dart'; import 'package:octo_image/octo_image.dart'; @@ -20,7 +20,7 @@ class ImImagePlaceholder extends StatelessWidget { } // ignore: prefer-single-widget-per-file -class ImImage extends StatelessWidget { +class ImImage extends StatefulWidget { final Asset asset; final double? width; final double? height; @@ -47,7 +47,7 @@ class ImImage extends StatelessWidget { } if (asset == null) { - return ImRemoteImageProvider(assetId: assetId!); + return ImmichRemoteThumbnailProvider(assetId: assetId!); } // Whether to use the local asset image provider or a remote one @@ -57,23 +57,45 @@ class ImImage extends StatelessWidget { return ImLocalImageProvider(asset: asset); } - return ImRemoteImageProvider(assetId: asset.remoteId!); + return ImmichRemoteThumbnailProvider(assetId: asset.remoteId!); + } + + @override + State createState() => _ImImageState(); +} + +class _ImImageState extends State { + late DisposableBuildContext _context; + + @override + void initState() { + super.initState(); + _context = DisposableBuildContext(this); + } + + @override + void dispose() { + _context.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { return OctoImage( - image: ImImage.imageProvider(asset: asset), - placeholderBuilder: (_) => placeholder, + image: ScrollAwareImageProvider( + context: _context, + imageProvider: ImImage.imageProvider(asset: widget.asset), + ), + placeholderBuilder: (_) => widget.placeholder, errorBuilder: (_, error, stackTrace) { if (error is PlatformException && error.code == "The asset not found!") { debugPrint( - "Asset ${asset.localId ?? asset.id ?? "-"} does not exist anymore on device!", + "Asset ${widget.asset.localId ?? widget.asset.id ?? "-"} does not exist anymore on device!", ); } else { debugPrint( - "Error getting thumb for assetId=${asset.localId ?? asset.id ?? "-"}: $error", + "Error getting thumb for assetId=${widget.asset.localId ?? widget.asset.id ?? "-"}: $error", ); } return Icon( @@ -83,8 +105,8 @@ class ImImage extends StatelessWidget { }, fadeOutDuration: Durations.short4, fadeInDuration: Duration.zero, - width: width, - height: height, + width: widget.width, + height: widget.height, fit: BoxFit.cover, ); } diff --git a/mobile-v2/lib/utils/constants/globals.dart b/mobile-v2/lib/utils/constants/globals.dart index 75ecff36c6..16d450413b 100644 --- a/mobile-v2/lib/utils/constants/globals.dart +++ b/mobile-v2/lib/utils/constants/globals.dart @@ -16,8 +16,8 @@ const int kGridThumbnailSize = 200; const int kGridThumbnailQuality = 80; /// RenderList constants -const int kRenderListBatchSize = 512; -const int kRenderListOppositeBatchSize = 128; +const int kRenderListBatchSize = 256; +const int kRenderListOppositeBatchSize = 64; /// Sync constants const int kFullSyncChunkSize = 10000; diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index a34c637e77..b9f5ce4161 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -1000,6 +1000,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" shelf: dependency: transitive description: diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml index 03724452ee..77942f556b 100644 --- a/mobile-v2/pubspec.yaml +++ b/mobile-v2/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: material_symbols_icons: ^4.2789.0 flutter_adaptive_scaffold: ^0.3.1 flutter_list_view: ^1.1.28 + scrollable_positioned_list: ^0.3.8 cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 skeletonizer: ^1.4.2