This commit is contained in:
@@ -2903,63 +2903,242 @@ class _VoiceStep extends StatelessWidget {
|
||||
final tokens = context.mapflowTokens;
|
||||
final canFinish = canContinue;
|
||||
|
||||
return Column(
|
||||
return Stack(
|
||||
children: [
|
||||
if (placeName.trim().isNotEmpty) ...[
|
||||
Text(
|
||||
placeName.trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final hint in promptHints)
|
||||
Chip(
|
||||
label: Text(hint),
|
||||
side: BorderSide(color: tokens.voiceAccent),
|
||||
backgroundColor: tokens.voiceAccent,
|
||||
labelStyle: TextStyle(
|
||||
color: tokens.onVoiceControl,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
if (placeName.trim().isNotEmpty) ...[
|
||||
Text(
|
||||
placeName.trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _VoiceProgressGrid(progress: informationProgress)),
|
||||
if (!micAllowed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Icon(
|
||||
Icons.mic_off_outlined,
|
||||
color: tokens.voiceAccentSoft,
|
||||
size: 22,
|
||||
Expanded(child: _VoiceProgressGrid(progress: informationProgress)),
|
||||
if (!micAllowed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Icon(
|
||||
Icons.mic_off_outlined,
|
||||
color: tokens.voiceAccentSoft,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
_VoiceRecordButton(
|
||||
progress: informationProgress,
|
||||
liveLevel: liveLevel,
|
||||
isRecording: isRecording,
|
||||
canFinish: canFinish,
|
||||
enabled: hasTelegramAuth && !isSubmitting,
|
||||
onPressed: canFinish ? onNext : onToggleRecording,
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned.fill(
|
||||
bottom: 230,
|
||||
child: _FloatingPromptHints(
|
||||
hints: promptHints,
|
||||
active: isRecording || informationProgress < 1,
|
||||
),
|
||||
_VoiceRecordButton(
|
||||
progress: informationProgress,
|
||||
liveLevel: liveLevel,
|
||||
isRecording: isRecording,
|
||||
canFinish: canFinish,
|
||||
enabled: hasTelegramAuth && !isSubmitting,
|
||||
onPressed: canFinish ? onNext : onToggleRecording,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FloatingPromptHints extends StatefulWidget {
|
||||
const _FloatingPromptHints({required this.hints, required this.active});
|
||||
|
||||
final List<String> hints;
|
||||
final bool active;
|
||||
|
||||
@override
|
||||
State<_FloatingPromptHints> createState() => _FloatingPromptHintsState();
|
||||
}
|
||||
|
||||
class _FloatingPromptHintsState extends State<_FloatingPromptHints>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 13500),
|
||||
);
|
||||
_syncAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _FloatingPromptHints oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_syncAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncAnimation() {
|
||||
if (widget.active && !_controller.isAnimating) {
|
||||
_controller.repeat();
|
||||
}
|
||||
if (!widget.active && _controller.isAnimating) {
|
||||
_controller.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.hints.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 260),
|
||||
opacity: widget.active ? 1 : 0,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
for (var index = 0; index < widget.hints.length; index++)
|
||||
_FloatingPromptHint(
|
||||
label: widget.hints[index],
|
||||
index: index,
|
||||
progress: _controller.value,
|
||||
size: constraints.biggest,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FloatingPromptHint extends StatelessWidget {
|
||||
const _FloatingPromptHint({
|
||||
required this.label,
|
||||
required this.index,
|
||||
required this.progress,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int index;
|
||||
final double progress;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.mapflowTokens;
|
||||
final phase = (progress + index * 0.173) % 1.0;
|
||||
final eased = Curves.easeInOutCubic.transform(phase);
|
||||
final visibility = _visibilityForPhase(phase);
|
||||
final seed = index + 1.0;
|
||||
final start = _alignment(seed, 0);
|
||||
final end = _alignment(seed, 1);
|
||||
final alignment = Alignment(
|
||||
_lerpDouble(start.x, end.x, eased),
|
||||
_lerpDouble(start.y, end.y, eased),
|
||||
);
|
||||
final drift = Offset(
|
||||
math.sin((phase * math.pi * 2) + seed) * size.width * 0.035,
|
||||
math.cos((phase * math.pi * 2) + seed * 1.9) * size.height * 0.026,
|
||||
);
|
||||
final scale =
|
||||
0.76 +
|
||||
visibility * 0.28 +
|
||||
math.sin((phase * math.pi * 2) + seed * 2.4) * 0.035;
|
||||
final rotation = math.sin((phase * math.pi * 2) + seed) * 0.035;
|
||||
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: Transform.translate(
|
||||
offset: drift,
|
||||
child: Transform.rotate(
|
||||
angle: rotation,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: Opacity(
|
||||
opacity: visibility,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.voiceAccent.withValues(alpha: 0.88),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: tokens.onDark.withValues(alpha: 0.16),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: tokens.voiceAccent.withValues(alpha: 0.30),
|
||||
blurRadius: 26,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 13,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: tokens.onVoiceControl,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _visibilityForPhase(double phase) {
|
||||
if (phase < 0.16) {
|
||||
return Curves.easeOutCubic.transform(phase / 0.16);
|
||||
}
|
||||
if (phase > 0.70) {
|
||||
return Curves.easeInCubic.transform((1 - phase) / 0.30);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
Alignment _alignment(double seed, int salt) {
|
||||
final xSeed = math.sin(seed * 19.73 + salt * 41.11) * 10000;
|
||||
final ySeed = math.sin(seed * 47.31 + salt * 17.17) * 10000;
|
||||
final x = ((xSeed - xSeed.floor()) * 1.76 - 0.88).clamp(-0.88, 0.88);
|
||||
final y = ((ySeed - ySeed.floor()) * 1.58 - 0.79).clamp(-0.79, 0.79);
|
||||
return Alignment(x, y);
|
||||
}
|
||||
|
||||
double _lerpDouble(double a, double b, double t) => a + (b - a) * t;
|
||||
}
|
||||
|
||||
class _VoiceProgressGrid extends StatelessWidget {
|
||||
const _VoiceProgressGrid({required this.progress});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user