timeline go brrrrr

This commit is contained in:
shenlong-tanwen 2025-03-19 00:03:45 +05:30
parent b82b9a550a
commit 6311ecadd4
12 changed files with 323 additions and 249 deletions

View File

@ -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:

View File

@ -16,7 +16,7 @@ class AssetRepository with LogMixin implements IAssetRepository {
@override
Future<bool> upsertAll(Iterable<Asset> assets) async {
try {
await _db.txn(() async => await _db.batch((batch) {
await _db.batch((batch) {
final rows = assets.map(_toEntity);
for (final row in rows) {
batch.insert(
@ -25,7 +25,7 @@ class AssetRepository with LogMixin implements IAssetRepository {
onConflict: DoUpdate((_) => row, target: [_db.asset.hash]),
);
}
}));
});
return true;
} catch (e, s) {

View File

@ -18,11 +18,10 @@ class DeviceAssetToHashRepository
@override
Future<bool> upsertAll(Iterable<DeviceAssetToHash> assetHash) async {
try {
await _db.txn(() async =>
await _db.batch((batch) => batch.insertAllOnConflictUpdate(
_db.deviceAssetToHash,
assetHash.map(_toEntity),
)));
));
return true;
} catch (e, s) {

View File

@ -46,9 +46,9 @@ class LogRepository implements ILogRepository {
@override
Future<bool> createAll(Iterable<LogMessage> logs) async {
try {
await _db.txn(() async => await _db.batch((b) {
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");

View File

@ -131,6 +131,10 @@ class LoginService with LogMixin {
}
ServiceLocator.registerCurrentUser(user);
await di.unregister<ServerInfoProvider>();
di.registerLazySingleton<ServerInfoProvider>(
() => ServerInfoProvider(serverApiRepo: di()),
);
await di<ServerInfoProvider>().fetchServerDisk();
// sync assets in background

View File

@ -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<ImAssetGrid> {
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<RenderListElement> elements, int currentPosition) {
final element = elements.elementAtOrNull(currentPosition);
@ -86,9 +73,11 @@ class _ImAssetGridState extends State<ImAssetGrid> {
elements.removeAt(0);
}
final grid = FlutterListView(
delegate: FlutterListViewDelegate(
(_, sectionIndex) {
final EdgeInsets? padding = null;
final grid = ScrollablePositionedList.builder(
itemCount: state.renderList.elements.length,
itemBuilder: (_, sectionIndex) {
// ignore: avoid-unsafe-collection-methods
final section = elements[sectionIndex];
@ -99,38 +88,31 @@ class _ImAssetGridState extends State<ImAssetGrid> {
RenderListMonthHeaderElement() =>
_MonthHeader(text: section.header),
RenderListDayHeaderElement() => Text(section.header),
RenderListAssetElement() => ImStaticGrid(
section: section,
isDragging: state.isDragScrolling,
),
RenderListAssetElement() => ImStaticGrid(section: section),
};
},
childCount: elements.length,
addAutomaticKeepAlives: false,
),
controller: _controller,
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<AssetGridCubit>().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,
);
},

View File

@ -8,18 +8,18 @@ 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<AssetGridCubit>().loadAssets(
return BlocSelector<AssetGridCubit, AssetGridState, bool>(
selector: (state) => state.isDragScrolling,
builder: (_, isDragging) => FutureBuilder(
future: isDragging
? Future.value(null)
// ignore: avoid-async-call-in-sync-function
: context.read<AssetGridCubit>().loadAssets(
section.assetOffset,
section.assetCount,
),
@ -35,13 +35,17 @@ class ImStaticGrid extends StatelessWidget {
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 || isDragging
child: asset == null
? const ImImagePlaceholder()
: ImThumbnail(asset),
);
@ -51,6 +55,7 @@ class ImStaticGrid extends StatelessWidget {
cacheExtent: 100,
);
},
),
);
}
}

View File

@ -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<double>? 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<DraggableScrollbar>
class DraggableScrollbarState extends State<DraggableScrollbar>
with TickerProviderStateMixin {
late double _barOffset;
late bool _isDragInProcess;
@ -217,11 +219,6 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
late AnimationController _labelAnimationController;
late Animation<double> _labelAnimation;
Timer? _fadeoutTimer;
List<FlutterListViewItemPosition> _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<DraggableScrollbar>
_currentItem = 0;
_thumbAnimationController = AnimationController(
duration: widget.scrollbarAnimationDuration,
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_thumbAnimation = CurvedAnimation(
@ -241,35 +238,33 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
);
_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<DraggableScrollbar>
}
return LayoutBuilder(
builder: (BuildContext _, BoxConstraints constraints) {
builder: (BuildContext context, BoxConstraints constraints) {
//print("LayoutBuilder constraints=$constraints");
return NotificationListener<ScrollNotification>(
onNotification: _onScrollNotification,
onNotification: (ScrollNotification notification) {
changePosition(notification);
return false;
},
child: Stack(
children: [
RepaintBoundary(child: widget.child),
children: <Widget>[
RepaintBoundary(
child: widget.child,
),
RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onVerticalDragStart,
@ -291,16 +293,16 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
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<DraggableScrollbar>
);
}
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<DraggableScrollbar>
_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<DraggableScrollbar>
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;
}
Timer? _dragHaltTimer;
int _lastTimerPos = 0;
widget.controller.jumpTo(
index: itemPosition,
);
}
Timer? dragHaltTimer;
int lastTimerPosition = 0;
void _onVerticalDragUpdate(DragUpdateDetails details) {
setState(() {
@ -415,31 +407,40 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
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<DraggableScrollbar>
}
/// 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<Path> {
@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<Path> oldClipper) => false;
}
class SlideFadeTransition extends StatelessWidget {
final Animation<double> 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,
),
),
);
}

View File

@ -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<ImImage> {
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,
);
}

View File

@ -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;

View File

@ -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:

View File

@ -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