feat(sheets): position desktop sheets near cursor

This commit is contained in:
edde746
2026-05-11 05:07:00 +02:00
parent 27006cfa99
commit 4cdcd2f2e4
3 changed files with 99 additions and 27 deletions
+6 -4
View File
@@ -594,7 +594,6 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
final isNumeric = mediaClient?.capabilities.numericUserRating ?? true;
final hasRating = metadata.userRating != null && metadata.userRating! > 0;
final starValue = hasRating ? metadata.userRating! / 2.0 : 0.0;
final activate = isNumeric ? () => _showRatingDialog(metadata, starValue) : () => _toggleLike(metadata);
final iconData = isNumeric ? Symbols.star_rounded : Symbols.thumb_up_rounded;
final activeIconColor = isNumeric ? Colors.amber : Colors.teal;
@@ -606,6 +605,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
return ListenableBuilder(
listenable: _ratingChipFocusNode,
builder: (context, _) {
final activate = isNumeric
? () => _showRatingDialog(context, metadata, starValue)
: () => _toggleLike(metadata);
final colorScheme = Theme.of(context).colorScheme;
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
final showFocus = _ratingChipFocusNode.hasFocus && isKeyboardMode;
@@ -680,9 +682,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
}
}
void _showRatingDialog(MediaItem metadata, double currentStarValue) {
showModalBottomSheet(
context: context,
void _showRatingDialog(BuildContext sheetContext, MediaItem metadata, double currentStarValue) {
OverlaySheetController.showAdaptive(
sheetContext,
builder: (context) => RatingBottomSheet(
currentRating: currentStarValue,
onRate: (stars) async {
@@ -19,6 +19,7 @@ import '../../utils/snackbar_helper.dart';
import '../../widgets/dialog_action_button.dart';
import '../../utils/video_player_navigation.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import '../../widgets/overlay_sheet.dart';
import '../models/watch_session.dart';
import '../providers/watch_together_provider.dart';
import '../services/recent_rooms_service.dart';
@@ -402,8 +403,8 @@ class _RecentRoomTile extends StatelessWidget {
}
void _showActions(BuildContext context) {
showModalBottomSheet(
context: context,
OverlaySheetController.showAdaptive(
context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -412,7 +413,7 @@ class _RecentRoomTile extends StatelessWidget {
leading: const Icon(Symbols.edit_rounded),
title: Text(t.watchTogether.renameRoom),
onTap: () {
Navigator.pop(context);
OverlaySheetController.closeAdaptive(context);
onRename();
},
),
@@ -420,7 +421,7 @@ class _RecentRoomTile extends StatelessWidget {
leading: Icon(Symbols.delete_rounded, color: Theme.of(context).colorScheme.error),
title: Text(t.watchTogether.removeRoom, style: TextStyle(color: Theme.of(context).colorScheme.error)),
onTap: () {
Navigator.pop(context);
OverlaySheetController.closeAdaptive(context);
onRemove();
},
),
+70 -1
View File
@@ -214,6 +214,8 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
BoxConstraints? _constraints;
Color? _explicitBackgroundColor;
Alignment _alignment = Alignment.bottomCenter;
Offset? _lastPointerPosition;
double? _sheetHorizontalAnchor;
// Drag-to-dismiss state
double _dragOffset = 0;
@@ -274,6 +276,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
final completer = Completer<T?>();
final entry = _OverlaySheetEntry(builder: builder, completer: completer, initialFocusNode: initialFocusNode);
final horizontalAnchor = _resolveSheetHorizontalAnchor(alignment);
setState(() {
_pageStack.add(entry);
@@ -284,6 +287,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
_constraints = constraints;
_explicitBackgroundColor = backgroundColor;
_alignment = alignment;
_sheetHorizontalAnchor = horizontalAnchor;
_dragOffset = 0;
_isDragging = false;
});
@@ -345,6 +349,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
_isClosing = false;
_dragOffset = 0;
_isDragging = false;
_sheetHorizontalAnchor = null;
});
// Clear stale back-key flags. handleBackKeyAction sets
// markClosedViaBackKey() expecting a route pop, but the overlay
@@ -355,6 +360,18 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
});
}
void _rememberPointerPosition(PointerEvent event) {
if (event.kind != PointerDeviceKind.mouse) return;
_lastPointerPosition = event.localPosition;
}
double? _resolveSheetHorizontalAnchor(Alignment alignment) {
if (!PlatformDetector.isDesktopOS() || PlatformDetector.isTV()) return null;
if (InputModeTracker.isKeyboardMode(context)) return null;
if (alignment.x != 0 || alignment.y <= 0) return null;
return _lastPointerPosition?.dx;
}
void _autoFocus() {
if (!InputModeTracker.isKeyboardMode(context)) return;
@@ -458,6 +475,10 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
return _OverlaySheetScope(
controller: _controller,
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: _rememberPointerPosition,
onPointerHover: _rememberPointerPosition,
child: Stack(
children: [
widget.child,
@@ -478,6 +499,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
],
],
),
),
);
}
@@ -586,8 +608,12 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
canRequestFocus: false,
skipTraversal: true,
onKeyEvent: _handleKeyEvent,
child: Align(
child: CustomSingleChildLayout(
delegate: _OverlaySheetLayoutDelegate(
alignment: _alignment,
horizontalAnchor: _sheetHorizontalAnchor,
edgePadding: isDesktop ? _OverlaySheetLayoutDelegate.desktopEdgePadding : 0,
),
child: AnimatedBuilder(
animation: _slideCurve,
builder: (context, child) {
@@ -654,3 +680,46 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
return sheet;
}
}
class _OverlaySheetLayoutDelegate extends SingleChildLayoutDelegate {
static const desktopEdgePadding = 16.0;
final Alignment alignment;
final double? horizontalAnchor;
final double edgePadding;
const _OverlaySheetLayoutDelegate({
required this.alignment,
required this.horizontalAnchor,
required this.edgePadding,
});
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
final size = constraints.biggest;
final maxWidth = size.width > edgePadding * 2 ? size.width - edgePadding * 2 : size.width;
return BoxConstraints.loose(Size(maxWidth, size.height));
}
@override
Offset getPositionForChild(Size size, Size childSize) {
final hasHorizontalPadding = edgePadding > 0 && size.width > childSize.width + edgePadding * 2;
final minLeft = hasHorizontalPadding ? edgePadding : 0.0;
final maxLeft = hasHorizontalPadding ? size.width - childSize.width - edgePadding : minLeft;
final left = horizontalAnchor == null
? minLeft + (maxLeft - minLeft) * (alignment.x + 1) / 2
: (horizontalAnchor! - childSize.width / 2).clamp(minLeft, maxLeft).toDouble();
final maxTop = size.height > childSize.height ? size.height - childSize.height : 0.0;
final top = maxTop * (alignment.y + 1) / 2;
return Offset(left, top);
}
@override
bool shouldRelayout(_OverlaySheetLayoutDelegate oldDelegate) {
return alignment != oldDelegate.alignment ||
horizontalAnchor != oldDelegate.horizontalAnchor ||
edgePadding != oldDelegate.edgePadding;
}
}