// Hangjegyzet widget a MapFeatureSaveSheet-ben: // - Felvétel gomb animált mikrofonnal // - Felvett klipek listája play/pause/delete gombokkal // - Haladásjelző csúszka lejátszás közben import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../models/note_item_audio.dart'; import '../../services/note_audio_service.dart'; class NoteAudioWidget extends StatefulWidget { final int? noteItemId; const NoteAudioWidget({super.key, required this.noteItemId}); @override State createState() => _NoteAudioWidgetState(); } class _NoteAudioWidgetState extends State { List _audios = []; @override void initState() { super.initState(); _load(); } Future _load() async { if (widget.noteItemId == null) return; final list = await NoteAudioService.to.loadAudios(widget.noteItemId!); if (mounted) setState(() => _audios = list); } @override Widget build(BuildContext context) { if (widget.noteItemId == null) { return const _DisabledAudio(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Fejléc sor Row(children: [ Text('Hangjegyzetek', style: TextStyle(fontSize: 13, color: Colors.grey.shade600)), const SizedBox(width: 6), if (_audios.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(10), ), child: Text('${_audios.length}', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600)), ), ]), const SizedBox(height: 10), // Felvétel gomb _RecordButton( noteItemId: widget.noteItemId!, onRecorded: (audio) { setState(() => _audios.add(audio)); }, ), // Felvett klipek listája if (_audios.isNotEmpty) ...[ const SizedBox(height: 10), ..._audios.map((audio) => _AudioClipTile( audio: audio, onDelete: () async { await NoteAudioService.to.deleteAudio(audio); setState(() => _audios.removeWhere((a) => a.id == audio.id)); }, )), ], ], ); } } // ─── Felvétel gomb ─────────────────────────────────────────────────────────── class _RecordButton extends StatelessWidget { final int noteItemId; final ValueChanged onRecorded; const _RecordButton({ required this.noteItemId, required this.onRecorded, }); @override Widget build(BuildContext context) { final svc = NoteAudioService.to; return Obx(() { final isRecording = svc.recordState.value == AudioRecordState.recording; final durationMs = svc.recordDurationMs.value; return Row(children: [ // Mikrofon gomb GestureDetector( onTap: () => isRecording ? _stopRecording(svc) : _startRecording(svc), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 52, height: 52, decoration: BoxDecoration( color: isRecording ? Colors.red : Theme.of(context).colorScheme.primaryContainer, shape: BoxShape.circle, boxShadow: isRecording ? [ BoxShadow( color: Colors.red.withOpacity(0.4), blurRadius: 12, spreadRadius: 2, ) ] : null, ), child: Icon( isRecording ? Icons.stop : Icons.mic, color: isRecording ? Colors.white : Theme.of(context).colorScheme.primary, size: 24, ), ), ), const SizedBox(width: 12), Expanded( child: isRecording // Felvétel közben: időmérő + animált hullám ? Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row(children: [ _PulsingDot(), const SizedBox(width: 8), Text( 'Felvétel: ${svc.formatMs(durationMs)}', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.red, fontSize: 14, ), ), ]), const SizedBox(height: 4), Text( 'Megállításhoz nyomd meg a gombot', style: TextStyle(fontSize: 11, color: Colors.grey.shade500), ), ], ) // Alap állapot : Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text('Hangjegyzet rögzítése', style: TextStyle( fontWeight: FontWeight.w500, fontSize: 14)), Text('Nyomd meg a mikrofon gombot', style: TextStyle( fontSize: 11, color: Colors.grey.shade500)), ], ), ), // Mégse — csak felvétel közben if (isRecording) TextButton( onPressed: () async { await svc.cancelRecording(); }, child: const Text('Mégse', style: TextStyle(color: Colors.grey)), ), ]); }); } Future _startRecording(NoteAudioService svc) async { await svc.startRecording(noteItemId); } Future _stopRecording(NoteAudioService svc) async { final audio = await svc.stopRecording(noteItemId); if (audio != null) onRecorded(audio); } } // ─── Egy felvett klip sor ──────────────────────────────────────────────────── class _AudioClipTile extends StatelessWidget { final NoteItemAudio audio; final VoidCallback onDelete; const _AudioClipTile({ required this.audio, required this.onDelete, }); @override Widget build(BuildContext context) { final svc = NoteAudioService.to; return Obx(() { final isThisPlaying = svc.playingAudioId.value == audio.id; final pState = svc.playState.value; final posMs = svc.playPositionMs.value; final totalMs = audio.durationSeconds * 1000; // Haladás 0.0 – 1.0 final progress = (isThisPlaying && totalMs > 0) ? (posMs / totalMs).clamp(0.0, 1.0) : 0.0; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: isThisPlaying ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.4) : Colors.grey.shade100, borderRadius: BorderRadius.circular(10), border: isThisPlaying ? Border.all( color: Theme.of(context).colorScheme.primary.withOpacity(0.4)) : null, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row(children: [ // Play / Pause gomb GestureDetector( onTap: () => svc.playAudio(audio), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, ), child: Icon( isThisPlaying && pState == AudioPlayState.playing ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 20, ), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Felirat vagy dátum if (audio.caption.isNotEmpty) Text(audio.caption, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis) else Text( _formatDate(audio.createdAt), style: TextStyle( fontSize: 12, color: Colors.grey.shade600), ), const SizedBox(height: 4), // Haladásjelző csúszka ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: isThisPlaying && totalMs > 0 ? (posMs / totalMs).clamp(0.0, 1.0) : 0.0, minHeight: 3, backgroundColor: Colors.grey.shade300, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ), const SizedBox(height: 2), // Időtartam Text( isThisPlaying ? '${svc.formatMs(posMs)} / ' '${audio.durationFormatted}' : audio.durationFormatted, style: TextStyle( fontSize: 10, color: Colors.grey.shade500, fontFeatures: const [FontFeature.tabularFigures()]), ), ], ), ), const SizedBox(width: 4), // Menü: felirat / törlés PopupMenuButton<_AudioAction>( icon: Icon(Icons.more_vert, size: 18, color: Colors.grey.shade500), onSelected: (a) => _onAction(a, context), itemBuilder: (_) => [ const PopupMenuItem( value: _AudioAction.caption, child: ListTile( leading: Icon(Icons.edit_outlined), title: Text('Felirat'), dense: true, ), ), const PopupMenuDivider(), const PopupMenuItem( value: _AudioAction.delete, child: ListTile( leading: Icon(Icons.delete_outline, color: Colors.red), title: Text('Törlés', style: TextStyle(color: Colors.red)), dense: true, ), ), ], ), ]), ], ), ); }); } void _onAction(_AudioAction action, BuildContext context) { switch (action) { case _AudioAction.caption: _editCaption(); case _AudioAction.delete: _confirmDelete(); } } Future _editCaption() async { final ctrl = TextEditingController(text: audio.caption); await Get.dialog(AlertDialog( title: const Text('Felirat'), content: TextField( controller: ctrl, decoration: const InputDecoration(hintText: 'Hangjegyzet leírása...'), autofocus: true, ), actions: [ TextButton(onPressed: Get.back, child: const Text('Mégse')), FilledButton( onPressed: () async { Get.back(); await NoteAudioService.to.updateCaption(audio, ctrl.text.trim()); }, child: const Text('Mentés'), ), ], )); } Future _confirmDelete() async { final ok = await Get.dialog(AlertDialog( title: const Text('Hangjegyzet törlése'), content: const Text('Ez a felvétel véglegesen törlődik.'), actions: [ TextButton(onPressed: Get.back, child: const Text('Mégse')), FilledButton( style: FilledButton.styleFrom(backgroundColor: Colors.red), onPressed: () => Get.back(result: true), child: const Text('Törlés'), ), ], )); if (ok == true) onDelete(); } String _formatDate(DateTime dt) => '${dt.year}.${dt.month.toString().padLeft(2, '0')}.' '${dt.day.toString().padLeft(2, '0')} ' '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}'; } // ─── Segéd widgetek ────────────────────────────────────────────────────────── class _DisabledAudio extends StatelessWidget { const _DisabledAudio(); @override Widget build(BuildContext context) => Container( height: 50, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Text('Mentés után adható hangjegyzet', style: TextStyle(fontSize: 12, color: Colors.grey.shade500)), ); } class _PulsingDot extends StatefulWidget { @override State<_PulsingDot> createState() => _PulsingDotState(); } class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin { late AnimationController _ctrl; late Animation _anim; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 600)) ..repeat(reverse: true); _anim = Tween(begin: 0.3, end: 1.0).animate(_ctrl); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) => AnimatedBuilder( animation: _anim, builder: (_, __) => Container( width: 8, height: 8, decoration: BoxDecoration( color: Colors.red.withOpacity(_anim.value), shape: BoxShape.circle, ), ), ); } enum _AudioAction { caption, delete }