mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
nearly feature complete, bar dragging down
This commit is contained in:
@@ -224,86 +224,85 @@ class AssetDetails extends ConsumerWidget {
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: minHeight),
|
||||
decoration: BoxDecoration(color: context.isDarkTheme ? context.colorScheme.surface : Colors.white),
|
||||
child: Stack(
|
||||
decoration: BoxDecoration(
|
||||
color: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const _DragHandle(),
|
||||
Column(
|
||||
children: [
|
||||
// Asset Date and Time
|
||||
SheetTile(
|
||||
title: _getDateTime(context, asset, exifInfo),
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
||||
),
|
||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner),
|
||||
const SheetPeopleDetails(),
|
||||
const SheetLocationDetails(),
|
||||
// Details header
|
||||
SheetTile(
|
||||
title: 'details'.t(context: context),
|
||||
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
// File info
|
||||
buildFileInfoTile(),
|
||||
// Camera info
|
||||
if (cameraTitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: cameraTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
// Lens info
|
||||
if (lensTitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: lensTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
// Rating bar
|
||||
if (isRatingEnabled) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
'rating'.t(context: context),
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
RatingBar(
|
||||
initialRating: exifInfo?.rating?.toDouble() ?? 0,
|
||||
filledColor: context.themeData.colorScheme.primary,
|
||||
unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100),
|
||||
itemSize: 40,
|
||||
onRatingUpdate: (rating) async {
|
||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
|
||||
},
|
||||
onClearRating: () async {
|
||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, 0);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 60),
|
||||
],
|
||||
// Asset Date and Time
|
||||
SheetTile(
|
||||
title: _getDateTime(context, asset, exifInfo),
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
||||
),
|
||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner),
|
||||
const SheetPeopleDetails(),
|
||||
const SheetLocationDetails(),
|
||||
// Details header
|
||||
SheetTile(
|
||||
title: 'details'.t(context: context),
|
||||
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
// File info
|
||||
buildFileInfoTile(),
|
||||
// Camera info
|
||||
if (cameraTitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: cameraTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
// Lens info
|
||||
if (lensTitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: lensTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
// Rating bar
|
||||
if (isRatingEnabled) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
'rating'.t(context: context),
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
RatingBar(
|
||||
initialRating: exifInfo?.rating?.toDouble() ?? 0,
|
||||
filledColor: context.themeData.colorScheme.primary,
|
||||
unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100),
|
||||
itemSize: 40,
|
||||
onRatingUpdate: (rating) async {
|
||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
|
||||
},
|
||||
onClearRating: () async {
|
||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, 0);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 60),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -408,14 +407,9 @@ class _DragHandle extends StatelessWidget {
|
||||
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
container: true,
|
||||
button: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: math.max(handleSize.height, kMinInteractiveDimension),
|
||||
transform: Matrix4.translationValues(0, -kMinInteractiveDimension, 0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: handleSize.height,
|
||||
|
||||
@@ -111,7 +111,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
late final int heroOffset;
|
||||
late PhotoViewControllerValue initialPhotoViewState;
|
||||
bool? hasDraggedDown;
|
||||
bool isSnapping = false;
|
||||
bool blockGestures = false;
|
||||
bool dragInProgress = false;
|
||||
bool shouldPopOnDrag = false;
|
||||
@@ -133,7 +132,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
double _assetDetailsOpacity = 0.0;
|
||||
|
||||
final _assetDetailsSnap = 5;
|
||||
// final _assetDetailsSnap = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -278,12 +277,29 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
viewController = controller;
|
||||
}
|
||||
|
||||
bool onScrollNotification(ScrollNotification notification) {
|
||||
if (notification is ScrollStartNotification) {
|
||||
// Drag started
|
||||
print('Scroll/drag started');
|
||||
// final dragDetails = notification.dragDetails;
|
||||
// _onDragStart(_, notification.dragDetails, controller, scaleStateController)
|
||||
} else if (notification is ScrollUpdateNotification) {
|
||||
// Drag is ongoing
|
||||
print('Scroll offset: ${notification.metrics.pixels}');
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
// Drag ended
|
||||
print('Scroll/drag ended');
|
||||
}
|
||||
return false; // return false to allow the notification to continue propagating
|
||||
}
|
||||
|
||||
void _onDragStart(
|
||||
_,
|
||||
DragStartDetails details,
|
||||
PhotoViewControllerBase controller,
|
||||
PhotoViewScaleStateController scaleStateController,
|
||||
) {
|
||||
print("photoview drag start");
|
||||
viewController = controller;
|
||||
dragDownPosition = details.localPosition;
|
||||
initialPhotoViewState = controller.value;
|
||||
@@ -560,6 +576,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
}
|
||||
});
|
||||
|
||||
// debugPaintSizeEnabled = true;
|
||||
|
||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||
@@ -599,41 +617,54 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
|
||||
// Calculate padding to center the image in the viewport
|
||||
final topPadding = math.max((viewportHeight - imageHeight) / 2, 0.0);
|
||||
final snapOffset = math.min(topPadding + (imageHeight / 2), (viewportHeight / 3) * 2);
|
||||
final snapOffset = math.max(topPadding + (imageHeight / 2), viewportHeight / 3 * 2);
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: VariableHeightSnappingPhysics(snapStart: 0, snapEnd: snapOffset, snapOffset: snapOffset),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: topPadding),
|
||||
SizedBox(
|
||||
height: imageHeight,
|
||||
child: PhotoViewGallery.builder(
|
||||
gaplessPlayback: true,
|
||||
loadingBuilder: _placeholderBuilder,
|
||||
pageController: pageController,
|
||||
scrollPhysics: CurrentPlatform.isIOS
|
||||
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
||||
itemCount: totalAssets,
|
||||
onPageChanged: _onPageChanged,
|
||||
scaleStateChangedCallback: _onScaleStateChanged,
|
||||
builder: _assetBuilder,
|
||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||
enablePanAlways: true,
|
||||
NotificationListener<ScrollNotification>(
|
||||
onNotification: onScrollNotification,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: VariableHeightSnappingPhysics(snapStart: 0, snapEnd: snapOffset, snapOffset: snapOffset),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Container(
|
||||
// height: topPadding,
|
||||
// decoration: const BoxDecoration(color: Colors.green),
|
||||
// ),
|
||||
SizedOverflowBox(
|
||||
size: Size(double.infinity, topPadding + imageHeight - (kMinInteractiveDimension / 2)),
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: viewportHeight,
|
||||
child: PhotoViewGallery.builder(
|
||||
gaplessPlayback: true,
|
||||
loadingBuilder: _placeholderBuilder,
|
||||
pageController: pageController,
|
||||
scrollPhysics: CurrentPlatform.isIOS
|
||||
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
||||
itemCount: totalAssets,
|
||||
onPageChanged: _onPageChanged,
|
||||
scaleStateChangedCallback: _onScaleStateChanged,
|
||||
builder: _assetBuilder,
|
||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||
enablePanAlways: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _assetDetailsOpacity,
|
||||
duration: kThemeAnimationDuration,
|
||||
child: AssetDetails(minHeight: viewportHeight / 3 * 2),
|
||||
),
|
||||
],
|
||||
|
||||
// TODO: if zooming, this should be hidden, and we should
|
||||
// probably disable the scroll physics
|
||||
AnimatedOpacity(
|
||||
opacity: _assetDetailsOpacity,
|
||||
duration: kThemeAnimationDuration,
|
||||
child: AssetDetails(minHeight: snapOffset),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -679,6 +710,14 @@ class VariableHeightSnappingPhysics extends ScrollPhysics {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) {
|
||||
if (value < position.pixels && position.pixels <= position.minScrollExtent) {
|
||||
return 0.0;
|
||||
}
|
||||
return super.applyBoundaryConditions(position, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
|
||||
final tolerance = toleranceFor(position);
|
||||
|
||||
Reference in New Issue
Block a user