()
23 | val transcriptState = viewModel.transcriptState.collectAsState()
24 | val uiEvent = viewModel.uiEvent.collectAsState()
25 | val scope = rememberCoroutineScope()
26 | val snackbarHostState = remember { SnackbarHostState() }
27 |
28 | LaunchedEffect(uiEvent.value) {
29 | uiEvent.value?.let { event ->
30 | when (event) {
31 | is SpeechToTextViewModel.UiEvent.ShowSnackbar -> {
32 | scope.launch {
33 | snackbarHostState.showSnackbar(event.message)
34 | }
35 | viewModel.onUiEventHandled()
36 | }
37 | }
38 | }
39 | }
40 |
41 | AppTheme(
42 | darkTheme = darkTheme,
43 | dynamicColor = dynamicColor,
44 | ) {
45 | AppContent(
46 | snackbarHostState = snackbarHostState,
47 | transcriptState = transcriptState.value,
48 | onLanguageSelected = viewModel::onLanguageSelected,
49 | onClickMic = viewModel::onClickMic,
50 | onClickCopy = viewModel::onClickCopy,
51 |
52 | permissionDialogEvents = object : PermissionDialogEvents {
53 | override fun onDismissRequest() {
54 | viewModel.onDismissRequest()
55 | }
56 |
57 | override fun onClickGoToSettings() {
58 | viewModel.openAppSettings()
59 | }
60 | }
61 | )
62 | }
63 | }
64 |
65 | interface PermissionDialogEvents {
66 | fun onDismissRequest()
67 | fun onClickGoToSettings()
68 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/AppContent.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import PermissionDialogEvents
4 | import androidx.compose.material3.Scaffold
5 | import androidx.compose.material3.SnackbarHost
6 | import androidx.compose.material3.SnackbarHostState
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import data.ListeningStatus
13 | import data.TranscriptState
14 |
15 | @Composable
16 | fun AppContent(
17 | snackbarHostState: SnackbarHostState,
18 | transcriptState: TranscriptState,
19 | onLanguageSelected: (String) -> Unit,
20 | onClickMic: () -> Unit,
21 | onClickCopy: () -> Unit,
22 | permissionDialogEvents: PermissionDialogEvents
23 | ) {
24 | var showLanguageDialog by remember { mutableStateOf(false) }
25 |
26 | var selectedLanguage by remember {
27 | mutableStateOf(
28 | transcriptState.selectedLanguage
29 | )
30 | }
31 |
32 | if (showLanguageDialog) {
33 | LanguageSelectionDialog(
34 | supportedLanguages = transcriptState.supportedLanguages,
35 | onSelected = {
36 | onLanguageSelected(it)
37 | selectedLanguage = it
38 | showLanguageDialog = false
39 | },
40 | onDismissRequest = {
41 | showLanguageDialog = false
42 | },
43 | selectedLanguage = selectedLanguage
44 | )
45 | }
46 |
47 | if (transcriptState.showPermissionNeedDialog) {
48 | PermissionNeedDialog(
49 | onDismissRequest = {permissionDialogEvents.onDismissRequest()},
50 | onClickGoToSettings = {permissionDialogEvents.onClickGoToSettings()}
51 | )
52 | }
53 |
54 | Scaffold(
55 | snackbarHost = { SnackbarHost(snackbarHostState) },
56 | bottomBar = {
57 | BottomBar(
58 | onClickShowLanguages = {
59 | showLanguageDialog = true
60 | },
61 | onClickMic = {
62 | onClickMic()
63 | },
64 | onClickCopy = {
65 | onClickCopy()
66 | },
67 | isListening = transcriptState.listeningStatus == ListeningStatus.LISTENING
68 | )
69 | }
70 | ) { paddingValues ->
71 | Content(
72 | paddingValues = paddingValues,
73 | transcriptState = transcriptState,
74 | )
75 | }
76 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.2.0"
3 | android-compileSdk = "34"
4 | android-minSdk = "24"
5 | android-targetSdk = "34"
6 | androidx-activityCompose = "1.9.3"
7 | androidx-appcompat = "1.7.0"
8 | androidx-constraintlayout = "2.2.0"
9 | androidx-core-ktx = "1.15.0"
10 | androidx-espresso-core = "3.6.1"
11 | androidx-material = "1.12.0"
12 | androidx-test-junit = "1.2.1"
13 | compose-plugin = "1.6.10"
14 | junit = "4.13.2"
15 | koinCompose = "1.1.2"
16 | koinVersion = "3.5.3"
17 | kotlin = "2.0.0"
18 |
19 | # Koin
20 | koin = "3.6.0-Beta4"
21 | koinComposeMultiplatform = "1.2.0-Beta4"
22 | navigationCompose = "2.8.0-alpha02"
23 | lifecycleViewModel = "2.8.4"
24 |
25 | [libraries]
26 | lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleViewModel"}
27 | koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
28 | koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
29 | koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
30 | koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" }
31 | koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" }
32 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
33 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
34 | junit = { group = "junit", name = "junit", version.ref = "junit" }
35 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
36 | androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
37 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
38 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
39 | androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
40 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
41 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
42 |
43 | [plugins]
44 | androidApplication = { id = "com.android.application", version.ref = "agp" }
45 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
46 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
47 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
48 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
--------------------------------------------------------------------------------
/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 |
5 | plugins {
6 | alias(libs.plugins.kotlinMultiplatform)
7 | alias(libs.plugins.androidApplication)
8 | alias(libs.plugins.jetbrainsCompose)
9 | alias(libs.plugins.compose.compiler)
10 | }
11 |
12 | kotlin {
13 | androidTarget {
14 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
15 | compilerOptions {
16 | jvmTarget.set(JvmTarget.JVM_11)
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach { iosTarget ->
25 | iosTarget.binaries.framework {
26 | baseName = "ComposeApp"
27 | isStatic = true
28 | }
29 | }
30 |
31 | sourceSets {
32 |
33 | androidMain.dependencies {
34 | implementation(compose.preview)
35 | implementation(libs.koin.android)
36 | implementation(libs.koin.androidx.compose)
37 | }
38 | commonMain.dependencies {
39 | implementation(compose.runtime)
40 | implementation(compose.foundation)
41 | implementation(compose.material3)
42 | implementation(compose.ui)
43 | implementation(compose.components.resources)
44 | implementation(compose.components.uiToolingPreview)
45 | implementation(compose.materialIconsExtended)
46 | api(libs.koin.core)
47 | implementation(libs.koin.compose)
48 | implementation(libs.koin.compose.viewmodel)
49 | implementation(libs.lifecycle.viewmodel)
50 | }
51 | iosMain.dependencies {
52 |
53 | }
54 | }
55 | }
56 |
57 | android {
58 | namespace = "org.example.speechtotext"
59 | compileSdk = libs.versions.android.compileSdk.get().toInt()
60 |
61 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
62 | sourceSets["main"].res.srcDirs("src/androidMain/res")
63 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
64 |
65 | defaultConfig {
66 | applicationId = "org.example.speechtotext"
67 | minSdk = libs.versions.android.minSdk.get().toInt()
68 | targetSdk = libs.versions.android.targetSdk.get().toInt()
69 | versionCode = 1
70 | versionName = "1.0"
71 | }
72 | packaging {
73 | resources {
74 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
75 | }
76 | }
77 | buildTypes {
78 | getByName("release") {
79 | isMinifyEnabled = false
80 | }
81 | }
82 | compileOptions {
83 | sourceCompatibility = JavaVersion.VERSION_11
84 | targetCompatibility = JavaVersion.VERSION_11
85 | }
86 | buildFeatures {
87 | compose = true
88 | }
89 | dependencies {
90 | debugImplementation(compose.uiTooling)
91 | }
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/VoiceAnimation.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.animation.animateColor
4 | import androidx.compose.animation.core.FastOutSlowInEasing
5 | import androidx.compose.animation.core.RepeatMode
6 | import androidx.compose.animation.core.animateFloat
7 | import androidx.compose.animation.core.infiniteRepeatable
8 | import androidx.compose.animation.core.rememberInfiniteTransition
9 | import androidx.compose.animation.core.tween
10 | import androidx.compose.foundation.background
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Row
14 | import androidx.compose.foundation.layout.Spacer
15 | import androidx.compose.foundation.layout.fillMaxWidth
16 | import androidx.compose.foundation.layout.height
17 | import androidx.compose.foundation.layout.width
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.draw.clip
25 | import androidx.compose.ui.unit.dp
26 |
27 | @Composable
28 | fun VoiceAnimation(isListening: Boolean) {
29 | val infiniteTransition = rememberInfiniteTransition()
30 | val barCount = 5
31 |
32 | Row(
33 | modifier = Modifier
34 | .fillMaxWidth()
35 | .height(100.dp),
36 | horizontalArrangement = Arrangement.Center,
37 | verticalAlignment = Alignment.CenterVertically
38 | ) {
39 | repeat(barCount) { index ->
40 | val delay = index * 100
41 | val animatedHeight by infiniteTransition.animateFloat(
42 | initialValue = 20f,
43 | targetValue = if (isListening) 60f else 20f,
44 | animationSpec = infiniteRepeatable(
45 | animation = tween(
46 | durationMillis = 1000,
47 | delayMillis = delay,
48 | easing = FastOutSlowInEasing
49 | ),
50 | repeatMode = RepeatMode.Reverse
51 | )
52 | )
53 |
54 | val animatedColor by infiniteTransition.animateColor(
55 | initialValue = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
56 | targetValue = MaterialTheme.colorScheme.primary,
57 | animationSpec = infiniteRepeatable(
58 | animation = tween(
59 | durationMillis = 1000,
60 | delayMillis = delay,
61 | easing = FastOutSlowInEasing
62 | ),
63 | repeatMode = RepeatMode.Reverse
64 | )
65 | )
66 |
67 | Spacer(modifier = Modifier.width(8.dp))
68 |
69 | Box(
70 | modifier = Modifier
71 | .width(8.dp)
72 | .height(animatedHeight.dp)
73 | .clip(RoundedCornerShape(4.dp))
74 | .background(
75 | if (isListening) animatedColor
76 | else MaterialTheme.colorScheme.surfaceVariant
77 | )
78 | )
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/Content.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.foundation.verticalScroll
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.text.TextStyle
22 | import androidx.compose.ui.text.font.FontFamily
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.unit.TextUnit
25 | import androidx.compose.ui.unit.TextUnitType
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import data.ListeningStatus
29 | import data.TranscriptState
30 |
31 | @Composable
32 | fun Content(
33 | paddingValues: PaddingValues,
34 | transcriptState: TranscriptState
35 | ) {
36 | Column(
37 | modifier = Modifier
38 | .fillMaxSize()
39 | .padding(paddingValues)
40 | .background(
41 | color = MaterialTheme.colorScheme.background
42 | ),
43 | horizontalAlignment = Alignment.CenterHorizontally,
44 | verticalArrangement = Arrangement.SpaceEvenly
45 | ) {
46 | val (result, resultTextColor) = when {
47 | transcriptState.error.isError -> {
48 | "ERROR: ${transcriptState.error.message}" to MaterialTheme.colorScheme.error
49 | }
50 |
51 | transcriptState.transcript != null -> {
52 | transcriptState.transcript to MaterialTheme.colorScheme.onBackground
53 | }
54 |
55 | else -> "" to MaterialTheme.colorScheme.onBackground
56 | }
57 |
58 | val scrollState = rememberScrollState()
59 |
60 | LaunchedEffect(result) {
61 | scrollState.animateScrollTo(scrollState.maxValue)
62 | }
63 |
64 | Box(
65 | modifier = Modifier
66 | .fillMaxWidth()
67 | .height(200.dp)
68 | .padding(
69 | vertical = 5.dp,
70 | horizontal = 10.dp
71 | )
72 | .verticalScroll(scrollState)
73 | ) {
74 | Text(
75 | modifier = Modifier.fillMaxWidth(),
76 | text = if (result.isBlank()) "Start to Listen" else result,
77 | style = TextStyle(
78 | fontSize = 28.sp,
79 | letterSpacing = TextUnit(
80 | 1.5f, TextUnitType.Sp
81 | ),
82 | textAlign = TextAlign.Center,
83 | color = resultTextColor,
84 | fontFamily = FontFamily.Serif,
85 | )
86 | )
87 | }
88 |
89 | VoiceAnimation(transcriptState.listeningStatus == ListeningStatus.LISTENING)
90 |
91 | Spacer(modifier = Modifier.height(30.dp))
92 | }
93 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/PermissionNeedDialog.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.PermCameraMic
12 | import androidx.compose.material3.Button
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Surface
16 | import androidx.compose.material3.Text
17 | import androidx.compose.material3.TextButton
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.unit.dp
23 | import androidx.compose.ui.window.Dialog
24 | import getPlatform
25 |
26 | @Composable
27 | fun PermissionNeedDialog(
28 | onDismissRequest: () -> Unit = {},
29 | onClickGoToSettings: () -> Unit = {},
30 | ) {
31 | val isIos = getPlatform().name == "ios"
32 |
33 | Dialog(onDismissRequest = onDismissRequest) {
34 | Surface(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .padding(horizontal = 16.dp)
38 | .padding(vertical = 32.dp),
39 | shape = RoundedCornerShape(28.dp),
40 | color = MaterialTheme.colorScheme.surface,
41 | tonalElevation = 6.dp
42 | ) {
43 | Column(
44 | modifier = Modifier.padding(24.dp),
45 | horizontalAlignment = Alignment.CenterHorizontally,
46 | ) {
47 | Icon(
48 | modifier = Modifier.size(80.dp),
49 | imageVector = Icons.Default.PermCameraMic,
50 | contentDescription = "permission_icon",
51 | tint = MaterialTheme.colorScheme.primary
52 | )
53 |
54 | Spacer(modifier = Modifier.height(24.dp))
55 |
56 | Text(
57 | text = "Microphone Permission Required",
58 | style = MaterialTheme.typography.headlineSmall,
59 | color = MaterialTheme.colorScheme.onSurface,
60 | textAlign = TextAlign.Center,
61 | )
62 |
63 | Spacer(modifier = Modifier.height(8.dp))
64 |
65 | Text(
66 | text = if (isIos) {
67 | "To use speech recognition, we need both microphone and speech recognition permissions. Please enable them in Settings:\n\n" +
68 | "1. Allow microphone access\n" +
69 | "2. Enable Speech Recognition"
70 | } else {
71 | "We need microphone permission to enable speech recognition feature. Please go to app settings and enable microphone permission."
72 | },
73 | style = MaterialTheme.typography.bodyMedium,
74 | color = MaterialTheme.colorScheme.onSurfaceVariant,
75 | textAlign = TextAlign.Center,
76 | )
77 |
78 | Spacer(modifier = Modifier.height(24.dp))
79 |
80 | TextButton(
81 | onClick = onDismissRequest,
82 | modifier = Modifier.fillMaxWidth()
83 | ) {
84 | Text(text = "Maybe Later")
85 | }
86 |
87 | Spacer(modifier = Modifier.height(8.dp))
88 |
89 | Button(
90 | onClick = onClickGoToSettings,
91 | modifier = Modifier.fillMaxWidth(),
92 | shape = RoundedCornerShape(16.dp)
93 | ) {
94 | Text(
95 | text = "Go to Settings",
96 | modifier = Modifier.padding(vertical = 4.dp)
97 | )
98 | }
99 | }
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
24 |
30 |
36 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Speech to Text - Cross-Platform Voice Recognition App
2 |
3 |
4 |
5 | [](https://kotlinlang.org/docs/multiplatform.html)
6 | [](https://www.jetbrains.com/lp/compose-multiplatform/)
7 | []()
8 | [](LICENSE)
9 |
10 | A modern, cross-platform voice recognition application built with Compose Multiplatform, delivering a seamless speech-to-text experience on both iOS and Android platforms.
11 |
12 | [Features](#-features) • [Demo](#-demo) • [Architecture](#-architecture) • [Installation](#-installation) • [Usage](#-usage) • [Contributing](#-contributing)
13 |
14 |
15 |
16 | ## ✨ Features
17 |
18 | ### Core Features
19 | - 📱 Single codebase for iOS and Android platforms
20 | - 🎙️ Real-time speech recognition with high accuracy
21 | - 🌍 Support for 40+ languages worldwide
22 | - 📋 One-tap text copying to clipboard
23 | - 🎨 Modern, intuitive user interface
24 | - 🌓 Light/Dark theme support
25 | - 🔒 Secure microphone permission handling
26 | - ⚡ High-performance native implementation
27 |
28 | ### Platform-Specific Features
29 | - 🤖 **Android**
30 | - Seamless integration with native Speech Recognition API
31 | - Background audio processing
32 | - Runtime permission management
33 | - Optimized for various Android versions
34 |
35 | - 🍎 **iOS**
36 | - Native Speech Framework integration
37 | - Real-time audio buffer processing with AVAudioEngine
38 | - High-quality voice recognition
39 | - Optimized for iOS devices
40 |
41 | ## 🛠️ Technology Stack
42 |
43 | ### Core Technologies
44 | - **Kotlin Multiplatform Mobile (KMM)**
45 | - Share business logic between platforms
46 | - Platform-specific implementations where needed
47 | - Efficient code sharing strategy
48 |
49 | - **Jetpack Compose Multiplatform**
50 | - Modern declarative UI
51 | - Consistent design across platforms
52 | - Rich component library
53 | - Custom composables for specific needs
54 |
55 | - **Dependency Injection**
56 | - Koin for dependency management
57 | - Clean architecture support
58 | - Easy testing capabilities
59 |
60 | ### Platform Integration
61 | - **Android Implementation**
62 | - `SpeechRecognizer` API for native voice recognition
63 | - Android Jetpack libraries
64 | - Material Design 3 components
65 | - Lifecycle-aware components
66 |
67 | - **iOS Implementation**
68 | - `Speech Framework` for native voice processing
69 | - `AVFoundation` for audio handling
70 | - SwiftUI interoperability
71 | - Native iOS UI components
72 |
73 | ## 🏗️ Architecture
74 |
75 | The project follows Clean Architecture principles with MVVM pattern:
76 |
77 | ```
78 | ├── commonMain
79 | │ ├── data
80 | │ │ ├── models
81 | │ ├── presentation
82 | │ │ ├── viewmodel
83 | │ │ └── components
84 | │ ├── platform
85 | │ ├── di
86 | │ │ └── modules
87 | ├── androidMain
88 | │ └── platform
89 | └── iosMain
90 | └── platform
91 | ```
92 |
93 | ## 🚀 Installation
94 |
95 | ### Prerequisites
96 | - Android Studio Arctic Fox or later
97 | - Xcode 13 or later
98 | - JDK 11 or later
99 | - Kotlin Multiplatform Mobile plugin
100 |
101 | ### Setup Steps
102 |
103 | 1. Clone the repository:
104 | ```bash
105 | git clone https://github.com/farukkaraca/speech-to-text-cmp.git
106 | cd speech-to-text-cmp
107 | ```
108 |
109 | 2. Configure platform-specific settings:
110 |
111 | #### Android Setup
112 | ```bash
113 | # Build Android app
114 | ./gradlew :composeApp:assembleDebug
115 |
116 | # Run Android app
117 | ./gradlew :composeApp:installDebug
118 | ```
119 |
120 | #### iOS Setup
121 | ```bash
122 | # Install CocoaPods dependencies
123 | cd iosApp
124 | pod install
125 | # Open Xcode workspace and run
126 | ```
127 |
128 | ## 📱 Usage Guide
129 |
130 | 1. **Initial Setup**
131 | - Launch the application
132 | - Grant microphone permissions when prompted
133 | - Select your preferred language from 40+ options
134 |
135 | 2. **Voice Recognition**
136 | - Tap the microphone button to start recording
137 | - Speak clearly into your device
138 | - Watch as your speech is converted to text in real-time
139 | - Tap again to stop recording
140 |
141 | 3. **Text Management**
142 | - Review the converted text
143 | - Copy text to clipboard with one tap
144 | - Start a new recording session anytime
145 |
146 | 4. **Customization**
147 | - Support light/dark themes
148 | - Change recognition language
149 | - Adjust UI preferences
150 |
151 | ## 🤝 Contributing
152 |
153 | We welcome contributions! Here's how you can help:
154 |
155 | 1. Fork the repository
156 | 2. Create your feature branch:
157 | ```bash
158 | git checkout -b feature/amazing-feature
159 | ```
160 | 3. Commit your changes:
161 | ```bash
162 | git commit -m 'feat: Add amazing feature'
163 | ```
164 | 4. Push to the branch:
165 | ```bash
166 | git push origin feature/amazing-feature
167 | ```
168 | 5. Open a Pull Request
169 |
170 | ### Development Guidelines
171 | - Follow Kotlin coding conventions
172 | - Write unit tests for new features
173 | - Update documentation as needed
174 | - Ensure cross-platform compatibility
175 |
176 | ## 📄 License
177 |
178 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
179 |
180 | ## 🙏 Acknowledgments
181 |
182 | - Thanks to the Compose Multiplatform team
183 |
184 | ---
185 |
186 | ## 🎥 Demo
187 |
188 | Watch our application in action on both platforms! These demos showcase the key features including real-time speech recognition, multiple language support, and the seamless user interface.
189 |
190 |
191 |
192 |
193 |
194 | Ios Demo
195 |
196 |
197 | Android demo showing real-time speech recognition and material design UI
198 | |
199 |
200 | Android Demo
201 |
202 |
203 | iOS demo featuring native speech recognition and SwiftUI integration
204 | |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/BottomBar.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.animation.core.FastOutSlowInEasing
4 | import androidx.compose.animation.core.LinearEasing
5 | import androidx.compose.animation.core.RepeatMode
6 | import androidx.compose.animation.core.animateFloat
7 | import androidx.compose.animation.core.infiniteRepeatable
8 | import androidx.compose.animation.core.rememberInfiniteTransition
9 | import androidx.compose.animation.core.tween
10 | import androidx.compose.foundation.background
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Row
14 | import androidx.compose.foundation.layout.fillMaxSize
15 | import androidx.compose.foundation.layout.fillMaxWidth
16 | import androidx.compose.foundation.layout.height
17 | import androidx.compose.foundation.layout.offset
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.foundation.layout.size
20 | import androidx.compose.foundation.shape.CircleShape
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.filled.ContentCopy
23 | import androidx.compose.material.icons.filled.Menu
24 | import androidx.compose.material.icons.rounded.Mic
25 | import androidx.compose.material.icons.rounded.MicNone
26 | import androidx.compose.material3.Icon
27 | import androidx.compose.material3.IconButton
28 | import androidx.compose.material3.MaterialTheme
29 | import androidx.compose.material3.Surface
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.getValue
32 | import androidx.compose.ui.Alignment
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.draw.scale
35 | import androidx.compose.ui.graphics.vector.ImageVector
36 | import androidx.compose.ui.unit.dp
37 |
38 | @Composable
39 | fun BottomBar(
40 | onClickShowLanguages: () -> Unit = {},
41 | onClickMic: () -> Unit = {},
42 | onClickCopy: () -> Unit = {},
43 | isListening: Boolean = false
44 | ) {
45 | Box(
46 | modifier = Modifier
47 | .fillMaxWidth()
48 | .height(100.dp)
49 | ) {
50 | Surface(
51 | modifier = Modifier
52 | .align(Alignment.BottomCenter)
53 | .fillMaxWidth()
54 | .height(64.dp),
55 | color = MaterialTheme.colorScheme.surface.copy(
56 | alpha = 0.95f
57 | ),
58 | tonalElevation = 4.dp,
59 | ) {
60 | Row(
61 | modifier = Modifier
62 | .fillMaxSize()
63 | .padding(
64 | horizontal = 32.dp
65 | ),
66 | horizontalArrangement = Arrangement.SpaceBetween,
67 | verticalAlignment = Alignment.CenterVertically
68 | ) {
69 | ActionButton(
70 | onClick = onClickShowLanguages,
71 | icon = Icons.Default.Menu
72 | )
73 |
74 | ActionButton(
75 | onClick = onClickCopy,
76 | icon = Icons.Default.ContentCopy
77 | )
78 | }
79 | }
80 |
81 | Box(
82 | modifier = Modifier
83 | .align(Alignment.TopCenter)
84 | .offset(
85 | y = 12.dp
86 | )
87 | ) {
88 | Surface(
89 | modifier = Modifier.size(
90 | 68.dp
91 | ),
92 | shape = CircleShape,
93 | color = MaterialTheme.colorScheme.surface,
94 | tonalElevation = 2.dp
95 | ) {
96 | Box(modifier = Modifier.fillMaxSize())
97 | }
98 |
99 | MicButton(
100 | onClick = onClickMic,
101 | isListening = isListening,
102 | modifier = Modifier
103 | .size(60.dp)
104 | .align(Alignment.Center)
105 | )
106 | }
107 | }
108 | }
109 |
110 | @Composable
111 | private fun ActionButton(
112 | onClick: () -> Unit,
113 | icon: ImageVector
114 | ) {
115 | IconButton(
116 | onClick = onClick,
117 | modifier = Modifier.size(40.dp)
118 | ) {
119 | Icon(
120 | imageVector = icon,
121 | contentDescription = null,
122 | tint = MaterialTheme.colorScheme.onSurface.copy(
123 | alpha = 0.7f
124 | ),
125 | modifier = Modifier.size(24.dp)
126 | )
127 | }
128 | }
129 |
130 | @Composable
131 | private fun MicButton(
132 | onClick: () -> Unit,
133 | isListening: Boolean,
134 | modifier: Modifier = Modifier
135 | ) {
136 | val infiniteTransition = rememberInfiniteTransition(label = "")
137 |
138 | val scale by infiniteTransition.animateFloat(
139 | initialValue = 1f,
140 | targetValue = if (isListening) 1.1f else 1f,
141 | animationSpec = infiniteRepeatable(
142 | animation = tween(1000, easing = LinearEasing),
143 | repeatMode = RepeatMode.Reverse
144 | ),
145 | label = ""
146 | )
147 |
148 | val rippleAnim by infiniteTransition.animateFloat(
149 | initialValue = 0f,
150 | targetValue = 1f,
151 | animationSpec = infiniteRepeatable(
152 | animation = tween(2000, easing = FastOutSlowInEasing),
153 | repeatMode = RepeatMode.Restart
154 | ),
155 | label = ""
156 | )
157 |
158 | Box(
159 | modifier = modifier,
160 | contentAlignment = Alignment.Center
161 | ) {
162 | if (isListening) {
163 | repeat(3) { index ->
164 | val delay = index * 0.3f
165 | val alpha = 0.3f - (rippleAnim + delay) % 1f * 0.3f
166 | val scale = 1f + (rippleAnim + delay) % 1f * 0.7f
167 |
168 | Box(
169 | modifier = Modifier
170 | .fillMaxSize()
171 | .scale(scale)
172 | .background(
173 | color = MaterialTheme.colorScheme.primary.copy(alpha = alpha),
174 | shape = CircleShape
175 | )
176 | )
177 | }
178 | }
179 |
180 | Surface(
181 | modifier = Modifier
182 | .fillMaxSize()
183 | .scale(scale)
184 | .padding(4.dp),
185 | onClick = onClick,
186 | shape = CircleShape,
187 | color = if (isListening)
188 | MaterialTheme.colorScheme.primary
189 | else
190 | MaterialTheme.colorScheme.surface,
191 | tonalElevation = if (isListening) 8.dp else 2.dp
192 | ) {
193 | Icon(
194 | imageVector = if (isListening)
195 | Icons.Rounded.Mic
196 | else
197 | Icons.Rounded.MicNone,
198 | contentDescription = if (isListening) "Stop Listening" else "Start Listening",
199 | tint = if (isListening)
200 | MaterialTheme.colorScheme.onPrimary
201 | else
202 | MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
203 | modifier = Modifier.size(28.dp)
204 | .padding(if (isListening) 10.dp else 0.dp)
205 | )
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/LanguageSelectionDialog.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.lazy.LazyColumn
13 | import androidx.compose.foundation.lazy.items
14 | import androidx.compose.foundation.shape.CircleShape
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.Check
18 | import androidx.compose.material.icons.filled.Close
19 | import androidx.compose.material.icons.filled.Search
20 | import androidx.compose.material3.ExperimentalMaterial3Api
21 | import androidx.compose.material3.Icon
22 | import androidx.compose.material3.IconButton
23 | import androidx.compose.material3.MaterialTheme
24 | import androidx.compose.material3.OutlinedTextField
25 | import androidx.compose.material3.Surface
26 | import androidx.compose.material3.Text
27 | import androidx.compose.material3.TextFieldDefaults
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.getValue
30 | import androidx.compose.runtime.mutableStateOf
31 | import androidx.compose.runtime.remember
32 | import androidx.compose.runtime.setValue
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.draw.clip
36 | import androidx.compose.ui.unit.dp
37 | import androidx.compose.ui.window.Dialog
38 |
39 | @OptIn(ExperimentalMaterial3Api::class)
40 | @Composable
41 | fun LanguageSelectionDialog(
42 | supportedLanguages: List,
43 | onSelected: (String) -> Unit,
44 | selectedLanguage: String,
45 | onDismissRequest: () -> Unit
46 | ) {
47 | Dialog(onDismissRequest = onDismissRequest) {
48 | var searchQuery by remember { mutableStateOf("") }
49 | val filteredLanguages = remember(searchQuery, supportedLanguages) {
50 | if (searchQuery.isEmpty()) {
51 | supportedLanguages
52 | } else {
53 | supportedLanguages.filter { it.contains(searchQuery, ignoreCase = true) }
54 | }
55 | }
56 |
57 | Surface(
58 | modifier = Modifier
59 | .fillMaxSize()
60 | .padding(vertical = 72.dp),
61 | shape = RoundedCornerShape(28.dp),
62 | color = MaterialTheme.colorScheme.surface,
63 | tonalElevation = 6.dp
64 | ) {
65 | Column(
66 | modifier = Modifier
67 | .padding(vertical = 16.dp)
68 | .fillMaxSize()
69 | ) {
70 | Row(
71 | modifier = Modifier
72 | .fillMaxWidth()
73 | .padding(horizontal = 24.dp)
74 | .padding(bottom = 16.dp),
75 | horizontalArrangement = Arrangement.SpaceBetween,
76 | verticalAlignment = Alignment.CenterVertically
77 | ) {
78 | Text(
79 | text = "Select Language",
80 | style = MaterialTheme.typography.headlineSmall,
81 | color = MaterialTheme.colorScheme.onSurface
82 | )
83 |
84 | IconButton(
85 | onClick = onDismissRequest,
86 | modifier = Modifier
87 | .size(32.dp)
88 | .clip(CircleShape)
89 | .background(MaterialTheme.colorScheme.surfaceVariant)
90 | ) {
91 | Icon(
92 | imageVector = Icons.Default.Close,
93 | contentDescription = "Close",
94 | tint = MaterialTheme.colorScheme.onSurfaceVariant,
95 | modifier = Modifier.size(16.dp)
96 | )
97 | }
98 | }
99 |
100 | OutlinedTextField(
101 | value = searchQuery,
102 | onValueChange = { searchQuery = it },
103 | modifier = Modifier
104 | .fillMaxWidth()
105 | .padding(horizontal = 24.dp)
106 | .padding(bottom = 16.dp),
107 | placeholder = { Text("Search...") },
108 | leadingIcon = {
109 | Icon(
110 | Icons.Default.Search,
111 | contentDescription = null,
112 | tint = MaterialTheme.colorScheme.onSurfaceVariant
113 | )
114 | },
115 | shape = RoundedCornerShape(12.dp),
116 | colors = TextFieldDefaults.outlinedTextFieldColors(
117 | focusedBorderColor = MaterialTheme.colorScheme.primary,
118 | unfocusedBorderColor = MaterialTheme.colorScheme.outline,
119 | cursorColor = MaterialTheme.colorScheme.primary
120 | ),
121 | singleLine = true
122 | )
123 |
124 | LazyColumn(
125 | modifier = Modifier
126 | .weight(1f, false)
127 | .padding(horizontal = 12.dp)
128 | ) {
129 | items(filteredLanguages) { language ->
130 | LanguageItem(
131 | language = language,
132 | isSelected = language == selectedLanguage,
133 | onSelected = onSelected
134 | )
135 | }
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
142 | @Composable
143 | private fun LanguageItem(
144 | language: String,
145 | isSelected: Boolean,
146 | onSelected: (String) -> Unit
147 | ) {
148 | Surface(
149 | modifier = Modifier
150 | .fillMaxWidth()
151 | .padding(horizontal = 8.dp, vertical = 4.dp)
152 | .clip(RoundedCornerShape(12.dp))
153 | .clickable { onSelected(language) },
154 | color = if (isSelected)
155 | MaterialTheme.colorScheme.primaryContainer
156 | else
157 | MaterialTheme.colorScheme.surface,
158 | tonalElevation = if (isSelected) 0.dp else 2.dp
159 | ) {
160 | Row(
161 | modifier = Modifier
162 | .fillMaxWidth()
163 | .padding(16.dp),
164 | horizontalArrangement = Arrangement.SpaceBetween,
165 | verticalAlignment = Alignment.CenterVertically
166 | ) {
167 | Text(
168 | text = language,
169 | style = MaterialTheme.typography.bodyLarge,
170 | color = if (isSelected)
171 | MaterialTheme.colorScheme.onPrimaryContainer
172 | else
173 | MaterialTheme.colorScheme.onSurface
174 | )
175 |
176 | if (isSelected) {
177 | Icon(
178 | imageVector = Icons.Default.Check,
179 | contentDescription = null,
180 | tint = MaterialTheme.colorScheme.primary,
181 | modifier = Modifier.size(24.dp)
182 | )
183 | }
184 | }
185 | }
186 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/SpeechToText.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import data.Error
4 | import data.ListeningStatus
5 | import data.PermissionRequestStatus
6 | import data.RecognizerError
7 | import data.TranscriptState
8 | import kotlinx.cinterop.ExperimentalForeignApi
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.update
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.suspendCancellableCoroutine
15 | import platform.AVFAudio.AVAudioEngine
16 | import platform.AVFAudio.AVAudioSession
17 | import platform.AVFAudio.AVAudioSessionCategoryOptions
18 | import platform.AVFAudio.AVAudioSessionCategoryPlayAndRecord
19 | import platform.AVFAudio.AVAudioSessionModeMeasurement
20 | import platform.AVFAudio.AVAudioSessionRecordPermissionDenied
21 | import platform.AVFAudio.AVAudioSessionSetActiveOptions
22 | import platform.AVFAudio.setActive
23 | import platform.Foundation.NSLocale
24 | import platform.Foundation.NSURL
25 | import platform.Foundation.localeIdentifier
26 | import platform.Speech.SFSpeechAudioBufferRecognitionRequest
27 | import platform.Speech.SFSpeechRecognitionTask
28 | import platform.Speech.SFSpeechRecognizer
29 | import platform.Speech.SFSpeechRecognizerAuthorizationStatus
30 | import platform.UIKit.UIApplication
31 | import platform.UIKit.UIApplicationOpenSettingsURLString
32 | import platform.UIKit.UIPasteboard
33 | import kotlin.coroutines.resume
34 |
35 | actual class SpeechToText {
36 |
37 | private var _transcriptState = MutableStateFlow(
38 | TranscriptState(
39 | listeningStatus = ListeningStatus.INACTIVE,
40 | error = Error(isError = false),
41 | transcript = null,
42 | )
43 | )
44 |
45 | actual val transcriptState: MutableStateFlow
46 | get() = _transcriptState
47 |
48 | private var audioEngine: AVAudioEngine? = null
49 | private var request: SFSpeechAudioBufferRecognitionRequest? = null
50 | private var task: SFSpeechRecognitionTask? = null
51 | private var recognizer = SFSpeechRecognizer()
52 | private val customScope = CoroutineScope(Dispatchers.Default)
53 |
54 | init {
55 | getSupportedLanguages { supportedLanguages ->
56 | transcriptState.update {
57 | it.copy(
58 | supportedLanguages = supportedLanguages,
59 | )
60 | }
61 | }
62 | }
63 |
64 | actual fun startTranscribing() {
65 | if (recognizer.isAvailable()) {
66 | try {
67 | val (engine, req) = prepareEngine()
68 | audioEngine = engine
69 | request = req
70 | task = recognizer.recognitionTaskWithRequest(req) { result, error ->
71 | if (result != null) {
72 | updateTranscript(
73 | isError = false,
74 | message = result.bestTranscription.formattedString
75 | )
76 | } else if (error != null) {
77 | //updateTranscript(isError = true, message = error.localizedDescription)
78 | resetTranscription()
79 | }
80 | }
81 | } catch (e: Exception) {
82 | updateTranscript(isError = true, message = e.message)
83 | resetTranscription()
84 | }
85 |
86 | transcriptState.update {
87 | it.copy(listeningStatus = ListeningStatus.LISTENING)
88 | }
89 |
90 | } else {
91 | transcriptState.update {
92 | it.copy(
93 | listeningStatus = ListeningStatus.INACTIVE,
94 | error = Error(
95 | isError = true,
96 | message = RecognizerError.RecognizerIsUnavailable.message
97 | )
98 | )
99 | }
100 | }
101 | }
102 |
103 | actual fun stopTranscribing() {
104 | resetTranscription()
105 | transcriptState.update {
106 | it.copy(
107 | listeningStatus = ListeningStatus.INACTIVE
108 | )
109 | }
110 | }
111 |
112 | private fun resetTranscription() {
113 | task?.cancel()
114 | audioEngine?.stop()
115 | audioEngine = null
116 | request = null
117 | task = null
118 | }
119 |
120 | actual fun requestPermission(onPermissionResult: (PermissionRequestStatus) -> Unit) {
121 | customScope.launch {
122 | val hasRecordPermission = hasPermissionToRecord()
123 | val hasSpeechPermission = hasAuthorizationToRecognize()
124 |
125 | when {
126 | hasRecordPermission && hasSpeechPermission -> {
127 | onPermissionResult(PermissionRequestStatus.ALLOWED)
128 | }
129 | !hasRecordPermission || !hasSpeechPermission -> {
130 | val recordAuthStatus = AVAudioSession.sharedInstance().recordPermission
131 | val speechAuthStatus = SFSpeechRecognizer.authorizationStatus()
132 |
133 | if (recordAuthStatus == AVAudioSessionRecordPermissionDenied ||
134 | speechAuthStatus == SFSpeechRecognizerAuthorizationStatus.SFSpeechRecognizerAuthorizationStatusDenied) {
135 | onPermissionResult(PermissionRequestStatus.NEVER_ASK_AGAIN)
136 | } else {
137 | onPermissionResult(PermissionRequestStatus.NOT_ALLOWED)
138 | }
139 | }
140 | }
141 | }
142 | }
143 |
144 | private suspend fun hasAuthorizationToRecognize(): Boolean =
145 | suspendCancellableCoroutine { continuation ->
146 | SFSpeechRecognizer.requestAuthorization { status ->
147 | continuation.resume(status == SFSpeechRecognizerAuthorizationStatus.SFSpeechRecognizerAuthorizationStatusAuthorized)
148 | }
149 | }
150 |
151 | private suspend fun hasPermissionToRecord(): Boolean =
152 | suspendCancellableCoroutine { continuation ->
153 | AVAudioSession.sharedInstance().requestRecordPermission { granted ->
154 | continuation.resume(granted)
155 | }
156 | }
157 |
158 | @OptIn(ExperimentalForeignApi::class)
159 | private fun prepareEngine(): Pair {
160 | val audioEngine = AVAudioEngine()
161 | val request = SFSpeechAudioBufferRecognitionRequest()
162 | .apply {
163 | shouldReportPartialResults = true
164 | }
165 |
166 | val audioSession = AVAudioSession.sharedInstance()
167 | audioSession.setCategory(
168 | AVAudioSessionCategoryPlayAndRecord,
169 | AVAudioSessionModeMeasurement,
170 | AVAudioSessionCategoryOptions.MIN_VALUE,
171 | null
172 | )
173 | audioSession.setActive(
174 | true,
175 | AVAudioSessionSetActiveOptions.MIN_VALUE,
176 | null
177 | )
178 |
179 | val inputNode = audioEngine.inputNode
180 | val recordingFormat = inputNode.outputFormatForBus(0u)
181 | inputNode.installTapOnBus(0u, 1024u, recordingFormat) { buffer, _ ->
182 | request.appendAudioPCMBuffer(buffer!!)
183 | }
184 |
185 | audioEngine.prepare()
186 | audioEngine.startAndReturnError(null)
187 |
188 | return Pair(audioEngine, request)
189 | }
190 |
191 | private fun updateTranscript(isError: Boolean, message: String?) {
192 | if (!isError) {
193 | transcriptState.update {
194 | it.copy(
195 | transcript = message,
196 | error = Error(isError = false)
197 | )
198 | }
199 | } else {
200 | transcriptState.update {
201 | it.copy(
202 | listeningStatus = ListeningStatus.INACTIVE,
203 | error = Error(isError = true, message = message),
204 | transcript = null
205 | )
206 | }
207 | }
208 | }
209 |
210 | actual fun setLanguage(languageCode: String) {
211 | val locale = NSLocale(languageCode)
212 | recognizer = SFSpeechRecognizer(locale)
213 | }
214 |
215 | actual fun getSupportedLanguages(onLanguagesResult: (List) -> Unit) {
216 | val supportedLocales = SFSpeechRecognizer.supportedLocales()
217 | val languages = supportedLocales.map { (it as NSLocale).localeIdentifier() }
218 | onLanguagesResult(languages)
219 | }
220 |
221 | actual fun copyText(text: String) {
222 | UIPasteboard.generalPasteboard.string = text
223 | }
224 |
225 | actual fun showNeedPermission() {
226 | transcriptState.update {
227 | it.copy(
228 | showPermissionNeedDialog = true,
229 | )
230 | }
231 | }
232 |
233 | actual fun dismissPermissionDialog() {
234 | transcriptState.update {
235 | it.copy(
236 | showPermissionNeedDialog = false,
237 | )
238 | }
239 | }
240 |
241 | actual fun openAppSettings() {
242 | val recordAuthStatus = AVAudioSession.sharedInstance().recordPermission
243 | val speechAuthStatus = SFSpeechRecognizer.authorizationStatus()
244 |
245 | when {
246 | recordAuthStatus == AVAudioSessionRecordPermissionDenied -> {
247 | val micSettingsUrl = NSURL.URLWithString("prefs:root=Privacy&path=MICROPHONE")
248 | if (micSettingsUrl != null && UIApplication.sharedApplication.canOpenURL(micSettingsUrl)) {
249 | UIApplication.sharedApplication.openURL(
250 | micSettingsUrl,
251 | mapOf(),
252 | null
253 | )
254 | } else {
255 | openGeneralSettings()
256 | }
257 | }
258 | speechAuthStatus == SFSpeechRecognizerAuthorizationStatus.SFSpeechRecognizerAuthorizationStatusDenied -> {
259 | val speechSettingsUrl = NSURL.URLWithString("prefs:root=Privacy&path=SPEECH_RECOGNITION")
260 | if (speechSettingsUrl != null && UIApplication.sharedApplication.canOpenURL(speechSettingsUrl)) {
261 | UIApplication.sharedApplication.openURL(
262 | speechSettingsUrl,
263 | mapOf(),
264 | null
265 | )
266 | } else {
267 | openGeneralSettings()
268 | }
269 | }
270 | else -> {
271 | openGeneralSettings()
272 | }
273 | }
274 | dismissPermissionDialog()
275 | }
276 |
277 | private fun openGeneralSettings() {
278 | val generalSettingsUrl = NSURL.URLWithString(UIApplicationOpenSettingsURLString)
279 | if (generalSettingsUrl != null) {
280 | UIApplication.sharedApplication.openURL(
281 | generalSettingsUrl,
282 | mapOf(),
283 | null
284 | )
285 | }
286 | }
287 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/SpeechToText.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.ClipData
6 | import android.content.ClipboardManager
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.pm.PackageManager
10 | import android.net.Uri
11 | import android.os.Bundle
12 | import android.provider.Settings
13 | import android.speech.RecognitionListener
14 | import android.speech.RecognizerIntent
15 | import android.speech.SpeechRecognizer
16 | import android.util.Log
17 | import androidx.activity.ComponentActivity
18 | import androidx.activity.result.contract.ActivityResultContracts
19 | import androidx.core.app.ActivityCompat
20 | import androidx.core.content.ContextCompat
21 | import data.Error
22 | import data.ListeningStatus
23 | import data.PermissionRequestStatus
24 | import data.RecognizerError
25 | import data.TranscriptState
26 | import kotlinx.coroutines.flow.MutableStateFlow
27 | import kotlinx.coroutines.flow.update
28 |
29 | actual class SpeechToText(
30 | private val context: Context,
31 | private val activity: Activity
32 | ) {
33 | private val _transcriptState = MutableStateFlow(
34 | TranscriptState(
35 | listeningStatus = ListeningStatus.INACTIVE,
36 | error = Error(isError = false),
37 | transcript = null,
38 | )
39 | )
40 |
41 | actual val transcriptState: MutableStateFlow
42 | get() = _transcriptState
43 |
44 | private var permissionLauncher = initPermissionLauncher()
45 | private var _permissionCallback: ((PermissionRequestStatus) -> Unit)? = null
46 |
47 | private fun initPermissionLauncher() =
48 | (activity as ComponentActivity).activityResultRegistry.register(
49 | "permission",
50 | ActivityResultContracts.RequestPermission()
51 | ) { isGranted ->
52 | if (isGranted) {
53 | _permissionCallback?.invoke(PermissionRequestStatus.ALLOWED)
54 | } else {
55 | if (!ActivityCompat.shouldShowRequestPermissionRationale(
56 | activity,
57 | Manifest.permission.RECORD_AUDIO
58 | )
59 | ) {
60 | _permissionCallback?.invoke(PermissionRequestStatus.NEVER_ASK_AGAIN)
61 | } else {
62 | _permissionCallback?.invoke(PermissionRequestStatus.NOT_ALLOWED)
63 | }
64 | }
65 | _permissionCallback = null
66 | }
67 |
68 | init {
69 | initializeSpeechRecognizer()
70 | getSupportedLanguages { supportedLanguages ->
71 | _transcriptState.update {
72 | it.copy(
73 | supportedLanguages = supportedLanguages,
74 | )
75 | }
76 | }
77 | }
78 |
79 | private var speechRecognizer: SpeechRecognizer? = null
80 | private var recognitionListener: RecognitionListener? = null
81 |
82 | private fun initializeSpeechRecognizer() {
83 | if (SpeechRecognizer.isRecognitionAvailable(context)) {
84 | speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
85 | Log.d("SpeechToText", "SpeechRecognizer initialized")
86 | } else {
87 | Log.e("SpeechToText", "SpeechRecognizer not available on this device")
88 | }
89 | }
90 |
91 | actual fun startTranscribing() {
92 | if (speechRecognizer == null) {
93 | _transcriptState.update {
94 | it.copy(
95 | listeningStatus = ListeningStatus.INACTIVE,
96 | error = Error(isError = true, message = RecognizerError.NilRecognizer.message)
97 | )
98 | }
99 | } else {
100 | val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
101 | putExtra(
102 | RecognizerIntent.EXTRA_LANGUAGE_MODEL,
103 | RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
104 | )
105 | }
106 |
107 | intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
108 | intent.putExtra(
109 | RecognizerIntent.EXTRA_LANGUAGE,
110 | _transcriptState.value.selectedLanguage
111 | )
112 |
113 |
114 | recognitionListener = object : RecognitionListener {
115 | override fun onReadyForSpeech(params: Bundle?) {
116 | _transcriptState.update {
117 | it.copy(listeningStatus = ListeningStatus.LISTENING)
118 | }
119 | }
120 |
121 | override fun onBeginningOfSpeech() {
122 |
123 | }
124 |
125 | override fun onRmsChanged(rmsdB: Float) {
126 |
127 | }
128 |
129 | override fun onBufferReceived(buffer: ByteArray?) {
130 |
131 | }
132 |
133 | override fun onEndOfSpeech() {
134 | _transcriptState.update {
135 | it.copy(listeningStatus = ListeningStatus.INACTIVE)
136 | }
137 | }
138 |
139 | override fun onError(error: Int) {
140 | _transcriptState.update {
141 | it.copy(
142 | listeningStatus = ListeningStatus.INACTIVE,
143 | error = Error(isError = true, message = error.toString())
144 | )
145 | }
146 | }
147 |
148 | override fun onResults(results: Bundle?) {
149 | val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
150 | if (!matches.isNullOrEmpty()) {
151 | _transcriptState.update {
152 | it.copy(
153 | listeningStatus = ListeningStatus.INACTIVE,
154 | error = Error(isError = false),
155 | transcript = matches[0]
156 | )
157 | }
158 | }
159 | }
160 |
161 | override fun onPartialResults(partialResults: Bundle?) {
162 | val matches =
163 | partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
164 | if (!matches.isNullOrEmpty()) {
165 | _transcriptState.update {
166 | it.copy(
167 | transcript = matches[0],
168 | error = Error(isError = false)
169 | )
170 | }
171 | }
172 | }
173 |
174 | override fun onEvent(eventType: Int, params: Bundle?) {
175 |
176 | }
177 |
178 | }
179 |
180 | speechRecognizer?.setRecognitionListener(recognitionListener)
181 | speechRecognizer?.startListening(intent)
182 | }
183 | }
184 |
185 | actual fun stopTranscribing() {
186 | speechRecognizer?.stopListening()
187 | }
188 |
189 | actual fun requestPermission(onPermissionResult: (PermissionRequestStatus) -> Unit) {
190 | when {
191 | ContextCompat.checkSelfPermission(
192 | context,
193 | Manifest.permission.RECORD_AUDIO
194 | ) == PackageManager.PERMISSION_GRANTED -> {
195 | onPermissionResult(PermissionRequestStatus.ALLOWED)
196 | }
197 |
198 | ActivityCompat.shouldShowRequestPermissionRationale(
199 | activity,
200 | Manifest.permission.RECORD_AUDIO
201 | ) -> {
202 | _permissionCallback = onPermissionResult
203 | permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
204 | }
205 |
206 | else -> {
207 | if (ContextCompat.checkSelfPermission(
208 | context,
209 | Manifest.permission.RECORD_AUDIO
210 | ) == PackageManager.PERMISSION_DENIED
211 | ) {
212 | _permissionCallback = onPermissionResult
213 | permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
214 | } else {
215 | onPermissionResult(PermissionRequestStatus.NEVER_ASK_AGAIN)
216 | }
217 | }
218 | }
219 | }
220 |
221 | actual fun setLanguage(languageCode: String) {
222 | _transcriptState.update {
223 | it.copy(selectedLanguage = languageCode)
224 | }
225 | }
226 |
227 | actual fun getSupportedLanguages(onLanguagesResult: (List) -> Unit) {
228 | val supportedLanguages = listOf(
229 | "en-US", // English (United States)
230 | "en-GB", // English (United Kingdom)
231 | "en-AU", // English (Australia)
232 | "en-CA", // English (Canada)
233 | "es-ES", // Spanish (Spain)
234 | "es-MX", // Spanish (Mexico)
235 | "es-AR", // Spanish (Argentina)
236 | "fr-FR", // French (France)
237 | "fr-CA", // French (Canada)
238 | "de-DE", // German
239 | "it-IT", // Italian
240 | "pt-PT", // Portuguese (Portugal)
241 | "pt-BR", // Portuguese (Brazil)
242 | "ru-RU", // Russian
243 | "zh-CN", // Chinese (Simplified)
244 | "zh-TW", // Chinese (Traditional)
245 | "ja-JP", // Japanese
246 | "ko-KR", // Korean
247 | "ar-SA", // Arabic (Saudi Arabia)
248 | "ar-AE", // Arabic (UAE)
249 | "tr-TR", // Turkish
250 | "hi-IN", // Hindi
251 | "th-TH", // Thai
252 | "vi-VN", // Vietnamese
253 | "id-ID", // Indonesian
254 | "ms-MY", // Malay
255 | "fil-PH", // Filipino
256 | "nl-NL", // Dutch
257 | "pl-PL", // Polish
258 | "ro-RO", // Romanian
259 | "hu-HU", // Hungarian
260 | "cs-CZ", // Czech
261 | "el-GR", // Greek
262 | "sv-SE", // Swedish
263 | "da-DK", // Danish
264 | "no-NO", // Norwegian
265 | "fi-FI", // Finnish
266 | "he-IL", // Hebrew
267 | "bn-IN", // Bengali
268 | "ta-IN" // Tamil
269 | ).filter { locale ->
270 | try {
271 | val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
272 | putExtra(RecognizerIntent.EXTRA_LANGUAGE, locale)
273 | putExtra(
274 | RecognizerIntent.EXTRA_LANGUAGE_MODEL,
275 | RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
276 | )
277 | }
278 | intent.resolveActivity(context.packageManager) != null &&
279 | SpeechRecognizer.isRecognitionAvailable(context)
280 | } catch (e: Exception) {
281 | false
282 | }
283 | }
284 |
285 | onLanguagesResult(supportedLanguages)
286 | }
287 |
288 | actual fun copyText(text: String) {
289 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
290 | val clip = ClipData.newPlainText("Speech Text", text)
291 | clipboard.setPrimaryClip(clip)
292 | }
293 |
294 | actual fun showNeedPermission() {
295 | _transcriptState.update {
296 | it.copy(
297 | showPermissionNeedDialog = true
298 | )
299 | }
300 | }
301 |
302 | actual fun dismissPermissionDialog() {
303 | _transcriptState.update {
304 | it.copy(
305 | showPermissionNeedDialog = false
306 | )
307 | }
308 | }
309 |
310 | actual fun openAppSettings() {
311 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
312 | data = Uri.fromParts("package", context.packageName, null)
313 | flags = Intent.FLAG_ACTIVITY_NEW_TASK
314 | }
315 | context.startActivity(intent)
316 | dismissPermissionDialog()
317 | }
318 | }
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
18 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
19 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
20 | 7555FF7B242A565900829871 /* Speech to Text.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Speech to Text.app"; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
23 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
24 | /* End PBXFileReference section */
25 |
26 | /* Begin PBXFrameworksBuildPhase section */
27 | B92378962B6B1156000C7307 /* Frameworks */ = {
28 | isa = PBXFrameworksBuildPhase;
29 | buildActionMask = 2147483647;
30 | files = (
31 | );
32 | runOnlyForDeploymentPostprocessing = 0;
33 | };
34 | /* End PBXFrameworksBuildPhase section */
35 |
36 | /* Begin PBXGroup section */
37 | 058557D7273AAEEB004C7B11 /* Preview Content */ = {
38 | isa = PBXGroup;
39 | children = (
40 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
41 | );
42 | path = "Preview Content";
43 | sourceTree = "";
44 | };
45 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = {
46 | isa = PBXGroup;
47 | children = (
48 | );
49 | name = Frameworks;
50 | sourceTree = "";
51 | };
52 | 7555FF72242A565900829871 = {
53 | isa = PBXGroup;
54 | children = (
55 | AB1DB47929225F7C00F7AF9C /* Configuration */,
56 | 7555FF7D242A565900829871 /* iosApp */,
57 | 7555FF7C242A565900829871 /* Products */,
58 | 42799AB246E5F90AF97AA0EF /* Frameworks */,
59 | );
60 | sourceTree = "";
61 | };
62 | 7555FF7C242A565900829871 /* Products */ = {
63 | isa = PBXGroup;
64 | children = (
65 | 7555FF7B242A565900829871 /* Speech to Text.app */,
66 | );
67 | name = Products;
68 | sourceTree = "";
69 | };
70 | 7555FF7D242A565900829871 /* iosApp */ = {
71 | isa = PBXGroup;
72 | children = (
73 | 058557BA273AAA24004C7B11 /* Assets.xcassets */,
74 | 7555FF82242A565900829871 /* ContentView.swift */,
75 | 7555FF8C242A565B00829871 /* Info.plist */,
76 | 2152FB032600AC8F00CF470E /* iOSApp.swift */,
77 | 058557D7273AAEEB004C7B11 /* Preview Content */,
78 | );
79 | path = iosApp;
80 | sourceTree = "";
81 | };
82 | AB1DB47929225F7C00F7AF9C /* Configuration */ = {
83 | isa = PBXGroup;
84 | children = (
85 | AB3632DC29227652001CCB65 /* Config.xcconfig */,
86 | );
87 | path = Configuration;
88 | sourceTree = "";
89 | };
90 | /* End PBXGroup section */
91 |
92 | /* Begin PBXNativeTarget section */
93 | 7555FF7A242A565900829871 /* iosApp */ = {
94 | isa = PBXNativeTarget;
95 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
96 | buildPhases = (
97 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
98 | 7555FF77242A565900829871 /* Sources */,
99 | B92378962B6B1156000C7307 /* Frameworks */,
100 | 7555FF79242A565900829871 /* Resources */,
101 | );
102 | buildRules = (
103 | );
104 | dependencies = (
105 | );
106 | name = iosApp;
107 | packageProductDependencies = (
108 | );
109 | productName = iosApp;
110 | productReference = 7555FF7B242A565900829871 /* Speech to Text.app */;
111 | productType = "com.apple.product-type.application";
112 | };
113 | /* End PBXNativeTarget section */
114 |
115 | /* Begin PBXProject section */
116 | 7555FF73242A565900829871 /* Project object */ = {
117 | isa = PBXProject;
118 | attributes = {
119 | BuildIndependentTargetsInParallel = YES;
120 | LastSwiftUpdateCheck = 1130;
121 | LastUpgradeCheck = 1540;
122 | ORGANIZATIONNAME = orgName;
123 | TargetAttributes = {
124 | 7555FF7A242A565900829871 = {
125 | CreatedOnToolsVersion = 11.3.1;
126 | };
127 | };
128 | };
129 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
130 | compatibilityVersion = "Xcode 14.0";
131 | developmentRegion = en;
132 | hasScannedForEncodings = 0;
133 | knownRegions = (
134 | en,
135 | Base,
136 | );
137 | mainGroup = 7555FF72242A565900829871;
138 | packageReferences = (
139 | );
140 | productRefGroup = 7555FF7C242A565900829871 /* Products */;
141 | projectDirPath = "";
142 | projectRoot = "";
143 | targets = (
144 | 7555FF7A242A565900829871 /* iosApp */,
145 | );
146 | };
147 | /* End PBXProject section */
148 |
149 | /* Begin PBXResourcesBuildPhase section */
150 | 7555FF79242A565900829871 /* Resources */ = {
151 | isa = PBXResourcesBuildPhase;
152 | buildActionMask = 2147483647;
153 | files = (
154 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
155 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
156 | );
157 | runOnlyForDeploymentPostprocessing = 0;
158 | };
159 | /* End PBXResourcesBuildPhase section */
160 |
161 | /* Begin PBXShellScriptBuildPhase section */
162 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
163 | isa = PBXShellScriptBuildPhase;
164 | buildActionMask = 2147483647;
165 | files = (
166 | );
167 | inputFileListPaths = (
168 | );
169 | inputPaths = (
170 | );
171 | name = "Compile Kotlin Framework";
172 | outputFileListPaths = (
173 | );
174 | outputPaths = (
175 | );
176 | runOnlyForDeploymentPostprocessing = 0;
177 | shellPath = /bin/sh;
178 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
179 | };
180 | /* End PBXShellScriptBuildPhase section */
181 |
182 | /* Begin PBXSourcesBuildPhase section */
183 | 7555FF77242A565900829871 /* Sources */ = {
184 | isa = PBXSourcesBuildPhase;
185 | buildActionMask = 2147483647;
186 | files = (
187 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
188 | 7555FF83242A565900829871 /* ContentView.swift in Sources */,
189 | );
190 | runOnlyForDeploymentPostprocessing = 0;
191 | };
192 | /* End PBXSourcesBuildPhase section */
193 |
194 | /* Begin XCBuildConfiguration section */
195 | 7555FFA3242A565B00829871 /* Debug */ = {
196 | isa = XCBuildConfiguration;
197 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
198 | buildSettings = {
199 | ALWAYS_SEARCH_USER_PATHS = NO;
200 | CLANG_ANALYZER_NONNULL = YES;
201 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
203 | CLANG_CXX_LIBRARY = "libc++";
204 | CLANG_ENABLE_MODULES = YES;
205 | CLANG_ENABLE_OBJC_ARC = YES;
206 | CLANG_ENABLE_OBJC_WEAK = YES;
207 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
208 | CLANG_WARN_BOOL_CONVERSION = YES;
209 | CLANG_WARN_COMMA = YES;
210 | CLANG_WARN_CONSTANT_CONVERSION = YES;
211 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
214 | CLANG_WARN_EMPTY_BODY = YES;
215 | CLANG_WARN_ENUM_CONVERSION = YES;
216 | CLANG_WARN_INFINITE_RECURSION = YES;
217 | CLANG_WARN_INT_CONVERSION = YES;
218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
222 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
224 | CLANG_WARN_STRICT_PROTOTYPES = YES;
225 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
227 | CLANG_WARN_UNREACHABLE_CODE = YES;
228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
229 | COPY_PHASE_STRIP = NO;
230 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
231 | ENABLE_STRICT_OBJC_MSGSEND = YES;
232 | ENABLE_TESTABILITY = YES;
233 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
234 | GCC_C_LANGUAGE_STANDARD = gnu11;
235 | GCC_DYNAMIC_NO_PIC = NO;
236 | GCC_NO_COMMON_BLOCKS = YES;
237 | GCC_OPTIMIZATION_LEVEL = 0;
238 | GCC_PREPROCESSOR_DEFINITIONS = (
239 | "DEBUG=1",
240 | "$(inherited)",
241 | );
242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
244 | GCC_WARN_UNDECLARED_SELECTOR = YES;
245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
246 | GCC_WARN_UNUSED_FUNCTION = YES;
247 | GCC_WARN_UNUSED_VARIABLE = YES;
248 | IPHONEOS_DEPLOYMENT_TARGET = 15.3;
249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
250 | MTL_FAST_MATH = YES;
251 | ONLY_ACTIVE_ARCH = YES;
252 | SDKROOT = iphoneos;
253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
255 | };
256 | name = Debug;
257 | };
258 | 7555FFA4242A565B00829871 /* Release */ = {
259 | isa = XCBuildConfiguration;
260 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
261 | buildSettings = {
262 | ALWAYS_SEARCH_USER_PATHS = NO;
263 | CLANG_ANALYZER_NONNULL = YES;
264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
266 | CLANG_CXX_LIBRARY = "libc++";
267 | CLANG_ENABLE_MODULES = YES;
268 | CLANG_ENABLE_OBJC_ARC = YES;
269 | CLANG_ENABLE_OBJC_WEAK = YES;
270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
271 | CLANG_WARN_BOOL_CONVERSION = YES;
272 | CLANG_WARN_COMMA = YES;
273 | CLANG_WARN_CONSTANT_CONVERSION = YES;
274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
277 | CLANG_WARN_EMPTY_BODY = YES;
278 | CLANG_WARN_ENUM_CONVERSION = YES;
279 | CLANG_WARN_INFINITE_RECURSION = YES;
280 | CLANG_WARN_INT_CONVERSION = YES;
281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
285 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
287 | CLANG_WARN_STRICT_PROTOTYPES = YES;
288 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
290 | CLANG_WARN_UNREACHABLE_CODE = YES;
291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
292 | COPY_PHASE_STRIP = NO;
293 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
294 | ENABLE_NS_ASSERTIONS = NO;
295 | ENABLE_STRICT_OBJC_MSGSEND = YES;
296 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
297 | GCC_C_LANGUAGE_STANDARD = gnu11;
298 | GCC_NO_COMMON_BLOCKS = YES;
299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
301 | GCC_WARN_UNDECLARED_SELECTOR = YES;
302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
303 | GCC_WARN_UNUSED_FUNCTION = YES;
304 | GCC_WARN_UNUSED_VARIABLE = YES;
305 | IPHONEOS_DEPLOYMENT_TARGET = 15.3;
306 | MTL_ENABLE_DEBUG_INFO = NO;
307 | MTL_FAST_MATH = YES;
308 | SDKROOT = iphoneos;
309 | SWIFT_COMPILATION_MODE = wholemodule;
310 | SWIFT_OPTIMIZATION_LEVEL = "-O";
311 | VALIDATE_PRODUCT = YES;
312 | };
313 | name = Release;
314 | };
315 | 7555FFA6242A565B00829871 /* Debug */ = {
316 | isa = XCBuildConfiguration;
317 | buildSettings = {
318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
319 | CODE_SIGN_IDENTITY = "Apple Development";
320 | CODE_SIGN_STYLE = Automatic;
321 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
322 | DEVELOPMENT_TEAM = "${TEAM_ID}";
323 | ENABLE_PREVIEWS = YES;
324 | FRAMEWORK_SEARCH_PATHS = (
325 | "$(inherited)",
326 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
327 | );
328 | INFOPLIST_FILE = iosApp/Info.plist;
329 | IPHONEOS_DEPLOYMENT_TARGET = 15.3;
330 | LD_RUNPATH_SEARCH_PATHS = (
331 | "$(inherited)",
332 | "@executable_path/Frameworks",
333 | );
334 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
335 | PRODUCT_NAME = "${APP_NAME}";
336 | PROVISIONING_PROFILE_SPECIFIER = "";
337 | SWIFT_VERSION = 5.0;
338 | TARGETED_DEVICE_FAMILY = "1,2";
339 | };
340 | name = Debug;
341 | };
342 | 7555FFA7242A565B00829871 /* Release */ = {
343 | isa = XCBuildConfiguration;
344 | buildSettings = {
345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
346 | CODE_SIGN_IDENTITY = "Apple Development";
347 | CODE_SIGN_STYLE = Automatic;
348 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
349 | DEVELOPMENT_TEAM = "${TEAM_ID}";
350 | ENABLE_PREVIEWS = YES;
351 | FRAMEWORK_SEARCH_PATHS = (
352 | "$(inherited)",
353 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
354 | );
355 | INFOPLIST_FILE = iosApp/Info.plist;
356 | IPHONEOS_DEPLOYMENT_TARGET = 15.3;
357 | LD_RUNPATH_SEARCH_PATHS = (
358 | "$(inherited)",
359 | "@executable_path/Frameworks",
360 | );
361 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
362 | PRODUCT_NAME = "${APP_NAME}";
363 | PROVISIONING_PROFILE_SPECIFIER = "";
364 | SWIFT_VERSION = 5.0;
365 | TARGETED_DEVICE_FAMILY = "1,2";
366 | };
367 | name = Release;
368 | };
369 | /* End XCBuildConfiguration section */
370 |
371 | /* Begin XCConfigurationList section */
372 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
373 | isa = XCConfigurationList;
374 | buildConfigurations = (
375 | 7555FFA3242A565B00829871 /* Debug */,
376 | 7555FFA4242A565B00829871 /* Release */,
377 | );
378 | defaultConfigurationIsVisible = 0;
379 | defaultConfigurationName = Release;
380 | };
381 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
382 | isa = XCConfigurationList;
383 | buildConfigurations = (
384 | 7555FFA6242A565B00829871 /* Debug */,
385 | 7555FFA7242A565B00829871 /* Release */,
386 | );
387 | defaultConfigurationIsVisible = 0;
388 | defaultConfigurationName = Release;
389 | };
390 | /* End XCConfigurationList section */
391 | };
392 | rootObject = 7555FF73242A565900829871 /* Project object */;
393 | }
--------------------------------------------------------------------------------