This commit is contained in:
@@ -2903,63 +2903,242 @@ class _VoiceStep extends StatelessWidget {
|
|||||||
final tokens = context.mapflowTokens;
|
final tokens = context.mapflowTokens;
|
||||||
final canFinish = canContinue;
|
final canFinish = canContinue;
|
||||||
|
|
||||||
return Column(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
if (placeName.trim().isNotEmpty) ...[
|
Column(
|
||||||
Text(
|
children: [
|
||||||
placeName.trim(),
|
if (placeName.trim().isNotEmpty) ...[
|
||||||
maxLines: 1,
|
Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
placeName.trim(),
|
||||||
textAlign: TextAlign.center,
|
maxLines: 1,
|
||||||
style: Theme.of(
|
overflow: TextOverflow.ellipsis,
|
||||||
context,
|
textAlign: TextAlign.center,
|
||||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
|
style: Theme.of(
|
||||||
),
|
context,
|
||||||
const SizedBox(height: 10),
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
|
||||||
],
|
),
|
||||||
Padding(
|
const SizedBox(height: 10),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
Expanded(child: _VoiceProgressGrid(progress: informationProgress)),
|
||||||
),
|
if (!micAllowed)
|
||||||
Expanded(child: _VoiceProgressGrid(progress: informationProgress)),
|
Padding(
|
||||||
if (!micAllowed)
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
Padding(
|
child: Icon(
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
Icons.mic_off_outlined,
|
||||||
child: Icon(
|
color: tokens.voiceAccentSoft,
|
||||||
Icons.mic_off_outlined,
|
size: 22,
|
||||||
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 {
|
class _VoiceProgressGrid extends StatelessWidget {
|
||||||
const _VoiceProgressGrid({required this.progress});
|
const _VoiceProgressGrid({required this.progress});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user