diff --git a/app/lib/presentation/widgets/speech_to_text_button.dart b/app/lib/presentation/widgets/speech_to_text_button.dart index 0ba0e3a5..79baea8d 100644 --- a/app/lib/presentation/widgets/speech_to_text_button.dart +++ b/app/lib/presentation/widgets/speech_to_text_button.dart @@ -17,31 +17,60 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:speech_to_text_google_dialog/speech_to_text_google_dialog.dart'; +import 'package:speech_to_text_dialog/speech_to_text_dialog.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; -class SpeechToTextButton extends StatelessWidget { - final Function(dynamic data) onTextReceived; +class SpeechToTextButton extends StatefulWidget { + final Function(String text) onTextReceived; const SpeechToTextButton({required this.onTextReceived, super.key}); + @override + State createState() => _SpeechToTextButtonState(); +} + +class _SpeechToTextButtonState extends State { + final _speechDialog = SpeechToTextDialog(); + StreamSubscription? _textSubscription; + + @override + void dispose() { + _textSubscription?.cancel().ignore(); + _speechDialog.dispose(); + super.dispose(); + } + + Future _showSpeechDialog(BuildContext context) async { + // Cancel any existing subscription + await _textSubscription?.cancel(); + + // Listen for the next text result + _textSubscription = _speechDialog.textStream.take(1).listen((text) { + if (text.isNotEmpty) { + widget.onTextReceived(text); + } + }); + + // Show the dialog + final isServiceAvailable = await _speechDialog.showDialog( + // locale: "en-US", + ); + + if (!isServiceAvailable) { + if (context.mounted) { + ui_helper.showErrorMessage(context, 'Service is not available'); + } + await _textSubscription?.cancel(); + } + } + @override Widget build(BuildContext context) { return IconButton( - onPressed: () async { - final isServiceAvailable = await SpeechToTextGoogleDialog.getInstance() - .showGoogleDialog( - onTextReceived: onTextReceived, - // locale: "en-US", - ); - - if (!isServiceAvailable) { - if (context.mounted) { - ui_helper.showErrorMessage(context, 'Service is not available'); - } - } - }, + onPressed: () => _showSpeechDialog(context), icon: const Icon(Icons.mic), ); } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index d4c54f8b..118a4d22 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -77,9 +77,8 @@ dependencies: sliver_tools: ^0.2.12 smooth_page_indicator: ^2.0.1 socks5_proxy: ^2.1.1 - speech_to_text_google_dialog: - git: - url: https://github.com/FaFre/speech_to_text_google_dialog.git + speech_to_text_dialog: + path: ../packages/speech_to_text_dialog sqlite3: ^2.9.4 sqlite3_flutter_libs: ^0.5.41 synchronized: ^3.4.0 diff --git a/lib/src/pigeons/speech_to_text.g.dart b/lib/src/pigeons/speech_to_text.g.dart new file mode 100644 index 00000000..124a8aee --- /dev/null +++ b/lib/src/pigeons/speech_to_text.g.dart @@ -0,0 +1,137 @@ +// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Host API - methods called from Flutter to native Android. +class SpeechToTextApi { + /// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Show the speech recognition dialog. + /// + /// Returns [true] if the dialog was shown successfully. + /// Returns [false] if the speech recognition service is not available. + /// + /// The [locale] parameter specifies the language locale for recognition + /// (e.g., 'en-US', 'de-DE'). If null, uses the device default. + Future showDialog({String? locale}) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([locale]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} + +/// Flutter API - callbacks from native Android to Flutter. +abstract class SpeechToTextEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when speech recognition completes with the recognized text. + /// + /// [text] contains the recognized speech text. May be empty if + /// recognition failed or was cancelled. + void onTextReceived(String text); + + static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.'); + final List args = (message as List?)!; + final String? arg_text = (args[0] as String?); + assert(arg_text != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.'); + try { + api.onTextReceived(arg_text!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/speech_to_text_dialog/.gitignore b/packages/speech_to_text_dialog/.gitignore new file mode 100644 index 00000000..b9d7f25b --- /dev/null +++ b/packages/speech_to_text_dialog/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/packages/speech_to_text_dialog/.metadata b/packages/speech_to_text_dialog/.metadata new file mode 100644 index 00000000..7a9751c8 --- /dev/null +++ b/packages/speech_to_text_dialog/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: android + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/speech_to_text_dialog/CHANGELOG.md b/packages/speech_to_text_dialog/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/speech_to_text_dialog/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/speech_to_text_dialog/LICENSE b/packages/speech_to_text_dialog/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/speech_to_text_dialog/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/speech_to_text_dialog/README.md b/packages/speech_to_text_dialog/README.md new file mode 100644 index 00000000..2e2ccf12 --- /dev/null +++ b/packages/speech_to_text_dialog/README.md @@ -0,0 +1,15 @@ +# speech_to_text_dialog + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/packages/speech_to_text_dialog/analysis_options.yaml b/packages/speech_to_text_dialog/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/speech_to_text_dialog/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/speech_to_text_dialog/android/.gitignore b/packages/speech_to_text_dialog/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/speech_to_text_dialog/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/speech_to_text_dialog/android/build.gradle b/packages/speech_to_text_dialog/android/build.gradle new file mode 100644 index 00000000..651a1583 --- /dev/null +++ b/packages/speech_to_text_dialog/android/build.gradle @@ -0,0 +1,66 @@ +group = "eu.weblibre.speech_to_text_dialog" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.2.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.11.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "eu.weblibre.speech_to_text_dialog" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/speech_to_text_dialog/android/settings.gradle b/packages/speech_to_text_dialog/android/settings.gradle new file mode 100644 index 00000000..21ea7340 --- /dev/null +++ b/packages/speech_to_text_dialog/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'speech_to_text_dialog' diff --git a/packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml b/packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..38d63fa9 --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt new file mode 100644 index 00000000..2f9c795a --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package eu.weblibre.speech_to_text_dialog + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.speech.RecognizerIntent +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.PluginRegistry +import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextApi as PigeonSpeechToTextApi +import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextEvents +import java.util.Locale + +/// Plugin implementation for speech recognition dialog. +/// +/// This plugin integrates Android's RecognizerIntent to provide +/// speech-to-text functionality to Flutter apps. +class SpeechToTextDialogPlugin : FlutterPlugin, ActivityAware, + PluginRegistry.ActivityResultListener { + + companion object { + private const val REQ_CODE_SPEECH_INPUT = 120752 + } + + private var activity: Activity? = null + private var speechApiHost: SpeechApiHost? = null + + override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + speechApiHost = SpeechApiHost(binding.binaryMessenger, this) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + speechApiHost?.release() + speechApiHost = null + } + + override fun onAttachedToActivity(@NonNull binding: ActivityPluginBinding) { + activity = binding.activity + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null + } + + override fun onReattachedToActivityForConfigChanges(@NonNull binding: ActivityPluginBinding) { + activity = binding.activity + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivity() { + activity = null + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (requestCode == REQ_CODE_SPEECH_INPUT) { + speechApiHost?.handleActivityResult(resultCode, data) + return true + } + return false + } + + /// Show the speech recognition dialog. + /// + /// @param locale The locale string (e.g., "en-US", "de-DE") or null for device default. + /// @return true if the dialog was shown successfully, false otherwise. + fun showSpeechDialog(locale: String?): Boolean { + val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM + ) + putExtra( + RecognizerIntent.EXTRA_LANGUAGE, + locale?.let { Locale(it) } ?: Locale.getDefault() + ) + putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak") + } + + return try { + activity?.startActivityForResult(intent, REQ_CODE_SPEECH_INPUT) + true + } catch (e: ActivityNotFoundException) { + false + } + } +} + +/// Host API implementation that handles Flutter-to-Android calls. +class SpeechApiHost( + private val messenger: BinaryMessenger, + private val plugin: SpeechToTextDialogPlugin +) { + private val events: SpeechToTextEvents = SpeechToTextEvents(messenger) + + init { + // Set up the API handler + PigeonSpeechToTextApi.setUp(messenger, object : PigeonSpeechToTextApi { + override fun showDialog(locale: String?): Boolean { + return plugin.showSpeechDialog(locale) + } + }) + } + + /// Handle the result from the speech recognition activity. + fun handleActivityResult(resultCode: Int, data: Intent?) { + sendResult(resultCode, data) + } + + private fun sendResult(resultCode: Int, data: Intent?) { + val text = if (resultCode == Activity.RESULT_OK && data != null) { + val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + if (!result.isNullOrEmpty()) { + result[0] + } else { + "" + } + } else { + // User cancelled or recognition failed + "" + } + + events.onTextReceived(text) { } + } + + fun release() { + PigeonSpeechToTextApi.setUp(messenger, null) + } +} diff --git a/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt new file mode 100644 index 00000000..a00fa699 --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt @@ -0,0 +1,143 @@ +// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package eu.weblibre.speech_to_text_dialog.pigeons + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object SpeechToTextPigeonUtils { + + fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() +private open class SpeechToTextPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return super.readValueOfType(type, buffer) + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + super.writeValue(stream, value) + } +} + +/** + * Host API - methods called from Flutter to native Android. + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ +interface SpeechToTextApi { + /** + * Show the speech recognition dialog. + * + * Returns [true] if the dialog was shown successfully. + * Returns [false] if the speech recognition service is not available. + * + * The [locale] parameter specifies the language locale for recognition + * (e.g., 'en-US', 'de-DE'). If null, uses the device default. + */ + fun showDialog(locale: String?): Boolean + + companion object { + /** The codec used by SpeechToTextApi. */ + val codec: MessageCodec by lazy { + SpeechToTextPigeonCodec() + } + /** Sets up an instance of `SpeechToTextApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: SpeechToTextApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val localeArg = args[0] as String? + val wrapped: List = try { + listOf(api.showDialog(localeArg)) + } catch (exception: Throwable) { + SpeechToTextPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** + * Flutter API - callbacks from native Android to Flutter. + * + * Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. + */ +class SpeechToTextEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by SpeechToTextEvents. */ + val codec: MessageCodec by lazy { + SpeechToTextPigeonCodec() + } + } + /** + * Called when speech recognition completes with the recognized text. + * + * [text] contains the recognized speech text. May be empty if + * recognition failed or was cancelled. + */ + fun onTextReceived(textArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(textArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(SpeechToTextPigeonUtils.createConnectionError(channelName))) + } + } + } +} diff --git a/packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt b/packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt new file mode 100644 index 00000000..56cbc61b --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt @@ -0,0 +1,27 @@ +package eu.weblibre.speech_to_text_dialog + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.mockito.Mockito +import kotlin.test.Test + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class SpeechToTextDialogPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = SpeechToTextDialogPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/packages/speech_to_text_dialog/example/.gitignore b/packages/speech_to_text_dialog/example/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/packages/speech_to_text_dialog/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/speech_to_text_dialog/example/README.md b/packages/speech_to_text_dialog/example/README.md new file mode 100644 index 00000000..429d20f5 --- /dev/null +++ b/packages/speech_to_text_dialog/example/README.md @@ -0,0 +1,16 @@ +# speech_to_text_dialog_example + +Demonstrates how to use the speech_to_text_dialog plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/speech_to_text_dialog/example/analysis_options.yaml b/packages/speech_to_text_dialog/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/speech_to_text_dialog/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/speech_to_text_dialog/example/android/.gitignore b/packages/speech_to_text_dialog/example/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/speech_to_text_dialog/example/android/app/build.gradle.kts b/packages/speech_to_text_dialog/example/android/app/build.gradle.kts new file mode 100644 index 00000000..a47fd1bf --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "eu.weblibre.speech_to_text_dialog_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "eu.weblibre.speech_to_text_dialog_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml b/packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml b/packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8e082513 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt b/packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt new file mode 100644 index 00000000..754d90e1 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt @@ -0,0 +1,5 @@ +package eu.weblibre.speech_to_text_dialog_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml b/packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/speech_to_text_dialog/example/android/build.gradle.kts b/packages/speech_to_text_dialog/example/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/speech_to_text_dialog/example/android/gradle.properties b/packages/speech_to_text_dialog/example/android/gradle.properties new file mode 100644 index 00000000..fbee1d8c --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e4ef43fb --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/speech_to_text_dialog/example/android/settings.gradle.kts b/packages/speech_to_text_dialog/example/android/settings.gradle.kts new file mode 100644 index 00000000..ca7fe065 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart b/packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart new file mode 100644 index 00000000..6ebb6c10 --- /dev/null +++ b/packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://flutter.dev/to/integration-testing + + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:speech_to_text_dialog/speech_to_text_dialog.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('showDialog returns bool', (WidgetTester tester) async { + final SpeechToTextDialog plugin = SpeechToTextDialog(); + final bool result = await plugin.showDialog(); + // The result indicates if the dialog was shown successfully. + // In a test environment without proper activity, this may return false. + expect(result, isA()); + plugin.dispose(); + }); + + testWidgets('textStream emits String values', (WidgetTester tester) async { + final SpeechToTextDialog plugin = SpeechToTextDialog(); + final Stream stream = plugin.textStream; + expect(stream, isA>()); + plugin.dispose(); + }); +} diff --git a/packages/speech_to_text_dialog/example/lib/main.dart b/packages/speech_to_text_dialog/example/lib/main.dart new file mode 100644 index 00000000..efa4c7e7 --- /dev/null +++ b/packages/speech_to_text_dialog/example/lib/main.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:speech_to_text_dialog/speech_to_text_dialog.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _speechToTextDialog = SpeechToTextDialog(); + StreamSubscription? _textSubscription; + String _recognizedText = 'Tap the button to start speaking...'; + String _selectedLocale = 'en-US'; + + final List _locales = [ + 'en-US', + 'de-DE', + 'es-ES', + 'fr-FR', + 'it-IT', + 'pt-BR', + 'ru-RU', + 'zh-CN', + 'ja-JP', + 'ko-KR', + ]; + + @override + void initState() { + super.initState(); + // Listen for recognized text + _textSubscription = _speechToTextDialog.textStream.listen((text) { + setState(() { + if (text.isEmpty) { + _recognizedText = 'No speech detected or cancelled'; + } else { + _recognizedText = text; + } + }); + }); + } + + @override + void dispose() { + _textSubscription?.cancel(); + _speechToTextDialog.dispose(); + super.dispose(); + } + + Future _startListening() async { + setState(() { + _recognizedText = 'Listening...'; + }); + + final success = await _speechToTextDialog.showDialog( + locale: _selectedLocale, + ); + + if (!success) { + setState(() { + _recognizedText = 'Speech recognition not available'; + }); + } + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Speech to Text Dialog'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.mic, + size: 80, + color: Colors.blue, + ), + const SizedBox(height: 24), + Text( + _recognizedText, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + DropdownButton( + value: _selectedLocale, + hint: const Text('Select Language'), + items: _locales.map((String locale) { + return DropdownMenuItem( + value: locale, + child: Text(locale), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedLocale = newValue; + }); + } + }, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _startListening, + icon: const Icon(Icons.mic), + label: const Text('Start Speech Recognition'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/speech_to_text_dialog/example/pubspec.yaml b/packages/speech_to_text_dialog/example/pubspec.yaml new file mode 100644 index 00000000..3da990ec --- /dev/null +++ b/packages/speech_to_text_dialog/example/pubspec.yaml @@ -0,0 +1,86 @@ +name: speech_to_text_dialog_example +description: "Demonstrates how to use the speech_to_text_dialog plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.10.4 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + speech_to_text_dialog: + # When depending on this package from a real application you should use: + # speech_to_text_dialog: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/speech_to_text_dialog/example/test/widget_test.dart b/packages/speech_to_text_dialog/example/test/widget_test.dart new file mode 100644 index 00000000..d72f4d27 --- /dev/null +++ b/packages/speech_to_text_dialog/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:speech_to_text_dialog_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart b/packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart new file mode 100644 index 00000000..c29f1e23 --- /dev/null +++ b/packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:async'; +import 'package:flutter/services.dart' show BinaryMessenger; +import 'src/pigeons/speech_to_text.g.dart'; + +/// Speech recognition dialog using Android's RecognizerIntent. +/// +/// This class provides a Flutter interface to Android's speech recognition +/// dialog, allowing users to dictate text that is then returned to the app. +/// +/// Example usage: +/// ```dart +/// final speechDialog = SpeechToTextDialog(); +/// +/// // Listen for results +/// final subscription = speechDialog.textStream.listen((text) { +/// print('Recognized: $text'); +/// }); +/// +/// // Show the dialog +/// final success = await speechDialog.showDialog(locale: 'en-US'); +/// +/// // Clean up when done +/// await subscription.cancel(); +/// speechDialog.dispose(); +/// ``` +class SpeechToTextDialog implements SpeechToTextEvents { + /// Creates a new [SpeechToTextDialog] instance. + /// + /// Optionally provide a custom [api] for testing or dependency injection. + /// If [binaryMessenger] is provided, it will be used for Pigeon communication. + SpeechToTextDialog({ + SpeechToTextApi? api, + BinaryMessenger? binaryMessenger, + }) : _api = api ?? SpeechToTextApi(binaryMessenger: binaryMessenger), + _binaryMessenger = binaryMessenger { + // Set up the event handler + SpeechToTextEvents.setUp(this, binaryMessenger: binaryMessenger); + } + + final SpeechToTextApi _api; + final BinaryMessenger? _binaryMessenger; + final _textStreamController = StreamController.broadcast(); + bool _disposed = false; + + /// Stream of recognized speech text. + /// + /// This stream emits the recognized text when the user completes + /// speech input. An empty string may be emitted if recognition failed + /// or the user cancelled. + Stream get textStream => _textStreamController.stream; + + /// Show the speech recognition dialog. + /// + /// Returns [true] if the dialog was shown successfully. + /// Returns [false] if the speech recognition service is not available. + /// + /// The [locale] parameter specifies the language locale for recognition + /// (e.g., 'en-US', 'de-DE', 'es-ES'). If null, uses the device default locale. + /// + /// Example: + /// ```dart + /// final success = await speechDialog.showDialog(locale: 'en-US'); + /// if (!success) { + /// print('Speech recognition not available'); + /// } + /// ``` + Future showDialog({String? locale}) async { + return _api.showDialog(locale: locale); + } + + @override + void onTextReceived(String text) { + if (!_disposed) { + _textStreamController.add(text); + } + } + + /// Release resources used by this instance. + /// + /// Call this when the speech dialog is no longer needed to avoid memory leaks. + /// After calling [dispose], this instance cannot be used again. + void dispose() { + if (!_disposed) { + _disposed = true; + SpeechToTextEvents.setUp(null, binaryMessenger: _binaryMessenger); + _textStreamController.close(); + } + } +} diff --git a/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart b/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart new file mode 100644 index 00000000..124a8aee --- /dev/null +++ b/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart @@ -0,0 +1,137 @@ +// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Host API - methods called from Flutter to native Android. +class SpeechToTextApi { + /// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Show the speech recognition dialog. + /// + /// Returns [true] if the dialog was shown successfully. + /// Returns [false] if the speech recognition service is not available. + /// + /// The [locale] parameter specifies the language locale for recognition + /// (e.g., 'en-US', 'de-DE'). If null, uses the device default. + Future showDialog({String? locale}) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([locale]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} + +/// Flutter API - callbacks from native Android to Flutter. +abstract class SpeechToTextEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when speech recognition completes with the recognized text. + /// + /// [text] contains the recognized speech text. May be empty if + /// recognition failed or was cancelled. + void onTextReceived(String text); + + static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.'); + final List args = (message as List?)!; + final String? arg_text = (args[0] as String?); + assert(arg_text != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.'); + try { + api.onTextReceived(arg_text!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/speech_to_text_dialog/pigeons/speech_to_text.dart b/packages/speech_to_text_dialog/pigeons/speech_to_text.dart new file mode 100644 index 00000000..245254e7 --- /dev/null +++ b/packages/speech_to_text_dialog/pigeons/speech_to_text.dart @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:pigeon/pigeon.dart'; + +/// Pigeon API for speech recognition dialog. +/// +/// This API provides type-safe communication between Flutter and Android +/// for showing the Google speech recognition dialog. +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/pigeons/speech_to_text.g.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt', + kotlinOptions: KotlinOptions( + package: 'eu.weblibre.speech_to_text_dialog.pigeons', + ), + dartPackageName: 'speech_to_text_dialog', + ), +) + +/// Host API - methods called from Flutter to native Android. +@HostApi() +abstract class SpeechToTextApi { + /// Show the speech recognition dialog. + /// + /// Returns [true] if the dialog was shown successfully. + /// Returns [false] if the speech recognition service is not available. + /// + /// The [locale] parameter specifies the language locale for recognition + /// (e.g., 'en-US', 'de-DE'). If null, uses the device default. + bool showDialog({String? locale}); +} + +/// Flutter API - callbacks from native Android to Flutter. +@FlutterApi() +abstract class SpeechToTextEvents { + /// Called when speech recognition completes with the recognized text. + /// + /// [text] contains the recognized speech text. May be empty if + /// recognition failed or was cancelled. + void onTextReceived(String text); +} diff --git a/packages/speech_to_text_dialog/pubspec.yaml b/packages/speech_to_text_dialog/pubspec.yaml new file mode 100644 index 00000000..d2463b22 --- /dev/null +++ b/packages/speech_to_text_dialog/pubspec.yaml @@ -0,0 +1,26 @@ +name: speech_to_text_dialog +description: Speech recognition dialog using Android's RecognizerIntent with type-safe Pigeon channels. +version: 0.0.1 +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.10.4 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + pigeon: ^26.1.5 + +flutter: + plugin: + platforms: + android: + package: eu.weblibre.speech_to_text_dialog + pluginClass: SpeechToTextDialogPlugin diff --git a/pubspec.yaml b/pubspec.yaml index ef4cd98e..d0b48117 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,8 @@ workspace: - packages/simple_intent_receiver/example - packages/locale_resolver - packages/locale_resolver/example + - packages/speech_to_text_dialog + - packages/speech_to_text_dialog/example dev_dependencies: melos: ^7.3.0