Polish voice recording screen
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m27s
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m27s
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:latlong2/latlong.dart' hide Path;
|
||||
import 'package:record/record.dart';
|
||||
import 'package:siri_wave/siri_wave.dart';
|
||||
|
||||
import '../api/mapflow_api.dart';
|
||||
import '../auth/telegram_login_button.dart';
|
||||
@@ -887,8 +888,12 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
),
|
||||
};
|
||||
|
||||
final isVoiceStep = _step == 1;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F3EA),
|
||||
backgroundColor: isVoiceStep
|
||||
? const Color(0xFF05030B)
|
||||
: const Color(0xFFF7F3EA),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
@@ -902,6 +907,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
_StoryProgress(
|
||||
step: _step,
|
||||
total: 3,
|
||||
dark: isVoiceStep,
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
@@ -1091,6 +1097,15 @@ class _VoiceStep extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
time,
|
||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: _VoiceWave(
|
||||
@@ -1103,7 +1118,11 @@ class _VoiceStep extends StatelessWidget {
|
||||
if (!micAllowed)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 10),
|
||||
child: Icon(Icons.mic_off_outlined, size: 22),
|
||||
child: Icon(
|
||||
Icons.mic_off_outlined,
|
||||
color: Color(0xFFFF7A90),
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
@@ -1112,6 +1131,10 @@ class _VoiceStep extends StatelessWidget {
|
||||
key: const ValueKey('next'),
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: const Color(0xFF090613),
|
||||
),
|
||||
onPressed: isSubmitting ? null : onNext,
|
||||
child: Text(isSubmitting ? 'Отправляем' : 'Далее'),
|
||||
),
|
||||
@@ -1130,7 +1153,7 @@ class _VoiceStep extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _VoiceRecordButton extends StatelessWidget {
|
||||
class _VoiceRecordButton extends StatefulWidget {
|
||||
const _VoiceRecordButton({
|
||||
required this.time,
|
||||
required this.progress,
|
||||
@@ -1146,57 +1169,117 @@ class _VoiceRecordButton extends StatelessWidget {
|
||||
final Future<void> Function() onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
State<_VoiceRecordButton> createState() => _VoiceRecordButtonState();
|
||||
}
|
||||
|
||||
class _VoiceRecordButtonState extends State<_VoiceRecordButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _pulseController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
);
|
||||
_syncPulse();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _VoiceRecordButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_syncPulse();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncPulse() {
|
||||
if (widget.isRecording && !_pulseController.isAnimating) {
|
||||
_pulseController.repeat();
|
||||
}
|
||||
if (!widget.isRecording && _pulseController.isAnimating) {
|
||||
_pulseController.stop();
|
||||
_pulseController.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 148,
|
||||
height: 148,
|
||||
child: Stack(
|
||||
width: 164,
|
||||
height: 164,
|
||||
child: AnimatedBuilder(
|
||||
animation: _pulseController,
|
||||
builder: (context, child) {
|
||||
final pulse = widget.isRecording ? _pulseController.value : 0.0;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
for (final offset in const [0.0, 0.32])
|
||||
Transform.scale(
|
||||
scale: 1 + ((pulse + offset) % 1) * 0.28,
|
||||
child: Container(
|
||||
width: 130,
|
||||
height: 130,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFF2D75).withValues(
|
||||
alpha: widget.isRecording
|
||||
? (0.22 * (1 - ((pulse + offset) % 1)))
|
||||
: 0,
|
||||
),
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 148,
|
||||
height: 148,
|
||||
width: 154,
|
||||
height: 154,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 8,
|
||||
value: widget.progress,
|
||||
strokeWidth: 7,
|
||||
strokeCap: StrokeCap.round,
|
||||
backgroundColor: colorScheme.primary.withValues(alpha: 0.14),
|
||||
color: const Color(0xFFFF2D75),
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 118,
|
||||
height: 118,
|
||||
child!,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: FilledButton(
|
||||
onPressed: enabled ? () => onPressed() : null,
|
||||
onPressed: widget.enabled ? () => widget.onPressed() : null,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: const Color(0xFF090613),
|
||||
disabledBackgroundColor: Colors.white.withValues(alpha: 0.28),
|
||||
disabledForegroundColor: Colors.white.withValues(alpha: 0.52),
|
||||
shape: const CircleBorder(),
|
||||
padding: EdgeInsets.zero,
|
||||
elevation: 0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(isRecording ? Icons.stop : Icons.mic, size: 42),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
time,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Icon(
|
||||
widget.isRecording ? Icons.pause_rounded : Icons.mic_rounded,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VoiceWave extends StatelessWidget {
|
||||
class _VoiceWave extends StatefulWidget {
|
||||
const _VoiceWave({
|
||||
required this.samples,
|
||||
required this.active,
|
||||
@@ -1208,105 +1291,84 @@ class _VoiceWave extends StatelessWidget {
|
||||
final double progress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _VoiceWavePainter(
|
||||
samples: samples,
|
||||
active: active,
|
||||
progress: progress,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
State<_VoiceWave> createState() => _VoiceWaveState();
|
||||
}
|
||||
|
||||
class _VoiceWavePainter extends CustomPainter {
|
||||
const _VoiceWavePainter({
|
||||
required this.samples,
|
||||
required this.active,
|
||||
required this.progress,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final List<double> samples;
|
||||
final bool active;
|
||||
final double progress;
|
||||
final Color color;
|
||||
class _VoiceWaveState extends State<_VoiceWave> {
|
||||
late final IOS9SiriWaveformController _controller;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final centerY = size.height / 2;
|
||||
final quietScale = active ? 1.0 : 0.42 + progress * 0.32;
|
||||
final top = <Offset>[];
|
||||
final bottom = <Offset>[];
|
||||
|
||||
for (var index = 0; index < samples.length; index++) {
|
||||
final x = samples.length == 1
|
||||
? 0.0
|
||||
: index / (samples.length - 1) * size.width;
|
||||
final envelope = math.sin(index / (samples.length - 1) * math.pi);
|
||||
final amplitude =
|
||||
samples[index] * quietScale * envelope * size.height * 0.46;
|
||||
final yTop = centerY - amplitude;
|
||||
final yBottom = centerY + amplitude;
|
||||
|
||||
top.add(Offset(x, yTop));
|
||||
bottom.add(Offset(x, yBottom));
|
||||
}
|
||||
|
||||
final topPath = Path()..addPolygon(top, false);
|
||||
final bottomPath = Path()..addPolygon(bottom, false);
|
||||
final shape = Path()
|
||||
..addPolygon([...top, ...bottom.reversed], true)
|
||||
..close();
|
||||
|
||||
final glow = Paint()
|
||||
..color = color.withValues(alpha: active ? 0.18 : 0.08)
|
||||
..strokeWidth = 18
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
|
||||
final stroke = Paint()
|
||||
..shader = LinearGradient(
|
||||
colors: [
|
||||
color.withValues(alpha: active ? 0.35 : 0.16),
|
||||
color.withValues(alpha: active ? 0.95 : 0.42),
|
||||
const Color(0xFFE11D48).withValues(alpha: active ? 0.86 : 0.34),
|
||||
],
|
||||
).createShader(Offset.zero & size)
|
||||
..strokeWidth = 5
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
final fill = Paint()
|
||||
..shader = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
color.withValues(alpha: active ? 0.20 : 0.08),
|
||||
const Color(0xFFE11D48).withValues(alpha: active ? 0.14 : 0.05),
|
||||
],
|
||||
).createShader(Offset.zero & size)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawPath(shape, fill);
|
||||
canvas.drawPath(topPath, glow);
|
||||
canvas.drawPath(bottomPath, glow);
|
||||
canvas.drawPath(topPath, stroke);
|
||||
canvas.drawPath(bottomPath, stroke);
|
||||
canvas.drawCircle(
|
||||
Offset(size.width / 2, centerY),
|
||||
active ? 3.4 : 2.2,
|
||||
Paint()..color = color.withValues(alpha: active ? 0.82 : 0.36),
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = IOS9SiriWaveformController(
|
||||
amplitude: 0.12,
|
||||
speed: 0.08,
|
||||
color1: const Color(0xFFFF2D75),
|
||||
color2: const Color(0xFF38F5D3),
|
||||
color3: const Color(0xFF7C5CFF),
|
||||
);
|
||||
_syncController();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _VoiceWavePainter oldDelegate) {
|
||||
return true;
|
||||
void didUpdateWidget(covariant _VoiceWave oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_syncController();
|
||||
}
|
||||
|
||||
void _syncController() {
|
||||
final lastLevel = widget.samples.isEmpty ? 0.04 : widget.samples.last;
|
||||
_controller.amplitude = widget.active
|
||||
? (0.18 + lastLevel * 0.78).clamp(0.0, 1.0)
|
||||
: (0.10 + widget.progress * 0.12).clamp(0.0, 1.0);
|
||||
_controller.speed = widget.active
|
||||
? (0.10 + lastLevel * 0.36).clamp(0.0, 1.0)
|
||||
: 0.035;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFF0C0718),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(
|
||||
0xFFFF2D75,
|
||||
).withValues(alpha: widget.active ? 0.18 : 0.08),
|
||||
blurRadius: 90,
|
||||
spreadRadius: 18,
|
||||
),
|
||||
BoxShadow(
|
||||
color: const Color(
|
||||
0xFF38F5D3,
|
||||
).withValues(alpha: widget.active ? 0.12 : 0.05),
|
||||
blurRadius: 120,
|
||||
spreadRadius: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SiriWaveform.ios9(
|
||||
controller: _controller,
|
||||
options: IOS9SiriWaveformOptions(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxHeight.clamp(280.0, 520.0),
|
||||
showSupportBar: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1331,11 +1393,13 @@ class _StoryProgress extends StatelessWidget {
|
||||
const _StoryProgress({
|
||||
required this.step,
|
||||
required this.total,
|
||||
required this.dark,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
final int step;
|
||||
final int total;
|
||||
final bool dark;
|
||||
final VoidCallback onClose;
|
||||
|
||||
@override
|
||||
@@ -1352,8 +1416,12 @@ class _StoryProgress extends StatelessWidget {
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: index <= step
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: const Color(0xFFE0D8CA),
|
||||
? (dark
|
||||
? const Color(0xFFFF2D75)
|
||||
: Theme.of(context).colorScheme.primary)
|
||||
: (dark
|
||||
? Colors.white.withValues(alpha: 0.16)
|
||||
: const Color(0xFFE0D8CA)),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
@@ -1366,7 +1434,7 @@ class _StoryProgress extends StatelessWidget {
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
onPressed: onClose,
|
||||
icon: const Icon(Icons.close),
|
||||
icon: Icon(Icons.close, color: dark ? Colors.white : null),
|
||||
tooltip: 'Закрыть',
|
||||
),
|
||||
],
|
||||
|
||||
42
pubspec.lock
42
pubspec.lock
@@ -53,10 +53,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -368,14 +368,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
latlong2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -428,18 +420,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -744,6 +736,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
siri_wave:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: siri_wave
|
||||
sha256: ea815d6627dc297f6be883bb0dd7a579a5f5f9729242d47c10e95850cccf169a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -817,26 +817,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.26.3"
|
||||
version: "1.30.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.10"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.12"
|
||||
version: "0.6.16"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -958,5 +958,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.7 <4.0.0"
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
@@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.7
|
||||
sdk: ^3.11.0
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@@ -41,6 +41,7 @@ dependencies:
|
||||
web: ^1.1.1
|
||||
geolocator: ^14.0.2
|
||||
record: ^6.2.0
|
||||
siri_wave: ^2.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user