cyberhybridhub/lib/widgets/swipe_question_tile.dart

199 lines
7.0 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../guid/guid_glyph_shape.dart';
import '../theme/app_theme.dart';
/// Swipeable question card: right submits, left defers (does not resolve).
///
/// The center band supports vertical drag as a slider from -10 (down) to 10 (up).
class SwipeQuestionTile extends StatefulWidget {
const SwipeQuestionTile({
super.key,
required this.questionId,
required this.onSwipeRight,
required this.onSwipeLeft,
this.busy = false,
});
final String questionId;
final Future<void> Function(num answer) onSwipeRight;
final Future<void> Function() onSwipeLeft;
final bool busy;
@override
State<SwipeQuestionTile> createState() => _SwipeQuestionTileState();
}
class _SwipeQuestionTileState extends State<SwipeQuestionTile> {
double _dragOffset = 0;
double _verticalOffset = 0;
bool _acting = false;
static const double _swipeThreshold = 96;
static const double _maxVerticalDrag = 120;
static const double _sliderMin = -10;
static const double _sliderMax = 10;
/// Swipe up → +10, swipe down → -10 (linear between).
double get _sliderValue =>
(_verticalOffset / _maxVerticalDrag * _sliderMax).clamp(_sliderMin, _sliderMax);
Future<void> _releaseDrag() async {
if (_acting || widget.busy) {
setState(() => _dragOffset = 0);
return;
}
if (_dragOffset > _swipeThreshold) {
setState(() {
_acting = true;
_dragOffset = MediaQuery.sizeOf(context).width;
});
await widget.onSwipeRight(_sliderValue);
} else if (_dragOffset < -_swipeThreshold) {
setState(() {
_acting = true;
_dragOffset = -MediaQuery.sizeOf(context).width;
});
await widget.onSwipeLeft();
} else if (mounted) {
setState(() => _dragOffset = 0);
}
if (mounted) {
setState(() {
_acting = false;
_dragOffset = 0;
});
}
}
@override
Widget build(BuildContext context) {
final double width = MediaQuery.sizeOf(context).width;
final double progress = (_dragOffset / _swipeThreshold).clamp(-1.0, 1.0);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
Colors.redAccent.withValues(
alpha: 0.15 + 0.35 * (-progress).clamp(0.0, 1.0),
),
AppColors.surfaceElevated,
AppColors.success.withValues(
alpha: 0.15 + 0.35 * progress.clamp(0.0, 1.0),
),
],
),
),
),
),
Transform.translate(
offset: Offset(_dragOffset, 0),
child: GestureDetector(
onHorizontalDragUpdate: widget.busy || _acting
? null
: (DragUpdateDetails details) {
setState(() {
_dragOffset += details.delta.dx;
_dragOffset = _dragOffset.clamp(-width * 0.55, width * 0.55);
});
},
onHorizontalDragEnd: widget.busy || _acting
? null
: (_) => unawaited(_releaseDrag()),
child: Material(
color: AppColors.surfaceElevated,
elevation: 4,
shadowColor: Colors.black45,
borderRadius: BorderRadius.circular(16),
child: Container(
width: constraints.maxWidth,
constraints: const BoxConstraints(minHeight: 220),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 24,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
top: 20,
bottom: 20,
left: constraints.maxWidth * 0.22,
right: constraints.maxWidth * 0.22,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: Color.lerp(
AppColors.surfaceElevated,
AppColors.surface,
0.45,
),
),
),
),
Positioned(
top: 24,
bottom: 24,
left: constraints.maxWidth * 0.22,
right: constraints.maxWidth * 0.22,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onVerticalDragUpdate: widget.busy || _acting
? null
: (DragUpdateDetails details) {
setState(() {
_verticalOffset -= details.delta.dy;
_verticalOffset = _verticalOffset.clamp(
-_maxVerticalDrag,
_maxVerticalDrag,
);
});
},
child: Center(
child: Transform.translate(
offset: Offset(0, -_verticalOffset),
child: QuestionGuidGlyph(
guid: widget.questionId,
displayValue: _sliderValue,
),
),
),
),
),
],
),
),
),
),
),
if (widget.busy)
const Positioned.fill(
child: ColoredBox(
color: Color(0x66000000),
child: Center(child: CircularProgressIndicator()),
),
),
],
);
},
);
}
}