mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
timeline go brrrrr
This commit is contained in:
parent
b82b9a550a
commit
6311ecadd4
@ -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:
|
||||
|
@ -16,16 +16,16 @@ class AssetRepository with LogMixin implements IAssetRepository {
|
||||
@override
|
||||
Future<bool> upsertAll(Iterable<Asset> 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) {
|
||||
|
@ -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),
|
||||
)));
|
||||
await _db.batch((batch) => batch.insertAllOnConflictUpdate(
|
||||
_db.deviceAssetToHash,
|
||||
assetHash.map(_toEntity),
|
||||
));
|
||||
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
|
@ -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) {
|
||||
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");
|
||||
|
@ -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
|
||||
|
@ -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,51 +73,46 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
||||
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<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,
|
||||
);
|
||||
},
|
||||
|
@ -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<AssetGridCubit>().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<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,
|
||||
),
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
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<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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user