From 8d285f0a732c256c768f22e2982cabdd5b6ee68e Mon Sep 17 00:00:00 2001 From: Matthias Nehlsen Date: Sun, 17 Nov 2024 18:14:26 +0100 Subject: [PATCH] feat: move list of transcripts to extended entry actions & improve --- .../entry_details/entry_detail_header.dart | 2 + .../speech/ui/widgets/audio_player.dart | 129 ---------- .../speech/ui/widgets/speech_modal.dart | 236 ++++++++++++++++++ lib/l10n/app_en.arb | 1 + pubspec.yaml | 2 +- 5 files changed, 240 insertions(+), 130 deletions(-) create mode 100644 lib/features/speech/ui/widgets/speech_modal.dart diff --git a/lib/features/journal/ui/widgets/entry_details/entry_detail_header.dart b/lib/features/journal/ui/widgets/entry_details/entry_detail_header.dart index 35c00eb95..4b26f4f40 100644 --- a/lib/features/journal/ui/widgets/entry_details/entry_detail_header.dart +++ b/lib/features/journal/ui/widgets/entry_details/entry_detail_header.dart @@ -8,6 +8,7 @@ import 'package:lotti/features/journal/ui/widgets/entry_details/delete_icon_widg import 'package:lotti/features/journal/ui/widgets/entry_details/save_button.dart'; import 'package:lotti/features/journal/ui/widgets/entry_details/share_button_widget.dart'; import 'package:lotti/features/journal/ui/widgets/tags/tag_add.dart'; +import 'package:lotti/features/speech/ui/widgets/speech_modal.dart'; import 'package:lotti/get_it.dart'; import 'package:lotti/l10n/app_localizations_context.dart'; import 'package:lotti/services/link_service.dart'; @@ -211,6 +212,7 @@ class ExtendedHeaderActions { entryId: entryId, beamBack: !inLinkedEntries, ), + SpeechModalListTile(entryId: entryId), ShareButtonListTile(entryId: entryId), TagAddListTile(entryId: entryId), ListTile( diff --git a/lib/features/speech/ui/widgets/audio_player.dart b/lib/features/speech/ui/widgets/audio_player.dart index 62e031e98..1a8a7c163 100644 --- a/lib/features/speech/ui/widgets/audio_player.dart +++ b/lib/features/speech/ui/widgets/audio_player.dart @@ -6,15 +6,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lotti/classes/journal_entities.dart'; import 'package:lotti/features/journal/state/entry_controller.dart'; -import 'package:lotti/features/journal/util/entry_tools.dart'; import 'package:lotti/features/speech/state/asr_service.dart'; import 'package:lotti/features/speech/state/player_cubit.dart'; import 'package:lotti/features/speech/state/player_state.dart'; import 'package:lotti/features/speech/ui/widgets/transcription_progress_modal.dart'; import 'package:lotti/get_it.dart'; -import 'package:lotti/logic/persistence_logic.dart'; import 'package:lotti/themes/theme.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class AudioPlayerWidget extends ConsumerWidget { const AudioPlayerWidget(this.journalAudio, {super.key}); @@ -50,7 +47,6 @@ class AudioPlayerWidget extends ConsumerWidget { builder: (BuildContext context, AudioPlayerState state) { final isActive = state.audioNote?.meta.id == journalAudio.meta.id; final cubit = context.read(); - final transcripts = journalAudio.data.transcripts; return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -127,14 +123,6 @@ class AudioPlayerWidget extends ConsumerWidget { ..emitState(); }, ), - if (transcripts?.isNotEmpty ?? false) - IconButton( - icon: const Icon(Icons.list), - iconSize: 20, - tooltip: 'Show Transcriptions', - color: context.colorScheme.outline, - onPressed: cubit.toggleTranscriptsList, - ), ], ), Row( @@ -159,126 +147,9 @@ class AudioPlayerWidget extends ConsumerWidget { ), ], ), - if ((transcripts?.isNotEmpty ?? false) && state.showTranscriptsList) - Column( - children: [ - const SizedBox(height: 10), - ...transcripts!.map( - (transcript) => TranscriptListItem( - transcript, - entryId: journalAudio.meta.id, - ), - ), - ], - ), ], ); }, ); } } - -class TranscriptListItem extends StatefulWidget { - const TranscriptListItem( - this.transcript, { - required this.entryId, - super.key, - }); - - final String entryId; - final AudioTranscript transcript; - - @override - State createState() => _TranscriptListItemState(); -} - -class _TranscriptListItemState extends State { - bool show = false; - - void toggleShow() { - setState(() { - show = !show; - }); - } - - @override - Widget build(BuildContext context) { - return Card( - color: context.colorScheme.primaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15, - ), - child: Column( - children: [ - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: toggleShow, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - dfShorter.format(widget.transcript.created), - style: transcriptHeaderStyle, - ), - const SizedBox(width: 10), - Text( - formatSeconds(widget.transcript.processingTime), - style: transcriptHeaderStyle, - ), - const SizedBox(width: 10), - Text( - 'Lang: ${widget.transcript.detectedLanguage}', - style: transcriptHeaderStyle, - ), - const SizedBox(width: 10), - Text( - '${widget.transcript.library}, ' - ' ${widget.transcript.model}', - style: transcriptHeaderStyle, - ), - const SizedBox(width: 10), - Opacity( - opacity: show ? 1 : 0, - child: IconButton( - onPressed: () { - getIt().removeAudioTranscript( - journalEntityId: widget.entryId, - transcript: widget.transcript, - ); - }, - icon: Icon( - MdiIcons.trashCanOutline, - size: fontSizeMedium, - ), - ), - ), - if (show) - const Icon( - Icons.keyboard_double_arrow_up_outlined, - size: fontSizeMedium, - ) - else - const Icon( - Icons.keyboard_double_arrow_down_outlined, - size: fontSizeMedium, - ), - ], - ), - ), - ), - if (show) - Padding( - padding: const EdgeInsets.only(bottom: 10), - child: SelectableText( - widget.transcript.transcript, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/speech/ui/widgets/speech_modal.dart b/lib/features/speech/ui/widgets/speech_modal.dart new file mode 100644 index 000000000..6eae45510 --- /dev/null +++ b/lib/features/speech/ui/widgets/speech_modal.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lotti/classes/journal_entities.dart'; +import 'package:lotti/features/journal/state/entry_controller.dart'; +import 'package:lotti/features/journal/util/entry_tools.dart'; +import 'package:lotti/get_it.dart'; +import 'package:lotti/l10n/app_localizations_context.dart'; +import 'package:lotti/logic/persistence_logic.dart'; +import 'package:lotti/themes/theme.dart'; +import 'package:lotti/widgets/misc/wolt_modal_config.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; + +class SpeechModal { + static SliverWoltModalSheetPage page1({ + required BuildContext context, + required TextTheme textTheme, + required String entryId, + }) { + return WoltModalSheetPage( + hasSabGradient: false, + topBarTitle: Text( + context.messages.speechModalTitle, + style: textTheme.titleLarge, + ), + isTopBarLayerAlwaysVisible: true, + child: Padding( + padding: const EdgeInsets.all(WoltModalConfig.pagePadding).copyWith( + top: 0, + ), + child: Column( + children: [ + TranscriptsList(entryId: entryId), + ], + ), + ), + ); + } + + static Future show({ + required BuildContext context, + required String entryId, + }) async { + await WoltModalSheet.show( + context: context, + pageListBuilder: (modalSheetContext) { + final textTheme = context.textTheme; + return [ + page1( + context: modalSheetContext, + textTheme: textTheme, + entryId: entryId, + ), + ]; + }, + modalTypeBuilder: (context) { + final size = MediaQuery.of(context).size.width; + if (size < WoltModalConfig.pageBreakpoint) { + return WoltModalType.bottomSheet(); + } else { + return WoltModalType.dialog(); + } + }, + onModalDismissedWithBarrierTap: () { + Navigator.of(context).pop(); + }, + ); + } +} + +class SpeechModalListTile extends ConsumerWidget { + const SpeechModalListTile({ + required this.entryId, + super.key, + }); + + final String entryId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = entryControllerProvider(id: entryId); + final entryState = ref.watch(provider).value; + + final item = entryState?.entry; + if (item == null || item is! JournalAudio) { + return const SizedBox.shrink(); + } + + void onTapAdd() { + SpeechModal.show( + context: context, + entryId: entryId, + ); + } + + return ListTile( + leading: const Icon(Icons.transcribe_rounded), + title: Text(context.messages.speechModalTitle), + onTap: onTapAdd, + ); + } +} + +class TranscriptsList extends ConsumerWidget { + const TranscriptsList({ + required this.entryId, + super.key, + }); + + final String entryId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = entryControllerProvider(id: entryId); + final entryState = ref.watch(provider).value; + + final item = entryState?.entry; + if (item == null || item is! JournalAudio) { + return const SizedBox.shrink(); + } + + final transcripts = item.data.transcripts; + + return Column( + children: [ + const SizedBox(height: 10), + ...?transcripts?.map( + (transcript) => TranscriptListItem( + transcript, + entryId: item.meta.id, + ), + ), + ], + ); + } +} + +class TranscriptListItem extends StatefulWidget { + const TranscriptListItem( + this.transcript, { + required this.entryId, + super.key, + }); + + final String entryId; + final AudioTranscript transcript; + + @override + State createState() => _TranscriptListItemState(); +} + +class _TranscriptListItemState extends State { + final ExpansionTileController _controller = ExpansionTileController(); + + bool show = false; + + void toggleShow() { + setState(() { + show = !show; + }); + + if (_controller.isExpanded) { + _controller.collapse(); + } else { + _controller.expand(); + } + } + + @override + Widget build(BuildContext context) { + return ExpansionTile( + controller: _controller, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Opacity( + opacity: show ? 1 : 0, + child: IconButton( + onPressed: () { + getIt().removeAudioTranscript( + journalEntityId: widget.entryId, + transcript: widget.transcript, + ); + }, + icon: Icon( + MdiIcons.trashCanOutline, + size: fontSizeMedium, + ), + ), + ), + IconButton( + onPressed: toggleShow, + icon: Icon( + show + ? Icons.keyboard_double_arrow_up_outlined + : Icons.keyboard_double_arrow_down_outlined, + size: fontSizeMedium, + ), + ), + ], + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + dfShorter.format(widget.transcript.created), + style: transcriptHeaderStyle, + ), + const SizedBox(width: 10), + Text( + formatSeconds(widget.transcript.processingTime), + style: transcriptHeaderStyle, + ), + const SizedBox(width: 10), + Text( + 'Lang: ${widget.transcript.detectedLanguage}', + style: transcriptHeaderStyle, + ), + const SizedBox(width: 10), + Text( + '${widget.transcript.library}, ' + ' ${widget.transcript.model}', + style: transcriptHeaderStyle, + ), + const SizedBox(width: 10), + ], + ), + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: SelectableText(widget.transcript.transcript), + ), + ], + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 71f334740..7c1cb7de1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -299,6 +299,7 @@ "settingsThemingTitle": "Theming", "settingThemingDark": "Dark Theme", "settingThemingLight": "Light Theme", + "speechModalTitle": "Manage Speech Recognition", "syncAssistantHeadline": "Sync Assistant", "syncAssistantPage1": "Let's get the synchronization between Lotti on Desktop and Lotti on your mobile device set up, shall we? You need to start on the desktop side.", "syncAssistantPage2": "The communication between happens without you having to give your data away to cloud-based services. Instead, you provide your own email account and each device in the communication stores encrypted messages for your other devices in an IMAP folder. Please provide your server settings on the next page.", diff --git a/pubspec.yaml b/pubspec.yaml index 30f5075fc..35581ef9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lotti description: Achieve your goals and keep your data private with Lotti. publish_to: 'none' -version: 0.9.531+2729 +version: 0.9.531+2730 msix_config: display_name: LottiApp