Replace chat search with voice filter recommendations
Build and deploy Flutter Web / build (push) Successful in 3m11s
Build and deploy Flutter Web / build (push) Successful in 3m11s
This commit is contained in:
@@ -125,12 +125,12 @@ class _MapContent extends StatelessWidget {
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: _MapSearchButton(
|
||||
query: state.searchQuery,
|
||||
onSearch: placeCubit.searchPlaces,
|
||||
onClear: state.searchQuery.isEmpty
|
||||
child: _VoiceFilterButton(
|
||||
transcript: state.voiceFilterTranscript,
|
||||
onApply: placeCubit.recommendPlacesByVoice,
|
||||
onClear: state.voiceFilterTranscript.isEmpty
|
||||
? null
|
||||
: placeCubit.clearSearch,
|
||||
: placeCubit.clearVoiceFilter,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -334,21 +334,25 @@ class _AddReviewButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MapSearchButton extends StatelessWidget {
|
||||
const _MapSearchButton({
|
||||
required this.query,
|
||||
required this.onSearch,
|
||||
class _VoiceFilterButton extends StatelessWidget {
|
||||
const _VoiceFilterButton({
|
||||
required this.transcript,
|
||||
required this.onApply,
|
||||
required this.onClear,
|
||||
});
|
||||
|
||||
final String query;
|
||||
final Future<void> Function(String) onSearch;
|
||||
final String transcript;
|
||||
final Future<void> Function({
|
||||
required String audioContentBase64,
|
||||
required String audioMimeType,
|
||||
})
|
||||
onApply;
|
||||
final Future<void> Function()? onClear;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.mapflowTokens;
|
||||
final label = query.isEmpty ? 'Поиск' : query;
|
||||
final label = transcript.isEmpty ? 'Голосом' : transcript;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 70, right: 70),
|
||||
@@ -364,7 +368,7 @@ class _MapSearchButton extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
const Icon(Icons.auto_awesome_outlined, size: 18),
|
||||
const Icon(Icons.mic_none_rounded, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
@@ -392,40 +396,141 @@ class _MapSearchButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _openSearch(BuildContext context) async {
|
||||
final controller = TextEditingController(text: query);
|
||||
final result = await showDialog<String>(
|
||||
final result = await showDialog<_VoiceFilterAudio>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.fromLTRB(18, 18, 18, 8),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.search,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.auto_awesome_outlined),
|
||||
hintText: 'Что ищем?',
|
||||
),
|
||||
onSubmitted: (value) => Navigator.of(context).pop(value),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text),
|
||||
child: const Text('Найти'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
builder: (_) => const _VoiceFilterDialog(),
|
||||
);
|
||||
controller.dispose();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
await onSearch(result);
|
||||
await onApply(
|
||||
audioContentBase64: result.audioContentBase64,
|
||||
audioMimeType: result.audioMimeType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VoiceFilterAudio {
|
||||
const _VoiceFilterAudio({
|
||||
required this.audioContentBase64,
|
||||
required this.audioMimeType,
|
||||
});
|
||||
|
||||
final String audioContentBase64;
|
||||
final String audioMimeType;
|
||||
}
|
||||
|
||||
class _VoiceFilterDialog extends StatefulWidget {
|
||||
const _VoiceFilterDialog();
|
||||
|
||||
@override
|
||||
State<_VoiceFilterDialog> createState() => _VoiceFilterDialogState();
|
||||
}
|
||||
|
||||
class _VoiceFilterDialogState extends State<_VoiceFilterDialog> {
|
||||
final _waveController = WaveformRecorderController(
|
||||
interval: const Duration(milliseconds: 60),
|
||||
config: const RecordConfig(
|
||||
numChannels: 1,
|
||||
sampleRate: 44100,
|
||||
autoGain: true,
|
||||
echoCancel: true,
|
||||
noiseSuppress: true,
|
||||
),
|
||||
);
|
||||
|
||||
var _recording = false;
|
||||
var _submitting = false;
|
||||
var _hasRecording = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_waveController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _toggleRecording() async {
|
||||
if (_recording) {
|
||||
await _waveController.stopRecording();
|
||||
setState(() {
|
||||
_recording = false;
|
||||
_hasRecording = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await _waveController.startRecording();
|
||||
setState(() => _recording = true);
|
||||
}
|
||||
|
||||
Future<void> _apply() async {
|
||||
if (_recording) {
|
||||
await _waveController.stopRecording();
|
||||
setState(() {
|
||||
_recording = false;
|
||||
_hasRecording = true;
|
||||
});
|
||||
}
|
||||
final file = _waveController.file;
|
||||
if (file == null) {
|
||||
throw StateError('Voice filter recording file is required.');
|
||||
}
|
||||
setState(() => _submitting = true);
|
||||
final bytes = await file.readAsBytes();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(
|
||||
_VoiceFilterAudio(
|
||||
audioContentBase64: base64Encode(bytes),
|
||||
audioMimeType: file.mimeType ?? 'audio/wav',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.fromLTRB(18, 18, 18, 8),
|
||||
content: SizedBox(
|
||||
width: 260,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton.filled(
|
||||
onPressed: _submitting ? null : _toggleRecording,
|
||||
icon: Icon(_recording ? Icons.stop_rounded : Icons.mic_rounded),
|
||||
iconSize: 34,
|
||||
style: IconButton.styleFrom(
|
||||
fixedSize: const Size.square(82),
|
||||
minimumSize: const Size.square(82),
|
||||
),
|
||||
tooltip: _recording ? 'Стоп' : 'Записать',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_recording ? 'Слушаю' : 'Голосовой фильтр',
|
||||
style: const TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _submitting || !_hasRecording ? null : _apply,
|
||||
child: _submitting
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Применить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user