├── .gitattributes
├── .github
├── ci-gradle.properties
└── workflows
│ └── fastlane-metadata.yaml
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── LICENSE
├── PrivacyPolicy.md
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── dev
│ │ └── atajan
│ │ └── lingva_android
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── dev
│ │ │ └── atajan
│ │ │ └── lingva_android
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainApplication.kt
│ │ │ ├── QuickTranslateActivity.kt
│ │ │ ├── common
│ │ │ ├── data
│ │ │ │ ├── api
│ │ │ │ │ ├── KtorLingvaApi.kt
│ │ │ │ │ ├── LingvaApi.kt
│ │ │ │ │ ├── constants
│ │ │ │ │ │ └── ApiConstants.kt
│ │ │ │ │ └── lingvadto
│ │ │ │ │ │ ├── audio
│ │ │ │ │ │ └── AudioDTO.kt
│ │ │ │ │ │ ├── language
│ │ │ │ │ │ ├── LanguageDTO.kt
│ │ │ │ │ │ └── LanguagesDTO.kt
│ │ │ │ │ │ └── translation
│ │ │ │ │ │ ├── DefinitionDTO.kt
│ │ │ │ │ │ ├── DefinitionInfoDTO.kt
│ │ │ │ │ │ ├── ExtraTranslationDTO.kt
│ │ │ │ │ │ ├── ExtraTranslationListDTO.kt
│ │ │ │ │ │ ├── InfoDTO.kt
│ │ │ │ │ │ ├── PronunciationDTO.kt
│ │ │ │ │ │ └── TranslationDTO.kt
│ │ │ │ └── datasource
│ │ │ │ │ ├── AudioRepository.kt
│ │ │ │ │ ├── LanguagesRepository.kt
│ │ │ │ │ ├── TranslationRepository.kt
│ │ │ │ │ └── impl
│ │ │ │ │ ├── KtorAudioRepository.kt
│ │ │ │ │ ├── KtorLanguagesRepository.kt
│ │ │ │ │ ├── KtorTranslationRepository.kt
│ │ │ │ │ └── PreferencesDatastore.kt
│ │ │ ├── di
│ │ │ │ ├── AppScopeModule.kt
│ │ │ │ ├── AudioPlayerModule.kt
│ │ │ │ ├── ClipboardManagerModule.kt
│ │ │ │ ├── DataStoreModule.kt
│ │ │ │ ├── LingvaApiModule.kt
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ └── UseCaseModule.kt
│ │ │ ├── domain
│ │ │ │ ├── errors
│ │ │ │ │ └── Errors.kt
│ │ │ │ ├── models
│ │ │ │ │ ├── audio
│ │ │ │ │ │ └── Audio.kt
│ │ │ │ │ ├── language
│ │ │ │ │ │ └── Language.kt
│ │ │ │ │ └── translation
│ │ │ │ │ │ ├── Translation.kt
│ │ │ │ │ │ ├── TranslationInfo.kt
│ │ │ │ │ │ └── TranslationWithInfo.kt
│ │ │ │ └── results
│ │ │ │ │ └── RepositoryResponse.kt
│ │ │ ├── media
│ │ │ │ ├── AudioPlayer.kt
│ │ │ │ └── NativeAudioPlayer.kt
│ │ │ ├── redux
│ │ │ │ ├── MVIViewModel.kt
│ │ │ │ └── MiddleWares.kt
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── AppThemeSelectionRadioButtonRows.kt
│ │ │ │ │ ├── BottomSheetSectionHeader.kt
│ │ │ │ │ ├── LanguageListPopUp.kt
│ │ │ │ │ ├── LanguageSelectionAndSettingsBar.kt
│ │ │ │ │ ├── LanguageSelectionBar.kt
│ │ │ │ │ ├── NotificationDialog.kt
│ │ │ │ │ ├── SelectDefaultLanguagesColumn.kt
│ │ │ │ │ ├── SettingsBottomSheet.kt
│ │ │ │ │ ├── SettingsBottomSheetThemedOutlinedTextField.kt
│ │ │ │ │ └── TitleBar.kt
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Helpers.kt
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ └── usecases
│ │ │ │ ├── FetchSupportedLanguagesUseCase.kt
│ │ │ │ ├── ObserveAudioDataUseCase.kt
│ │ │ │ ├── ObserveTranslationResultUseCase.kt
│ │ │ │ ├── PlayByteArrayAudioUseCase.kt
│ │ │ │ ├── RequestAudioDataUseCase.kt
│ │ │ │ ├── TranslateUseCase.kt
│ │ │ │ └── ktorimpl
│ │ │ │ ├── AudioPlayerPlayByteArrayAudioUseCase.kt
│ │ │ │ ├── KtorFetchSupportedLanguagesUseCase.kt
│ │ │ │ ├── KtorObserveAudioDataUseCase.kt
│ │ │ │ ├── KtorObserveTranslationResultUseCase.kt
│ │ │ │ ├── KtorRequestAudioDataUseCase.kt
│ │ │ │ └── KtorTranslateUseCase.kt
│ │ │ ├── quicktranslatefeature
│ │ │ ├── redux
│ │ │ │ ├── QuickTranslateScreenIntention.kt
│ │ │ │ ├── QuickTranslateScreenSideEffect.kt
│ │ │ │ └── QuickTranslateScreenState.kt
│ │ │ └── screens
│ │ │ │ ├── QuickTranslateScreen.kt
│ │ │ │ └── QuickTranslateScreenViewModel.kt
│ │ │ └── translatefeature
│ │ │ ├── redux
│ │ │ ├── TranslateScreenIntention.kt
│ │ │ ├── TranslateScreenSideEffect.kt
│ │ │ └── TranslateScreenState.kt
│ │ │ └── screens
│ │ │ ├── TranslateScreen.kt
│ │ │ └── TranslateScreenViewModel.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_foreground.xml
│ │ └── ic_launcher_monochrome_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values-fr
│ │ └── strings.xml
│ │ ├── values-it
│ │ └── strings.xml
│ │ ├── values-ja
│ │ └── strings.xml
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values-pt-rBR
│ │ └── strings.xml
│ │ ├── values-pt
│ │ └── strings.xml
│ │ ├── values-ru
│ │ └── strings.xml
│ │ ├── values-tr
│ │ └── strings.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── dev
│ └── atajan
│ └── lingva_android
│ └── common
│ └── domain
│ └── language
│ └── LanguageTest.kt
├── build.gradle.kts
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 15.txt
│ ├── 16.txt
│ ├── 17.txt
│ ├── 18.txt
│ ├── 19.txt
│ ├── 20.txt
│ ├── 21.txt
│ └── 22.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ ├── short_description.txt
│ └── title.txt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/ci-gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.daemon=false
2 | org.gradle.parallel=true
3 | org.gradle.jvmargs=-Xmx5120m
4 | org.gradle.workers.max=2
5 |
6 | kotlin.incremental=false
7 | kotlin.compiler.execution.strategy=in-process
--------------------------------------------------------------------------------
/.github/workflows/fastlane-metadata.yaml:
--------------------------------------------------------------------------------
1 | name: Fastlane Metadata
2 | on:
3 | push:
4 | branches:
5 | - main
6 | - "[0-9]+.[0-9]+.x"
7 | tags:
8 | - "[0-9]+.[0-9]+.[0-9]+"
9 | paths:
10 | - "fastlane/**"
11 | - ".github/workflows/fastlane-metadata.yaml"
12 | pull_request:
13 | branches:
14 | - main
15 | - "[0-9]+.[0-9]+.x"
16 | paths:
17 | - "fastlane/**"
18 | - ".github/workflows/fastlane-metadata.yaml"
19 |
20 | jobs:
21 | validate:
22 | name: Validate
23 | runs-on: ubuntu-latest
24 | timeout-minutes: 15
25 | steps:
26 | - uses: actions/checkout@v3
27 | - uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # MacOS
2 | *.DS_Store
3 |
4 | # Built application files
5 | *.apk
6 | *.ap_
7 | *.aab
8 |
9 | # Files for the ART/Dalvik VM
10 | *.dex
11 |
12 | # Java class files
13 | *.class
14 |
15 | # Generated files
16 | bin/
17 | gen/
18 | out/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | .idea/caches
48 |
49 | # Keystore files
50 | # Uncomment the following lines if you do not want to check your keystore files in.
51 | #*.jks
52 | #*.keystore
53 |
54 | # External native build folder generated in Android Studio 2.2 and later
55 | .externalNativeBuild
56 |
57 | # Google Services (e.g. APIs or Firebase)
58 | google-services.json
59 |
60 | # Freeline
61 | freeline.py
62 | freeline/
63 | freeline_project_description.json
64 |
65 | # fastlane
66 | fastlane/report.xml
67 | fastlane/Preview.html
68 | fastlane/screenshots
69 | fastlane/test_output
70 | fastlane/readme.md
71 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | lingva-android
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | xmlns:android
23 |
24 | ^$
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | xmlns:.*
34 |
35 | ^$
36 |
37 |
38 | BY_NAME
39 |
40 |
41 |
42 |
43 |
44 |
45 | .*:id
46 |
47 | http://schemas.android.com/apk/res/android
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | .*:name
57 |
58 | http://schemas.android.com/apk/res/android
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | name
68 |
69 | ^$
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | style
79 |
80 | ^$
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | .*
90 |
91 | ^$
92 |
93 |
94 | BY_NAME
95 |
96 |
97 |
98 |
99 |
100 |
101 | .*
102 |
103 | http://schemas.android.com/apk/res/android
104 |
105 |
106 | ANDROID_ATTRIBUTE_ORDER
107 |
108 |
109 |
110 |
111 |
112 |
113 | .*
114 |
115 | .*
116 |
117 |
118 | BY_NAME
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/PrivacyPolicy.md:
--------------------------------------------------------------------------------
1 | # LENTIL TRANSLATE PRIVACY POLICY
2 | **Privacy Policy**
3 |
4 | Lentil Translate app is built as an Open Source app. This SERVICE is provided by Yaxarat at no cost and is intended for use as is.
5 |
6 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.
7 |
8 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.
9 |
10 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which are accessible at Lentil Translate unless otherwise defined in this Privacy Policy.
11 |
12 | **Information Collection and Use**
13 |
14 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way.
15 |
16 | The app does use third-party services that may collect information used to identify you.
17 |
18 | Link to the privacy policy of third-party service providers used by the app
19 |
20 | * [Google Play Services](https://www.google.com/policies/privacy/)
21 |
22 | **Log Data**
23 |
24 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.
25 |
26 | **Cookies**
27 |
28 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
29 |
30 | This Service does not use these “cookies” explicitly. However, the app may use third-party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.
31 |
32 | **Service Providers**
33 |
34 | I may employ third-party companies and individuals due to the following reasons:
35 |
36 | * To facilitate our Service;
37 | * To provide the Service on our behalf;
38 | * To perform Service-related services; or
39 | * To assist us in analyzing how our Service is used.
40 |
41 | I want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.
42 |
43 | **Security**
44 |
45 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.
46 |
47 | **Children’s Privacy**
48 |
49 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions.
50 |
51 | **Changes to This Privacy Policy**
52 |
53 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page.
54 |
55 | This policy is effective as of 2022-05-05
56 |
57 | **Contact Us**
58 |
59 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at ya.atajan@gmail.com.
60 |
61 | This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/)
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lentil Translate
2 | An unofficial Android client for [Lingva Translate](https://github.com/TheDavidDelta/lingva-translate)
3 |
4 | > If you want to try on the web client:
5 | https://lingva.ml
6 |
7 | [
](https://f-droid.org/packages/dev.atajan.lingva_android)
10 | [
](https://play.google.com/store/apps/details?id=dev.atajan.lingva_android)
13 |
14 | ---
15 |
16 | ## Features
17 |
18 | • `It's open source.`
19 |
20 | • `It's privacy respecting.`
21 | > Check our [privacy policy](https://github.com/yaxarat/lingvaandroid/blob/main/PrivacyPolicy.md)
22 |
23 | • `Audio playbacks for your translations.`
24 |
25 | • `Supports multiple themes, including Material You.`
26 |
27 | • `Custom Lingva
28 | endpoints.`
29 |
30 | ---
31 |
32 | ## Screenshots
33 |
34 |
35 |
36 |
37 |

38 |
39 |

40 |
41 |

42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("dagger.hilt.android.plugin")
4 | kotlin("kapt")
5 | kotlin("android")
6 | kotlin("plugin.serialization")
7 | }
8 |
9 | android {
10 | compileSdk = 34
11 |
12 | defaultConfig {
13 | applicationId = "dev.atajan.lingva_android"
14 | minSdk = 26
15 | targetSdk = 34
16 | versionCode = 22
17 | versionName = "1.3.4"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary = true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | postprocessing {
28 | isRemoveUnusedCode = false
29 | isRemoveUnusedResources = true
30 | isObfuscate = false
31 | isOptimizeCode = true
32 | }
33 | }
34 | debug { }
35 | }
36 | compileOptions {
37 | sourceCompatibility = JavaVersion.VERSION_1_8
38 | targetCompatibility = JavaVersion.VERSION_1_8
39 | }
40 | kotlinOptions {
41 | jvmTarget = "1.8"
42 | }
43 | buildFeatures {
44 | compose = true
45 | }
46 | composeOptions {
47 | kotlinCompilerExtensionVersion = "1.4.3"
48 | }
49 | packagingOptions { // https://stackoverflow.com/a/47509465/8685398
50 | resources.excludes.add("META-INF/DEPENDENCIES")
51 | resources.excludes.add("META-INF/AL2.0")
52 | resources.excludes.add("META-INF/LGPL2.1")
53 | }
54 | namespace = "dev.atajan.lingva_android"
55 | }
56 |
57 | kotlin.sourceSets.all {
58 | languageSettings.apply {
59 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
60 | optIn("androidx.compose.ui.ExperimentalComposeUiApi")
61 | optIn("androidx.compose.foundation.ExperimentalFoundationApi")
62 | }
63 | }
64 |
65 | dependencies {
66 | implementation("androidx.core:core-ktx:1.12.0")
67 | implementation("androidx.appcompat:appcompat:1.6.1")
68 | implementation("com.google.android.material:material:1.9.0")
69 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
70 | implementation("androidx.activity:activity-compose:1.7.2")
71 |
72 | // Accompanist
73 | implementation("com.google.accompanist:accompanist-systemuicontroller:0.30.1")
74 |
75 | // Datastore
76 | implementation("androidx.datastore:datastore-preferences:1.0.0")
77 |
78 | // Result
79 | implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.14")
80 |
81 | // Ktor
82 | implementation("io.ktor:ktor-client-android:2.1.2")
83 | implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.2")
84 | implementation("io.ktor:ktor-client-logging-jvm:2.1.2")
85 | implementation("io.ktor:ktor-client-content-negotiation-jvm:2.1.2")
86 |
87 | // Compose
88 | implementation("androidx.compose.ui:ui:1.5.1")
89 | implementation("androidx.compose.material:material:1.5.1")
90 | implementation("androidx.compose.animation:animation:1.5.1")
91 | implementation("androidx.compose.ui:ui-tooling-preview:1.5.1")
92 | implementation("androidx.compose.material3:material3-android:1.2.0-alpha07")
93 |
94 | // note that due to the very large size of this dependency you should make sure to use
95 | // R8 / ProGuard to remove unused icons from your application.
96 | implementation("androidx.compose.material:material-icons-extended:1.5.1")
97 |
98 | // Hilt
99 | implementation("com.google.dagger:hilt-android:2.47")
100 | kapt("com.google.dagger:hilt-android-compiler:2.47")
101 |
102 | testImplementation("junit:junit:4.13.2")
103 | debugImplementation("androidx.compose.ui:ui-tooling:1.5.1")
104 | }
105 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/atajan/lingva_android/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("dev.atajan.lingva_android", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaxarat/lingvaandroid/a159c29933e037f31b11f68b4a243b82bef0be52/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
7 | import androidx.compose.foundation.isSystemInDarkTheme
8 | import androidx.compose.material.ExperimentalMaterialApi
9 | import androidx.compose.runtime.MutableState
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.lifecycle.lifecycleScope
12 | import dagger.hilt.android.AndroidEntryPoint
13 | import dev.atajan.lingva_android.common.ui.theme.LingvaAndroidTheme
14 | import dev.atajan.lingva_android.common.ui.theme.ThemingOptions
15 | import dev.atajan.lingva_android.common.ui.theme.canUseDynamicColor
16 | import dev.atajan.lingva_android.common.ui.theme.isSystemInNightMode
17 | import dev.atajan.lingva_android.common.ui.theme.selectedThemeFlow
18 | import dev.atajan.lingva_android.translatefeature.screens.TranslateScreenViewModel
19 | import dev.atajan.lingva_android.translatefeature.screens.TranslationScreen
20 | import kotlinx.coroutines.flow.launchIn
21 | import kotlinx.coroutines.flow.onEach
22 |
23 | @ExperimentalMaterialApi
24 | @AndroidEntryPoint
25 | class MainActivity : ComponentActivity() {
26 |
27 | private val viewModel: TranslateScreenViewModel by viewModels()
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 |
32 | val theme: MutableState = mutableStateOf(
33 | if (canUseDynamicColor) {
34 | ThemingOptions.YOU
35 | } else if (isSystemInNightMode(this)) {
36 | ThemingOptions.DARK
37 | } else {
38 | ThemingOptions.LIGHT
39 | }
40 | )
41 |
42 | selectedThemeFlow(applicationContext)
43 | .onEach {
44 | it?.let { theme.value = ThemingOptions.valueOf(it) }
45 | }
46 | .launchIn(lifecycleScope)
47 |
48 | setContent {
49 | isSystemInDarkTheme()
50 | LingvaAndroidTheme(appTheme = theme.value) {
51 | TranslationScreen(
52 | viewModel = viewModel,
53 | currentTheme = theme
54 | )
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class MainApplication : Application()
8 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/QuickTranslateActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.viewModels
9 | import androidx.compose.foundation.isSystemInDarkTheme
10 | import androidx.compose.material.ExperimentalMaterialApi
11 | import androidx.compose.runtime.collectAsState
12 | import androidx.compose.runtime.getValue
13 | import dagger.hilt.android.AndroidEntryPoint
14 | import dev.atajan.lingva_android.common.ui.theme.LingvaAndroidTheme
15 | import dev.atajan.lingva_android.common.ui.theme.ThemingOptions
16 | import dev.atajan.lingva_android.common.ui.theme.canUseDynamicColor
17 | import dev.atajan.lingva_android.common.ui.theme.selectedThemeFlow
18 | import dev.atajan.lingva_android.quicktranslatefeature.screens.QuickTranslateScreen
19 | import dev.atajan.lingva_android.quicktranslatefeature.screens.QuickTranslateScreenViewModel
20 |
21 | @ExperimentalMaterialApi
22 | @AndroidEntryPoint
23 | class QuickTranslateActivity : ComponentActivity() {
24 |
25 | private val viewModel: QuickTranslateScreenViewModel by viewModels()
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 |
30 | setContent {
31 | val selectedTheme: String? by selectedThemeFlow(applicationContext)
32 | .collectAsState(initial = null)
33 |
34 | val appTheme: ThemingOptions = if (selectedTheme.isNullOrBlank()) {
35 | if (canUseDynamicColor) {
36 | ThemingOptions.YOU
37 | } else if (isSystemInDarkTheme()) {
38 | ThemingOptions.DARK
39 | } else {
40 | ThemingOptions.LIGHT
41 | }
42 | } else {
43 | ThemingOptions.valueOf(selectedTheme!!)
44 | }
45 |
46 | LingvaAndroidTheme(appTheme = appTheme) {
47 | QuickTranslateScreen(
48 | textToTranslate = getTextToTranslate(),
49 | viewModel = viewModel
50 | )
51 | }
52 | }
53 | }
54 |
55 | override fun onPause() {
56 | super.onPause()
57 | finish()
58 | }
59 |
60 |
61 | private fun getTextToTranslate(): String {
62 | val textToQuickTranslate = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
63 | intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
64 | } else {
65 | null
66 | }
67 |
68 | return (textToQuickTranslate ?: intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)) as String
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/KtorLingvaApi.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import dev.atajan.lingva_android.common.data.api.constants.AUDIO_PATH_SEGMENT
6 | import dev.atajan.lingva_android.common.data.api.constants.SUPPORTED_LANGUAGE_PATH_SEGMENT
7 | import dev.atajan.lingva_android.common.data.api.lingvadto.audio.AudioDTO
8 | import dev.atajan.lingva_android.common.data.api.lingvadto.language.LanguagesDTO
9 | import dev.atajan.lingva_android.common.data.api.lingvadto.translation.TranslationDTO
10 | import dev.atajan.lingva_android.common.data.datasource.impl.CUSTOM_LINGVA_ENDPOINT
11 | import dev.atajan.lingva_android.common.domain.errors.LingvaApiError.BadCustomEndpoint
12 | import dev.atajan.lingva_android.common.domain.errors.LingvaApiError.BadEndpoints
13 | import io.ktor.client.HttpClient
14 | import io.ktor.client.call.body
15 | import io.ktor.client.request.get
16 | import io.ktor.client.statement.HttpResponse
17 | import io.ktor.http.appendPathSegments
18 | import kotlinx.coroutines.Dispatchers
19 | import kotlinx.coroutines.flow.first
20 | import kotlinx.coroutines.withContext
21 | import javax.inject.Inject
22 |
23 | class KtorLingvaApi @Inject constructor(
24 | private val androidHttpClient: HttpClient,
25 | private val dataStore: DataStore,
26 | private val endpoints: List
27 | ) : LingvaApi {
28 |
29 | /**
30 | * Requesting list of supported languages from Lingva API
31 | */
32 | override suspend fun getSupportedLanguages() = attemptSupportedLanguagesRequest()
33 |
34 | private suspend fun attemptSupportedLanguagesRequest(endpointIndex: Int = 0): LanguagesDTO {
35 |
36 | // Throw only when all fallback endpoints have been exhausted
37 | if (endpointIndex > endpoints.lastIndex) throw BadEndpoints
38 |
39 | return try {
40 | val response = androidHttpClient
41 | .get(endpoints[endpointIndex] + SUPPORTED_LANGUAGE_PATH_SEGMENT)
42 |
43 | if (response.isSuccessful()) return response.body() else throw BadEndpoints
44 | } catch (e: Exception) {
45 | attemptSupportedLanguagesRequest(endpointIndex = endpointIndex + 1)
46 | }
47 | }
48 |
49 | /**
50 | * Requesting translation from Lingva API
51 | */
52 | override suspend fun translate(
53 | source: String,
54 | target: String,
55 | query: String
56 | ): TranslationDTO {
57 | val customEndpoint = getCustomEndpoint()
58 | val encodedQuery = withContext(Dispatchers.IO) {
59 | escapeQuery(query.trim())
60 | }
61 |
62 | return if (customEndpoint.isNotEmpty()) {
63 | try {
64 | requestToEndpoint(
65 | source = source,
66 | target = target,
67 | query = encodedQuery,
68 | endpoint = customEndpoint
69 | )
70 | } catch (e: Exception) {
71 | throw BadCustomEndpoint
72 | }
73 | } else {
74 | attemptTranslationRequest(
75 | source = source,
76 | target = target,
77 | query = encodedQuery
78 | )
79 | }
80 | }
81 |
82 | private suspend fun getCustomEndpoint(): String {
83 | return dataStore.data.first()[CUSTOM_LINGVA_ENDPOINT] ?: ""
84 | }
85 |
86 | private suspend fun attemptTranslationRequest(
87 | source: String,
88 | target: String,
89 | query: String,
90 | endpointIndex: Int = 0
91 | ): TranslationDTO {
92 | // Throw only when all fallback endpoints have been exhausted
93 | if (endpointIndex > endpoints.lastIndex) throw BadEndpoints
94 |
95 | return try {
96 | requestToEndpoint(
97 | source = source,
98 | target = target,
99 | query = query,
100 | endpoint = endpoints[endpointIndex]
101 | )
102 | } catch (e: Exception) {
103 | attemptTranslationRequest(
104 | source = source,
105 | target = target,
106 | query = query,
107 | endpointIndex = endpointIndex + 1
108 | )
109 | }
110 | }
111 |
112 | private suspend fun requestToEndpoint(
113 | source: String,
114 | target: String,
115 | query: String,
116 | endpoint: String
117 | ): TranslationDTO {
118 | val response = androidHttpClient.get(endpoint) {
119 | url {
120 | appendPathSegments(source, target, query)
121 | }
122 | }
123 |
124 | if (response.isSuccessful()) return response.body() else throw BadEndpoints
125 | }
126 |
127 | /**
128 | * Requesting audio from Lingva API
129 | */
130 | override suspend fun getAudio(
131 | language: String,
132 | query: String
133 | ): AudioDTO {
134 | return attemptAudioRequest(
135 | language = language,
136 | query = query
137 | )
138 | }
139 |
140 | private suspend fun attemptAudioRequest(
141 | language: String,
142 | query: String,
143 | endpointIndex: Int = 0
144 | ): AudioDTO {
145 | // Throw only when all fallback endpoints have been exhausted
146 | if (endpointIndex > endpoints.lastIndex) throw BadEndpoints
147 |
148 | return try {
149 | requestToAudioEndoint(
150 | language = language,
151 | query = query,
152 | endpoint = endpoints[endpointIndex]
153 | )
154 | } catch (e: Exception) {
155 | attemptAudioRequest(
156 | language = language,
157 | query = query,
158 | endpointIndex = endpointIndex + 1
159 | )
160 | }
161 | }
162 |
163 | private suspend fun requestToAudioEndoint(
164 | language: String,
165 | query: String,
166 | endpoint: String
167 | ): AudioDTO {
168 | val response = androidHttpClient.get(endpoint + AUDIO_PATH_SEGMENT) {
169 | url {
170 | appendPathSegments(language, query)
171 | }
172 | }
173 |
174 | if (response.isSuccessful()) return response.body() else throw BadEndpoints
175 | }
176 |
177 |
178 | /**
179 | * Helper functions
180 | */
181 | private fun escapeQuery(query: String): String {
182 | return query.replace("/", "%2F")
183 | }
184 |
185 | private fun HttpResponse.isSuccessful() = status.value in 200..299
186 | }
187 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/LingvaApi.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.audio.AudioDTO
4 | import dev.atajan.lingva_android.common.data.api.lingvadto.language.LanguagesDTO
5 | import dev.atajan.lingva_android.common.data.api.lingvadto.translation.TranslationDTO
6 |
7 | interface LingvaApi {
8 |
9 | suspend fun translate(
10 | source: String,
11 | target: String,
12 | query: String
13 | ): TranslationDTO
14 |
15 | suspend fun getSupportedLanguages(): LanguagesDTO
16 |
17 | suspend fun getAudio(
18 | language: String,
19 | query: String
20 | ): AudioDTO
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/constants/ApiConstants.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.constants
2 |
3 | internal const val LINGVA = "https://lingva.ml/api/v1/"
4 | //internal const val PLAUSIBILITY = "https://translate.plausibility.cloud/api/v1/" // DOES NOT INCLUDE TRANSLATION INFO IN RESPONSE!
5 | internal const val PROJECTSEGFAU = "https://translate.projectsegfau.lt/api/v1/"
6 | internal const val DR460NF1R3 = "https://translate.dr460nf1r3.org/api/v1/"
7 | internal const val GARUDALINUX = "https://lingva.garudalinux.org/api/v1/"
8 |
9 | internal val TRANSLATION_PROVIDERS by lazy {
10 | listOf(
11 | LINGVA,
12 | // PLAUSIBILITY,
13 | PROJECTSEGFAU,
14 | DR460NF1R3,
15 | GARUDALINUX
16 | )
17 | }
18 |
19 | internal const val SUPPORTED_LANGUAGE_PATH_SEGMENT = "languages/?:(source|target)"
20 |
21 | internal const val AUDIO_PATH_SEGMENT = "audio/"
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/audio/AudioDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.audio
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class AudioDTO(val audio : ArrayList? = null)
9 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/language/LanguageDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.language
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class LanguageDTO(
9 | val code: String? = null,
10 | val name: String? = null,
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/language/LanguagesDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.language
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class LanguagesDTO(
9 | val languages: List? = null,
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/DefinitionDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class DefinitionDTO(
9 | val list: List? = null,
10 | val type: String? = null,
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/DefinitionInfoDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class DefinitionInfoDTO(
9 | val definition: String? = null,
10 | val example: String? = null,
11 | val synonyms: List? = null,
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/ExtraTranslationDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class ExtraTranslationDTO(
9 | val list: List? = null,
10 | val type: String? = null,
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/ExtraTranslationListDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class ExtraTranslationListDTO(
9 | val frequency: Int? = null,
10 | val meanings: List? = null,
11 | val word: String? = null,
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/InfoDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class InfoDTO(
9 | val detectedSource: String? = null,
10 | val definitions: List? = null,
11 | val examples: List? = null,
12 | val extraTranslation: List? = null,
13 | val pronunciation: PronunciationDTO? = null,
14 | val similar: List? = null,
15 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/PronunciationDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class PronunciationDTO(
9 | val query: String? = null,
10 | val translation: String? = null
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/api/lingvadto/translation/TranslationDTO.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.api.lingvadto.translation
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.serialization.Serializable
5 |
6 | @Keep
7 | @Serializable
8 | data class TranslationDTO(
9 | val info: InfoDTO? = null,
10 | val translation: String? = null,
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/AudioRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource
2 |
3 | import dev.atajan.lingva_android.common.domain.results.AudioRepositoryResponse
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface AudioRepository {
7 |
8 | val audioRequestResult: Flow
9 |
10 | fun requestAudio(
11 | language: String,
12 | query: String
13 | )
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/LanguagesRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource
2 |
3 | import dev.atajan.lingva_android.common.domain.results.LanguagesRepositoryResponse
4 |
5 | interface LanguagesRepository {
6 |
7 | suspend fun fetchSupportedLanguages(): LanguagesRepositoryResponse
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/TranslationRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource
2 |
3 | import dev.atajan.lingva_android.common.domain.models.translation.Translation
4 | import dev.atajan.lingva_android.common.domain.models.translation.TranslationWithInfo
5 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface TranslationRepository {
9 |
10 | /**
11 | * Emits translation result as either [Translation] or [TranslationWithInfo] depending on
12 | * whether [translate] has requireInfo set to true or not.
13 | */
14 | val translationResult: Flow
15 |
16 | /**
17 | * Translates the [query] from [source] to [target]. [requireInfo] determines whether the translation
18 | * result should be emitted as [Translation] or [TranslationWithInfo].
19 | */
20 | fun translate(
21 | source: String,
22 | target: String,
23 | query: String
24 | )
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/impl/KtorAudioRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource.impl
2 |
3 | import dev.atajan.lingva_android.common.data.api.KtorLingvaApi
4 | import dev.atajan.lingva_android.common.data.datasource.AudioRepository
5 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError
6 | import dev.atajan.lingva_android.common.domain.errors.LingvaApiError
7 | import dev.atajan.lingva_android.common.domain.models.audio.Audio.Companion.toAudioDomain
8 | import dev.atajan.lingva_android.common.domain.results.AudioRepositoryResponse
9 | import dev.atajan.lingva_android.common.domain.results.AudioRepositoryResponse.Failure
10 | import dev.atajan.lingva_android.common.domain.results.AudioRepositoryResponse.Success
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.flow.MutableSharedFlow
14 | import kotlinx.coroutines.launch
15 |
16 | class KtorAudioRepository(
17 | private val api: KtorLingvaApi,
18 | applicationScope: CoroutineScope,
19 | ) : AudioRepository, CoroutineScope by applicationScope {
20 |
21 | override val audioRequestResult: MutableSharedFlow = MutableSharedFlow()
22 |
23 | override fun requestAudio(
24 | language: String,
25 | query: String
26 | ) {
27 | launch(Dispatchers.IO) {
28 | val rawAudio = try {
29 | api.getAudio(
30 | language = language,
31 | query = query
32 | )
33 | } catch (error: LingvaApiError) {
34 | Failure(error.message).emit()
35 | return@launch
36 | }
37 |
38 | try {
39 | Success(rawAudio.toAudioDomain()).emit()
40 | } catch (error: DTOToDomainModelMappingError) {
41 | Failure(error.message).emit()
42 | }
43 | }
44 | }
45 |
46 | private suspend fun AudioRepositoryResponse.emit() {
47 | audioRequestResult.emit(this)
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/impl/KtorLanguagesRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource.impl
2 |
3 | import dev.atajan.lingva_android.common.data.api.KtorLingvaApi
4 | import dev.atajan.lingva_android.common.data.datasource.LanguagesRepository
5 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError
6 | import dev.atajan.lingva_android.common.domain.errors.LingvaApiError
7 | import dev.atajan.lingva_android.common.domain.models.language.Language.Companion.toDomainModel
8 | import dev.atajan.lingva_android.common.domain.results.LanguagesRepositoryResponse
9 | import dev.atajan.lingva_android.common.domain.results.LanguagesRepositoryResponse.Failure
10 | import dev.atajan.lingva_android.common.domain.results.LanguagesRepositoryResponse.Success
11 |
12 | class KtorLanguagesRepository(private val api: KtorLingvaApi) : LanguagesRepository {
13 |
14 | // No need to hit remote on every check, we can cache the result.
15 | private var supportedLanguagesReceived: LanguagesRepositoryResponse? = null
16 |
17 | override suspend fun fetchSupportedLanguages(): LanguagesRepositoryResponse {
18 | return if (supportedLanguagesReceived != null && supportedLanguagesReceived is Success) {
19 | supportedLanguagesReceived as Success
20 | } else {
21 | getFromRemote().also { supportedLanguagesReceived = it }
22 | }
23 | }
24 |
25 | private suspend fun getFromRemote(): LanguagesRepositoryResponse {
26 | val supportedLanguagesDTO = try {
27 | api.getSupportedLanguages()
28 | } catch (error: LingvaApiError) {
29 | return Failure(error.message)
30 | }
31 |
32 | with(supportedLanguagesDTO) {
33 | if (languages.isNullOrEmpty()) {
34 | return Failure("Languages can't be null or empty")
35 | }
36 |
37 | languages.let { languages ->
38 | return try {
39 | languages
40 | .map { it.toDomainModel() }
41 | .let { Success(it) }
42 | } catch (error: DTOToDomainModelMappingError) {
43 | Failure(error.message)
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/impl/KtorTranslationRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource.impl
2 |
3 | import dev.atajan.lingva_android.common.data.api.KtorLingvaApi
4 | import dev.atajan.lingva_android.common.data.datasource.TranslationRepository
5 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError
6 | import dev.atajan.lingva_android.common.domain.errors.LingvaApiError
7 | import dev.atajan.lingva_android.common.domain.models.translation.TranslationWithInfo.Companion.toTranslationWithInfoDomain
8 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse
9 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse.Failure
10 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse.Loading
11 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse.Success
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.launch
16 |
17 | class KtorTranslationRepository(
18 | private val api: KtorLingvaApi,
19 | applicationScope: CoroutineScope,
20 | ) : TranslationRepository, CoroutineScope by applicationScope {
21 |
22 | override val translationResult: MutableStateFlow = MutableStateFlow(Loading)
23 |
24 | override fun translate(
25 | source: String,
26 | target: String,
27 | query: String
28 | ) {
29 | launch(Dispatchers.IO) {
30 | val translated = try {
31 | api.translate(
32 | source = source,
33 | target = target,
34 | query = query
35 | )
36 | } catch (error: LingvaApiError) {
37 | Failure(error.message).emit()
38 | return@launch
39 | }
40 |
41 | try {
42 | Success(translated.toTranslationWithInfoDomain()).emit()
43 | } catch (error: DTOToDomainModelMappingError) {
44 | Failure(error.message).emit()
45 | }
46 | }
47 | }
48 |
49 | private fun TranslationRepositoryResponse.emit() {
50 | translationResult.value = this
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/data/datasource/impl/PreferencesDatastore.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.data.datasource.impl
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.booleanPreferencesKey
7 | import androidx.datastore.preferences.core.stringPreferencesKey
8 | import androidx.datastore.preferences.preferencesDataStore
9 |
10 | /**
11 | * Name of the datastore.
12 | */
13 | private const val PREFERENCE_NAME: String = "settings"
14 |
15 | /**
16 | * Corresponding key type function to define a key for each value that you need to store.
17 | */
18 | val APP_THEME: Preferences.Key = stringPreferencesKey("app_theme")
19 | val DEFAULT_SOURCE_LANGUAGE: Preferences.Key = stringPreferencesKey("default_source_language")
20 | val DEFAULT_TARGET_LANGUAGE: Preferences.Key = stringPreferencesKey("default_target_language")
21 | val CUSTOM_LINGVA_ENDPOINT: Preferences.Key = stringPreferencesKey("custom_lingva_endpoint")
22 | val LIVE_TRANSLATE_ENABLED: Preferences.Key = booleanPreferencesKey("live_translate_enabled")
23 |
24 | /**
25 | * Use the property delegate to create an instance of Datastore.
26 | * Call it once at the top level of your kotlin file, and access it through this property throughout the rest of your application.
27 | */
28 | val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCE_NAME)
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/AppScopeModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object AppScopeModule {
14 |
15 | @Singleton
16 | @Provides
17 | fun provideApplicationScope(): CoroutineScope {
18 | return CoroutineScope(Dispatchers.IO)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/AudioPlayerModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dev.atajan.lingva_android.common.media.AudioPlayer
8 | import dev.atajan.lingva_android.common.media.NativeAudioPlayer
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object AudioPlayerModule {
14 |
15 | @Singleton
16 | @Provides
17 | fun provideAudioPlayer(): AudioPlayer {
18 | return NativeAudioPlayer()
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/ClipboardManagerModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import android.content.ClipboardManager
4 | import android.content.Context
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | object ClipboardManagerModule {
15 |
16 | @Singleton
17 | @Provides
18 | fun provideClipboardManager(@ApplicationContext application: Context): ClipboardManager {
19 | return application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/DataStoreModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import dev.atajan.lingva_android.common.data.datasource.impl.dataStore
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | object DataStoreModule {
17 |
18 | @Singleton
19 | @Provides
20 | fun provideDataStore(@ApplicationContext application: Context): DataStore {
21 | return application.applicationContext.dataStore
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/LingvaApiModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import android.util.Log
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import dev.atajan.lingva_android.common.data.api.KtorLingvaApi
11 | import dev.atajan.lingva_android.common.data.api.LingvaApi
12 | import dev.atajan.lingva_android.common.data.api.constants.TRANSLATION_PROVIDERS
13 | import io.ktor.client.HttpClient
14 | import io.ktor.client.engine.android.Android
15 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
16 | import io.ktor.client.plugins.logging.LogLevel
17 | import io.ktor.client.plugins.logging.Logger
18 | import io.ktor.client.plugins.logging.Logging
19 | import io.ktor.http.ContentType
20 | import io.ktor.serialization.ContentConverter
21 | import io.ktor.serialization.kotlinx.KotlinxSerializationConverter
22 | import kotlinx.serialization.ExperimentalSerializationApi
23 | import kotlinx.serialization.json.Json
24 | import javax.inject.Singleton
25 |
26 | @Module
27 | @InstallIn(SingletonComponent::class)
28 | object LingvaApiModule {
29 |
30 | @OptIn(ExperimentalSerializationApi::class)
31 | @Singleton
32 | @Provides
33 | fun provideKotlinxSerializationConverter(): ContentConverter {
34 | return KotlinxSerializationConverter(
35 | Json {
36 | prettyPrint = true
37 | isLenient = true
38 | ignoreUnknownKeys = true
39 | explicitNulls = false
40 | }
41 | )
42 | }
43 |
44 | @Singleton
45 | @Provides
46 | fun provideAndroidHttpClient(kotlinxSerializationConverter: ContentConverter): HttpClient {
47 | return HttpClient(Android) {
48 | install(ContentNegotiation) {
49 | register(
50 | contentType = ContentType.Application.Json,
51 | converter = kotlinxSerializationConverter
52 | )
53 | }
54 |
55 | install(Logging) {
56 | logger = object : Logger {
57 | override fun log(message: String) {
58 | Log.d("Logger Ktor =>", message)
59 | }
60 | }
61 | level = LogLevel.INFO
62 | }
63 | }
64 | }
65 |
66 | @Singleton
67 | @Provides
68 | fun provideTranslationEndpoints(): List {
69 | return TRANSLATION_PROVIDERS
70 | }
71 |
72 | @Singleton
73 | @Provides
74 | fun provideLingvaApi(
75 | androidHttpClient: HttpClient,
76 | dataStore: DataStore,
77 | endpoints: List
78 | ): LingvaApi {
79 | return KtorLingvaApi(
80 | androidHttpClient = androidHttpClient,
81 | dataStore = dataStore,
82 | endpoints = endpoints
83 | )
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dev.atajan.lingva_android.common.data.api.KtorLingvaApi
8 | import dev.atajan.lingva_android.common.data.datasource.AudioRepository
9 | import dev.atajan.lingva_android.common.data.datasource.LanguagesRepository
10 | import dev.atajan.lingva_android.common.data.datasource.TranslationRepository
11 | import dev.atajan.lingva_android.common.data.datasource.impl.KtorAudioRepository
12 | import dev.atajan.lingva_android.common.data.datasource.impl.KtorLanguagesRepository
13 | import dev.atajan.lingva_android.common.data.datasource.impl.KtorTranslationRepository
14 | import kotlinx.coroutines.CoroutineScope
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | object RepositoryModule {
20 |
21 | @Singleton
22 | @Provides
23 | fun provideLanguagesRepository(api: KtorLingvaApi): LanguagesRepository {
24 | return KtorLanguagesRepository(api)
25 | }
26 |
27 | @Singleton
28 | @Provides
29 | fun provideTranslationRepository(
30 | api: KtorLingvaApi,
31 | applicationScope: CoroutineScope
32 | ): TranslationRepository {
33 | return KtorTranslationRepository(
34 | api = api,
35 | applicationScope = applicationScope
36 | )
37 | }
38 |
39 | @Singleton
40 | @Provides
41 | fun provideAudioRepository(
42 | api: KtorLingvaApi,
43 | applicationScope: CoroutineScope
44 | ): AudioRepository {
45 | return KtorAudioRepository(
46 | api = api,
47 | applicationScope = applicationScope
48 | )
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dev.atajan.lingva_android.common.data.datasource.AudioRepository
8 | import dev.atajan.lingva_android.common.data.datasource.LanguagesRepository
9 | import dev.atajan.lingva_android.common.data.datasource.TranslationRepository
10 | import dev.atajan.lingva_android.common.media.AudioPlayer
11 | import dev.atajan.lingva_android.common.usecases.FetchSupportedLanguagesUseCase
12 | import dev.atajan.lingva_android.common.usecases.ObserveAudioDataUseCase
13 | import dev.atajan.lingva_android.common.usecases.ObserveTranslationResultUseCase
14 | import dev.atajan.lingva_android.common.usecases.PlayByteArrayAudioUseCase
15 | import dev.atajan.lingva_android.common.usecases.RequestAudioDataUseCase
16 | import dev.atajan.lingva_android.common.usecases.TranslateUseCase
17 | import dev.atajan.lingva_android.common.usecases.ktorimpl.AudioPlayerPlayByteArrayAudioUseCase
18 | import dev.atajan.lingva_android.common.usecases.ktorimpl.KtorFetchSupportedLanguagesUseCase
19 | import dev.atajan.lingva_android.common.usecases.ktorimpl.KtorObserveAudioDataUseCase
20 | import dev.atajan.lingva_android.common.usecases.ktorimpl.KtorObserveTranslationResultUseCase
21 | import dev.atajan.lingva_android.common.usecases.ktorimpl.KtorRequestAudioDataUseCase
22 | import dev.atajan.lingva_android.common.usecases.ktorimpl.KtorTranslateUseCase
23 | import javax.inject.Singleton
24 |
25 | @Module
26 | @InstallIn(SingletonComponent::class)
27 | object UseCaseModule {
28 |
29 | @Singleton
30 | @Provides
31 | fun provideKtorFetchSupportedLanguagesUseCase(languagesRepository: LanguagesRepository): FetchSupportedLanguagesUseCase {
32 | return KtorFetchSupportedLanguagesUseCase(languagesRepository)
33 | }
34 |
35 | @Singleton
36 | @Provides
37 | fun provideKtorTranslateUseCase(translationRepository: TranslationRepository): TranslateUseCase {
38 | return KtorTranslateUseCase(translationRepository)
39 | }
40 |
41 | @Singleton
42 | @Provides
43 | fun provideKtorObserveTranslationResultUseCase(translationRepository: TranslationRepository): ObserveTranslationResultUseCase {
44 | return KtorObserveTranslationResultUseCase(translationRepository)
45 | }
46 |
47 | @Singleton
48 | @Provides
49 | fun provideAudioPlayerPlayByteArrayAudioUseCase(audioPlayer: AudioPlayer): PlayByteArrayAudioUseCase {
50 | return AudioPlayerPlayByteArrayAudioUseCase(audioPlayer)
51 | }
52 |
53 | @Singleton
54 | @Provides
55 | fun provideKtorObserveAudioDataUseCase(audioRepository: AudioRepository): ObserveAudioDataUseCase {
56 | return KtorObserveAudioDataUseCase(audioRepository)
57 | }
58 |
59 | @Singleton
60 | @Provides
61 | fun provideKtorRequestAudioDataUseCase(audioRepository: AudioRepository): RequestAudioDataUseCase {
62 | return KtorRequestAudioDataUseCase(audioRepository)
63 | }
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/errors/Errors.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.errors
2 |
3 | sealed class DTOToDomainModelMappingError(override val message: String) : Exception() {
4 | class NullValue(message: String) : DTOToDomainModelMappingError(message)
5 | }
6 |
7 | sealed class LingvaApiError(override val message: String) : Exception() {
8 | object BadEndpoints : LingvaApiError("All endpoints failed")
9 | object BadCustomEndpoint : LingvaApiError("Invalid custom endpoint")
10 | object TranslationFailure : LingvaApiError("Error during translation request")
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/models/audio/Audio.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.models.audio
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.audio.AudioDTO
4 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError.NullValue
5 |
6 | data class Audio(val audioByteArray: ByteArray) {
7 |
8 | companion object {
9 | fun AudioDTO.toAudioDomain() : Audio {
10 | return Audio(
11 | audio
12 | ?.map { it.toByte() }
13 | ?.toByteArray() ?: throw NullValue("audio can't be null")
14 | )
15 | }
16 | }
17 |
18 | override fun equals(other: Any?): Boolean {
19 | if (this === other) return true
20 | if (javaClass != other?.javaClass) return false
21 |
22 | other as Audio
23 |
24 | if (!audioByteArray.contentEquals(other.audioByteArray)) return false
25 |
26 | return true
27 | }
28 |
29 | override fun hashCode(): Int {
30 | return audioByteArray.contentHashCode()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/models/language/Language.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.models.language
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.language.LanguageDTO
4 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError.NullValue
5 |
6 | data class Language(
7 | val code: String,
8 | val name: String,
9 | ) {
10 | companion object {
11 | fun LanguageDTO.toDomainModel(): Language {
12 | return Language(
13 | code = code ?: throw NullValue("language code can't be null"),
14 | name = name ?: throw NullValue("language name can't be null")
15 | )
16 | }
17 | }
18 | }
19 |
20 | internal fun List.containsLanguageOrNull(languageCode: String): Language? {
21 | return this.find { it.code == languageCode }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/models/translation/Translation.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.models.translation
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.translation.TranslationDTO
4 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError.NullValue
5 |
6 | data class Translation(val result: String) {
7 | companion object {
8 | fun TranslationDTO.toTranslationDomain() : Translation {
9 | return Translation(
10 | result = translation ?: throw NullValue("translation can't be null")
11 | )
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/models/translation/TranslationInfo.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.models.translation
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.translation.InfoDTO
4 |
5 | /**
6 | * Although the API returns more information about the translation,
7 | * only subset of it is used for the app features at the moment.
8 | *
9 | * See [InfoDTO] for other fields returned by the API.
10 | */
11 | data class TranslationInfo(
12 | val detectedSource: String,
13 | val pronunciation: String,
14 | ) {
15 | companion object {
16 | fun InfoDTO.toTranslationInfoDomain() : TranslationInfo {
17 | return TranslationInfo(
18 | detectedSource = detectedSource ?: "",
19 | pronunciation = pronunciation?.translation ?: ""
20 | )
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/models/translation/TranslationWithInfo.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.models.translation
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.translation.TranslationDTO
4 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError.NullValue
5 | import dev.atajan.lingva_android.common.domain.models.translation.Translation.Companion.toTranslationDomain
6 | import dev.atajan.lingva_android.common.domain.models.translation.TranslationInfo.Companion.toTranslationInfoDomain
7 |
8 | data class TranslationWithInfo(
9 | val translation: Translation,
10 | val info: TranslationInfo
11 | ) {
12 | companion object {
13 | fun TranslationDTO.toTranslationWithInfoDomain() : TranslationWithInfo {
14 | return TranslationWithInfo(
15 | translation = toTranslationDomain(),
16 | info = info?.toTranslationInfoDomain() ?: throw NullValue("translation info can't be null")
17 | )
18 | }
19 |
20 | fun TranslationWithInfo.toTranslation() : Translation {
21 | return Translation(this.translation.result)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/domain/results/RepositoryResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.results
2 |
3 | import dev.atajan.lingva_android.common.domain.models.audio.Audio
4 | import dev.atajan.lingva_android.common.domain.models.language.Language
5 | import dev.atajan.lingva_android.common.domain.models.translation.TranslationWithInfo
6 |
7 | sealed interface LanguagesRepositoryResponse {
8 | data class Success(val languageList: List) : LanguagesRepositoryResponse
9 | data class Failure(val errorMessage: String) : LanguagesRepositoryResponse
10 | }
11 |
12 | sealed interface TranslationRepositoryResponse {
13 | data class Success(val response: TranslationWithInfo) : TranslationRepositoryResponse
14 | data class Failure(val errorMessage: String) : TranslationRepositoryResponse
15 | object Loading : TranslationRepositoryResponse
16 | }
17 |
18 | sealed interface AudioRepositoryResponse {
19 | data class Success(val audio: Audio) : AudioRepositoryResponse
20 | data class Failure(val errorMessage: String) : AudioRepositoryResponse
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/media/AudioPlayer.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.media
2 |
3 | interface AudioPlayer {
4 |
5 | // Play audio from a ByteArray
6 | fun playAudio(audio: ByteArray)
7 |
8 | // Release the MediaPlayer
9 | fun releaseMediaPlayer()
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/media/NativeAudioPlayer.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.media
2 |
3 | import android.media.MediaPlayer
4 | import java.io.File
5 | import javax.inject.Inject
6 |
7 | class NativeAudioPlayer @Inject constructor() : AudioPlayer {
8 |
9 | private var mediaPlayer: MediaPlayer? = null
10 | private var temporaryAudioFile: File? = null
11 |
12 | override fun playAudio(audio: ByteArray) {
13 | if (mediaPlayer != null) releaseMediaPlayer()
14 | mediaPlayer = MediaPlayer()
15 | temporaryAudioFile = createTempAudioFile(audio)
16 |
17 | mediaPlayer?.setOnCompletionListener {
18 | releaseMediaPlayer()
19 | }
20 |
21 | temporaryAudioFile?.let { file ->
22 | mediaPlayer?.apply {
23 | setDataSource(file.absolutePath)
24 | prepare()
25 | start()
26 | }
27 | }
28 | }
29 |
30 | override fun releaseMediaPlayer() {
31 | mediaPlayer?.release()
32 | mediaPlayer = null
33 | temporaryAudioFile?.delete()
34 | temporaryAudioFile = null
35 | }
36 |
37 | private fun createTempAudioFile(audio: ByteArray): File {
38 | return File
39 | .createTempFile(
40 | /* prefix = */ "temp_audio",
41 | /* suffix = */ ".mp3"
42 | )
43 | .apply { writeBytes(audio) }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/redux/MVIViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.redux
2 |
3 | import androidx.lifecycle.ViewModel
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.ObsoleteCoroutinesApi
6 | import kotlinx.coroutines.channels.Channel
7 | import kotlinx.coroutines.channels.actor
8 | import kotlinx.coroutines.channels.consumeEach
9 | import kotlinx.coroutines.flow.MutableSharedFlow
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.SharedFlow
12 | import kotlinx.coroutines.flow.StateFlow
13 | import kotlinx.coroutines.flow.asSharedFlow
14 | import kotlinx.coroutines.flow.asStateFlow
15 |
16 | typealias MiddleWare = (S, I) -> Unit
17 |
18 | @OptIn(ObsoleteCoroutinesApi::class)
19 | abstract class MVIViewModel(
20 | scope: CoroutineScope,
21 | initialState: State
22 | ) : ViewModel() {
23 |
24 | private val _states = MutableStateFlow(initialState)
25 | private val _sideEffects = MutableSharedFlow(Channel.UNLIMITED)
26 | private val _middleWareList = mutableListOf>()
27 |
28 | val states: StateFlow = _states.asStateFlow()
29 | val sideEffects: SharedFlow = _sideEffects.asSharedFlow()
30 | val middleWareList: List> = _middleWareList.toList()
31 |
32 | private val actor = scope.actor(capacity = Channel.UNLIMITED) {
33 | channel.consumeEach { intention ->
34 | _states.value = reduce(
35 | currentState = _states.value,
36 | intention = intention,
37 | middleWares = _middleWareList
38 | )
39 | }
40 | }
41 |
42 | abstract fun reduce(
43 | currentState: State,
44 | intention: Intention,
45 | middleWares: List> = emptyList()
46 | ): State
47 |
48 | protected fun sideEffect(sideEffect: SideEffect) {
49 | _sideEffects.tryEmit(sideEffect)
50 | }
51 |
52 | fun send(intention: Intention) {
53 | actor.trySend(intention)
54 | }
55 |
56 | fun provideMiddleWares(vararg middleWares: MiddleWare) {
57 | _middleWareList.addAll(middleWares)
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/redux/MiddleWares.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.redux
2 |
3 | import android.util.Log
4 |
5 | fun Any.stateLogger(state: String, intention: String) {
6 | Log.d(this.javaClass.simpleName, "state: $state \nintention: $intention")
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/AppThemeSelectionRadioButtonRows.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import android.content.Context
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.selection.selectableGroup
9 | import androidx.compose.material.RadioButton
10 | import androidx.compose.material.RadioButtonDefaults
11 | import androidx.compose.material.Text
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.MutableState
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import dev.atajan.lingva_android.R
18 | import dev.atajan.lingva_android.common.ui.theme.ThemingOptions
19 | import dev.atajan.lingva_android.common.ui.theme.canUseDynamicColor
20 |
21 | @Composable
22 | fun AppThemeSelectionRadioButtonRows(
23 | toggleTheme: (ThemingOptions) -> Unit,
24 | currentTheme: MutableState,
25 | context: Context
26 | ) {
27 | Column(modifier = Modifier.selectableGroup()) {
28 | ThemingOptions.values()
29 | .forEach { option ->
30 | if (option == ThemingOptions.YOU) {
31 | if (canUseDynamicColor) {
32 | Row(
33 | horizontalArrangement = Arrangement.Start,
34 | verticalAlignment = Alignment.CenterVertically,
35 | modifier = Modifier.fillMaxWidth()
36 | ) {
37 | RadioButton(
38 | selected = currentTheme.value.name == option.name,
39 | onClick = {
40 | toggleTheme(ThemingOptions.valueOf(option.name))
41 | },
42 | colors = RadioButtonDefaults.colors(
43 | selectedColor = MaterialTheme.colorScheme.primary,
44 | unselectedColor = MaterialTheme.colorScheme.onBackground,
45 | disabledColor = MaterialTheme.colorScheme.error
46 | )
47 | )
48 |
49 | Text(
50 | text = option.name.uppercase() + " - " + context.getString(R.string.material_you_descriptor),
51 | style = MaterialTheme.typography.labelLarge,
52 | color = MaterialTheme.colorScheme.onBackground
53 | )
54 | }
55 | }
56 | } else {
57 | Row(
58 | horizontalArrangement = Arrangement.Start,
59 | verticalAlignment = Alignment.CenterVertically,
60 | modifier = Modifier.fillMaxWidth()
61 | ) {
62 | RadioButton(
63 | selected = currentTheme.value.name == option.name,
64 | onClick = {
65 | toggleTheme(ThemingOptions.valueOf(option.name))
66 | },
67 | colors = RadioButtonDefaults.colors(
68 | selectedColor = MaterialTheme.colorScheme.primary,
69 | unselectedColor = MaterialTheme.colorScheme.onBackground,
70 | disabledColor = MaterialTheme.colorScheme.error
71 | )
72 | )
73 |
74 | Text(
75 | text = option.name.uppercase(),
76 | style = MaterialTheme.typography.labelLarge,
77 | color = MaterialTheme.colorScheme.onBackground
78 | )
79 | }
80 | }
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/BottomSheetSectionHeader.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material.Text
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 |
10 | @Composable
11 | fun BottomSheetSectionHeader(
12 | titleName: String
13 | ) {
14 | Text(
15 | text = titleName,
16 | color = MaterialTheme.colorScheme.primary,
17 | style = MaterialTheme.typography.titleLarge,
18 | modifier = Modifier.padding(16.dp)
19 | )
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/LanguageListPopUp.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.border
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxHeight
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.foundation.lazy.itemsIndexed
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Divider
15 | import androidx.compose.material.Text
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.MutableState
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.window.Dialog
22 | import dev.atajan.lingva_android.common.domain.models.language.Language
23 |
24 | @Composable
25 | fun LanguageListPopUp(
26 | openDialog: MutableState,
27 | languageList: List,
28 | onLanguageSelected: ((Language) -> Unit),
29 | ) {
30 | if (languageList.isEmpty()) {
31 | openDialog.value = false
32 | } else {
33 | if (openDialog.value) {
34 | Dialog(onDismissRequest = { openDialog.value = false }) {
35 | Box(
36 | Modifier
37 | .fillMaxWidth()
38 | .fillMaxHeight(0.95f)
39 | .background(
40 | color = MaterialTheme.colorScheme.background,
41 | RoundedCornerShape(16.dp)
42 | )
43 | .border(
44 | width = 3.dp,
45 | color = MaterialTheme.colorScheme.primary,
46 | shape = RoundedCornerShape(16.dp)
47 | )
48 | ) {
49 | LazyColumn(modifier = Modifier.padding(8.dp)) {
50 | itemsIndexed(items = languageList) { index, language ->
51 | Column(
52 | modifier = Modifier
53 | .fillParentMaxWidth()
54 | .clickable {
55 | onLanguageSelected(language)
56 | openDialog.value = false
57 | }
58 | ) {
59 | Text(
60 | text = language.name,
61 | style = MaterialTheme.typography.titleMedium,
62 | color = MaterialTheme.colorScheme.onBackground,
63 | modifier = Modifier
64 | .padding(horizontal = 16.dp)
65 | .padding(top = 8.dp)
66 |
67 | )
68 | if (index < languageList.size) {
69 | Divider(
70 | color = MaterialTheme.colorScheme.secondary,
71 | modifier = Modifier
72 | .padding(horizontal = 8.dp)
73 | .padding(top = 8.dp)
74 | )
75 | }
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/LanguageSelectionAndSettingsBar.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxHeight
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.rounded.Settings
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.vector.ImageVector
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.unit.dp
20 | import dev.atajan.lingva_android.R
21 | import dev.atajan.lingva_android.common.domain.models.language.Language
22 |
23 | @Composable
24 | fun LanguageSelectionAndSettingsBar(
25 | supportedLanguages: List,
26 | sourceLanguage: Language,
27 | targetLanguage: Language,
28 | toggleErrorDialogState: (Boolean) -> Unit,
29 | onNewSourceLanguageSelected: (Language) -> Unit,
30 | onNewTargetLanguageSelected: (Language) -> Unit,
31 | middleIcon: ImageVector,
32 | onMiddleIconTap: () -> Unit,
33 | onEndIconTap: () -> Unit,
34 | modifier: Modifier = Modifier
35 | ) {
36 | val context = LocalContext.current
37 |
38 | Row(
39 | modifier = modifier
40 | .height(50.dp)
41 | .fillMaxSize(),
42 | horizontalArrangement = Arrangement.SpaceBetween,
43 | verticalAlignment = Alignment.CenterVertically
44 | ) {
45 | LanguageSelectionBar(
46 | supportedLanguages = supportedLanguages,
47 | sourceLanguage = sourceLanguage,
48 | targetLanguage = targetLanguage,
49 | toggleErrorDialogState = toggleErrorDialogState,
50 | onNewSourceLanguageSelected = onNewSourceLanguageSelected,
51 | onNewTargetLanguageSelected = onNewTargetLanguageSelected,
52 | middleIcon = middleIcon,
53 | onMiddleIconTap = onMiddleIconTap,
54 | modifier = Modifier
55 | .fillMaxHeight()
56 | .fillMaxWidth(0.85f)
57 | )
58 |
59 | IconButton(
60 | onClick = onEndIconTap,
61 | modifier = Modifier.fillMaxHeight()
62 | ) {
63 | Icon(
64 | imageVector = Icons.Rounded.Settings,
65 | contentDescription = context.getString(R.string.setting_icon_ax),
66 | tint = MaterialTheme.colorScheme.onBackground
67 | )
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/LanguageSelectionBar.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxHeight
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material.Icon
12 | import androidx.compose.material.IconButton
13 | import androidx.compose.material.Surface
14 | import androidx.compose.material.Text
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.vector.ImageVector
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.text.style.TextOverflow
25 | import androidx.compose.ui.unit.dp
26 | import dev.atajan.lingva_android.R
27 | import dev.atajan.lingva_android.common.domain.models.language.Language
28 |
29 | @Composable
30 | fun LanguageSelectionBar(
31 | supportedLanguages: List,
32 | sourceLanguage: Language,
33 | targetLanguage: Language,
34 | toggleErrorDialogState: (Boolean) -> Unit,
35 | onNewSourceLanguageSelected: (Language) -> Unit,
36 | onNewTargetLanguageSelected: (Language) -> Unit,
37 | middleIcon: ImageVector,
38 | onMiddleIconTap: () -> Unit,
39 | modifier: Modifier = Modifier
40 | ) {
41 | val context = LocalContext.current
42 | val sourceLanguagesPopUpShown = remember { mutableStateOf(false) }
43 | val targetLanguagesPopUpShown = remember { mutableStateOf(false) }
44 |
45 | Row(
46 | modifier = modifier,
47 | horizontalArrangement = Arrangement.SpaceBetween
48 | ) {
49 | Surface(
50 | modifier = Modifier
51 | .weight(1f)
52 | .fillMaxSize()
53 | .clickable {
54 | if (supportedLanguages.isNotEmpty()) {
55 | sourceLanguagesPopUpShown.value = true
56 | } else {
57 | toggleErrorDialogState(true)
58 | }
59 | },
60 | shape = RoundedCornerShape(20.dp),
61 | color = MaterialTheme.colorScheme.secondary
62 | ) {
63 | Row(verticalAlignment = Alignment.CenterVertically) {
64 | Text(
65 | text = if (sourceLanguage.name == "Detect") {
66 | context.getString(R.string.detect_language)
67 | } else {
68 | sourceLanguage.name
69 | },
70 | style = MaterialTheme.typography.labelLarge,
71 | textAlign = TextAlign.Center,
72 | softWrap = false,
73 | overflow = TextOverflow.Ellipsis,
74 | color = MaterialTheme.colorScheme.onSecondary,
75 | modifier = Modifier
76 | .fillMaxWidth()
77 | .padding(horizontal = 8.dp)
78 | )
79 | }
80 | }
81 |
82 | IconButton(
83 | onClick = onMiddleIconTap,
84 | modifier = Modifier
85 | .fillMaxHeight()
86 | .fillMaxWidth(0.25f)
87 | ) {
88 | Icon(
89 | imageVector = middleIcon,
90 | contentDescription = context.getString(R.string.swap_icon_ax),
91 | tint = MaterialTheme.colorScheme.onBackground,
92 | modifier = Modifier.fillMaxSize()
93 | )
94 | }
95 |
96 | Surface(
97 | modifier = Modifier
98 | .weight(1f)
99 | .fillMaxSize()
100 | .clickable {
101 | if (supportedLanguages.isNotEmpty()) {
102 | targetLanguagesPopUpShown.value = true
103 | } else {
104 | toggleErrorDialogState(true)
105 | }
106 | },
107 | shape = RoundedCornerShape(20.dp),
108 | color = MaterialTheme.colorScheme.secondary
109 | ) {
110 | Row(verticalAlignment = Alignment.CenterVertically) {
111 | Text(
112 | text = targetLanguage.name,
113 | style = MaterialTheme.typography.labelLarge,
114 | textAlign = TextAlign.Center,
115 | softWrap = false,
116 | overflow = TextOverflow.Ellipsis,
117 | color = MaterialTheme.colorScheme.onSecondary,
118 | modifier = Modifier
119 | .fillMaxWidth()
120 | .padding(horizontal = 8.dp)
121 | )
122 | }
123 | }
124 | }
125 |
126 | LanguageListPopUp(
127 | openDialog = sourceLanguagesPopUpShown,
128 | languageList = supportedLanguages
129 | ) { onNewSourceLanguageSelected(it) }
130 |
131 | // Drop the first language, "Detect", since it won't make sense for target language
132 | LanguageListPopUp(
133 | openDialog = targetLanguagesPopUpShown,
134 | languageList = supportedLanguages.drop(1)
135 | ) { onNewTargetLanguageSelected(it) }
136 | }
137 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/NotificationDialog.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.material.AlertDialog
4 | import androidx.compose.material.Text
5 | import androidx.compose.material.TextButton
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.tooling.preview.Preview
10 | import dev.atajan.lingva_android.R
11 |
12 | @Composable
13 | fun ErrorNotificationDialog(
14 | shouldShowDialog: Boolean,
15 | onDismissRequest: () -> Unit
16 | ) {
17 | val context = LocalContext.current
18 |
19 | if (shouldShowDialog) {
20 | AlertDialog(
21 | backgroundColor = MaterialTheme.colorScheme.secondary,
22 | contentColor = MaterialTheme.colorScheme.onSecondary,
23 | onDismissRequest = {
24 | // Dismiss the dialog when the user clicks outside the dialog or on the back
25 | // button. If you want to disable that functionality, simply use an empty onCloseRequest.
26 | onDismissRequest()
27 | },
28 | title = {
29 | Text(
30 | text = "Uh oh :(",
31 | style = MaterialTheme.typography.headlineLarge
32 | )
33 | },
34 | text = {
35 | Text(context.getString(R.string.error_dialog_body))
36 | },
37 | confirmButton = {
38 | // this component is meant only for a passive error notification. No positive action should be here.
39 | },
40 | dismissButton = {
41 | TextButton(
42 | onClick = {
43 | onDismissRequest()
44 | }
45 | ) {
46 | Text(
47 | text = context.getString(R.string.error_dialog_dismiss_button),
48 | color = MaterialTheme.colorScheme.onSecondary,
49 | style = MaterialTheme.typography.labelLarge
50 | )
51 | }
52 | }
53 | )
54 | }
55 | }
56 |
57 | @Preview
58 | @Composable
59 | fun PreviewNotificationDialog() {
60 | ErrorNotificationDialog(
61 | shouldShowDialog = true,
62 | onDismissRequest = {}
63 | )
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/SelectDefaultLanguagesColumn.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import android.content.Context
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.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material.Text
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.unit.dp
18 | import dev.atajan.lingva_android.R
19 | import dev.atajan.lingva_android.common.domain.models.language.Language
20 |
21 | @Composable
22 | fun SelectDefaultLanguagesColumn(
23 | defaultSourceLanguage: String,
24 | defaultTargetLanguage: String,
25 | setDefaultSourceLanguage: (Language) -> Unit,
26 | setDefaultTargetLanguage: (Language) -> Unit,
27 | supportedLanguages: List,
28 | toggleErrorDialogState: (Boolean) -> Unit,
29 | context: Context
30 | ) {
31 | val sourceLanguagesPopUpShown = remember { mutableStateOf(false) }
32 | val targetLanguagesPopUpShown = remember { mutableStateOf(false) }
33 |
34 | Column(
35 | horizontalAlignment = Alignment.Start,
36 | verticalArrangement = Arrangement.Center
37 | ) {
38 | Row(
39 | horizontalArrangement = Arrangement.Start,
40 | verticalAlignment = Alignment.CenterVertically,
41 | modifier = Modifier.fillMaxWidth()
42 | ) {
43 | Text(
44 | text = context.getString(R.string.default_language_source),
45 | color = MaterialTheme.colorScheme.primary,
46 | style = MaterialTheme.typography.labelLarge,
47 | modifier = Modifier.padding(
48 | start = 16.dp,
49 | top = 16.dp,
50 | bottom = 16.dp,
51 | )
52 | )
53 |
54 | Text(
55 | text = defaultSourceLanguage.ifEmpty { context.getString(R.string.tap_to_select) },
56 | color = MaterialTheme.colorScheme.onBackground,
57 | style = MaterialTheme.typography.labelLarge,
58 | modifier = Modifier
59 | .padding(16.dp)
60 | .clickable {
61 | if (supportedLanguages.isNotEmpty()) {
62 | sourceLanguagesPopUpShown.value = true
63 | } else {
64 | toggleErrorDialogState(true)
65 | }
66 | }
67 | )
68 | }
69 |
70 | Row(
71 | horizontalArrangement = Arrangement.Start,
72 | verticalAlignment = Alignment.CenterVertically,
73 | modifier = Modifier.fillMaxWidth()
74 | ) {
75 | Text(
76 | text = context.getString(R.string.default_language_target),
77 | color = MaterialTheme.colorScheme.primary,
78 | style = MaterialTheme.typography.labelLarge,
79 | modifier = Modifier.padding(
80 | start = 16.dp,
81 | top = 16.dp,
82 | bottom = 16.dp,
83 | )
84 | )
85 |
86 | Text(
87 | text = defaultTargetLanguage.ifEmpty { context.getString(R.string.tap_to_select) },
88 | color = MaterialTheme.colorScheme.onBackground,
89 | style = MaterialTheme.typography.labelLarge,
90 | modifier = Modifier
91 | .padding(16.dp)
92 | .clickable {
93 | if (supportedLanguages.isNotEmpty()) {
94 | targetLanguagesPopUpShown.value = true
95 | } else {
96 | toggleErrorDialogState(true)
97 | }
98 | }
99 | )
100 | }
101 | }
102 |
103 | LanguageListPopUp(
104 | openDialog = sourceLanguagesPopUpShown,
105 | languageList = supportedLanguages,
106 | ) { selectedLanguage: Language ->
107 | setDefaultSourceLanguage(selectedLanguage)
108 | }
109 |
110 | // Drop the first language, "Detect", since it won't make sense for target language
111 | LanguageListPopUp(
112 | openDialog = targetLanguagesPopUpShown,
113 | languageList = supportedLanguages.drop(1),
114 | ) { selectedLanguage: Language ->
115 | setDefaultTargetLanguage(selectedLanguage)
116 | }
117 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/SettingsBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import android.content.Context
4 | import androidx.compose.foundation.background
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.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material.ExperimentalMaterialApi
11 | import androidx.compose.material.ModalBottomSheetLayout
12 | import androidx.compose.material.ModalBottomSheetState
13 | import androidx.compose.material.Switch
14 | import androidx.compose.material.SwitchDefaults
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.MutableState
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.semantics.contentDescription
22 | import androidx.compose.ui.semantics.semantics
23 | import androidx.compose.ui.unit.dp
24 | import dev.atajan.lingva_android.R
25 | import dev.atajan.lingva_android.common.domain.models.language.Language
26 | import dev.atajan.lingva_android.common.ui.theme.ThemingOptions
27 |
28 | @ExperimentalMaterialApi
29 | @Composable
30 | fun SettingsBottomSheet(
31 | modalBottomSheetState: ModalBottomSheetState,
32 | toggleTheme: (ThemingOptions) -> Unit,
33 | currentTheme: MutableState,
34 | setDefaultSourceLanguage: (Language) -> Unit,
35 | setDefaultTargetLanguage: (Language) -> Unit,
36 | supportedLanguages: List,
37 | defaultSourceLanguage: String,
38 | defaultTargetLanguage: String,
39 | toggleErrorDialogState: (Boolean) -> Unit,
40 | customLingvaServerUrl: MutableState,
41 | currentCustomLingvaServerUrl: String,
42 | liveTranslateEnabled: Boolean,
43 | onToggleLiveTranslate: (Boolean) -> Unit,
44 | context: Context,
45 | ) {
46 |
47 | ModalBottomSheetLayout(
48 | sheetState = modalBottomSheetState,
49 | sheetContent = {
50 | Column(modifier = Modifier
51 | .fillMaxWidth()
52 | .background(MaterialTheme.colorScheme.background)
53 | ) {
54 | BottomSheetSectionHeader(context.getString(R.string.app_theme_setting_title))
55 | AppThemeSelectionRadioButtonRows(
56 | toggleTheme = toggleTheme,
57 | currentTheme = currentTheme,
58 | context = context
59 | )
60 |
61 | BottomSheetSectionHeader(context.getString(R.string.default_languages_title))
62 | SelectDefaultLanguagesColumn(
63 | defaultSourceLanguage = defaultSourceLanguage,
64 | defaultTargetLanguage = defaultTargetLanguage,
65 | setDefaultSourceLanguage = setDefaultSourceLanguage,
66 | setDefaultTargetLanguage = setDefaultTargetLanguage,
67 | supportedLanguages = supportedLanguages,
68 | toggleErrorDialogState = toggleErrorDialogState,
69 | context = context
70 | )
71 |
72 | Row(
73 | verticalAlignment = Alignment.CenterVertically,
74 | horizontalArrangement = Arrangement.SpaceBetween,
75 | modifier = Modifier
76 | .padding(
77 | start = 16.dp,
78 | bottom = 16.dp,
79 | )
80 | .fillMaxWidth()
81 | ) {
82 | Text(
83 | text = context.getString(R.string.toggle_live_translate),
84 | color = MaterialTheme.colorScheme.primary,
85 | style = MaterialTheme.typography.labelLarge,
86 | )
87 |
88 | Switch(
89 | modifier = Modifier.semantics { contentDescription = "toggle live translate" },
90 | checked = liveTranslateEnabled,
91 | onCheckedChange = {
92 | onToggleLiveTranslate(it)
93 | },
94 | colors = SwitchDefaults.colors(
95 | checkedThumbColor = MaterialTheme.colorScheme.primary,
96 | uncheckedThumbColor = MaterialTheme.colorScheme.secondary,
97 | checkedTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
98 | uncheckedTrackColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
99 | )
100 | )
101 | }
102 |
103 | BottomSheetSectionHeader(context.getString(R.string.advanced_settings_title))
104 | SettingsBottomSheetOutlinedTextField(
105 | label = context.getString(R.string.custom_lingva_instance_address),
106 | currentTextFieldValue = customLingvaServerUrl,
107 | hint = currentCustomLingvaServerUrl,
108 | onValueChange = { customLingvaServerUrl.value = it },
109 | )
110 | }
111 | }
112 | ) { }
113 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/SettingsBottomSheetThemedOutlinedTextField.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material.ContentAlpha
7 | import androidx.compose.material.OutlinedTextField
8 | import androidx.compose.material.TextFieldDefaults
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.MutableState
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.text.input.ImeAction
15 | import androidx.compose.ui.text.input.KeyboardType
16 | import androidx.compose.ui.unit.dp
17 | import dev.atajan.lingva_android.common.data.api.constants.LINGVA
18 |
19 | @Composable
20 | fun SettingsBottomSheetOutlinedTextField(
21 | label: String,
22 | currentTextFieldValue: MutableState,
23 | hint: String,
24 | onValueChange: (String) -> Unit,
25 | ) {
26 | // TODO: add URL validation
27 | OutlinedTextField(
28 | value = currentTextFieldValue.value,
29 | placeholder = {
30 | Text(
31 | text = if (hint.isEmpty()) "Default: $LINGVA" else "Currently: $hint",
32 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.disabled)
33 | )
34 | },
35 | onValueChange = { onValueChange(it) },
36 | label = {
37 | Text(
38 | text = label,
39 | color = MaterialTheme.colorScheme.primary
40 | )
41 | },
42 | modifier = Modifier
43 | .fillMaxWidth()
44 | .padding(16.dp),
45 | keyboardOptions = KeyboardOptions(
46 | keyboardType = KeyboardType.Uri,
47 | imeAction = ImeAction.Default
48 | ),
49 | textStyle = MaterialTheme.typography.labelLarge,
50 | colors = TextFieldDefaults.outlinedTextFieldColors(
51 | focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
52 | unfocusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.disabled),
53 | focusedLabelColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
54 | unfocusedLabelColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.disabled),
55 | cursorColor = MaterialTheme.colorScheme.primary,
56 | textColor = MaterialTheme.colorScheme.onBackground,
57 | )
58 | )
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/components/TitleBar.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxHeight
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.material.Text
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.rounded.Settings
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.text.style.TextAlign
19 | import androidx.compose.ui.text.style.TextOverflow
20 | import androidx.compose.ui.unit.dp
21 | import dev.atajan.lingva_android.R
22 |
23 | @Composable
24 | fun TitleBar(
25 | title: String,
26 | onEndIconTap: () -> Unit,
27 | modifier: Modifier = Modifier
28 | ) {
29 | val context = LocalContext.current
30 |
31 | Row(
32 | modifier = modifier.height(30.dp).fillMaxSize(),
33 | horizontalArrangement = Arrangement.SpaceBetween,
34 | verticalAlignment = Alignment.CenterVertically
35 | ) {
36 | Text(
37 | text = title,
38 | style = MaterialTheme.typography.headlineSmall,
39 | textAlign = TextAlign.Start,
40 | softWrap = false,
41 | overflow = TextOverflow.Ellipsis,
42 | color = MaterialTheme.colorScheme.onBackground
43 | )
44 |
45 | IconButton(
46 | onClick = onEndIconTap,
47 | modifier = Modifier.fillMaxHeight()
48 | ) {
49 | Icon(
50 | imageVector = Icons.Rounded.Settings,
51 | contentDescription = context.getString(R.string.setting_icon_ax),
52 | tint = MaterialTheme.colorScheme.onBackground
53 | )
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val LingvaGreen = Color(0xFF61FD96)
6 | val LingvaGreenLighter = Color(0xFFc5fed3)
7 | val LingvaGreenDarker = Color(0xFF00cc49)
8 | val LingvaGray = Color(0xFFE5E5E5)
9 | val LingvaDarkGray = Color(0xFF5E5C5C)
10 | val AccentMagenta = Color(0xFFfd61c9)
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/theme/Helpers.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.theme
2 |
3 | import android.content.Context
4 | import dev.atajan.lingva_android.common.data.datasource.impl.APP_THEME
5 | import dev.atajan.lingva_android.common.data.datasource.impl.dataStore
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.map
8 |
9 | fun selectedThemeFlow(applicationContext: Context): Flow {
10 | return applicationContext.dataStore.data
11 | .map { preferences ->
12 | preferences[APP_THEME]
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.ui.unit.dp
5 |
6 | val smallRoundedCornerShape = RoundedCornerShape(2.dp)
7 | val mediumRoundedCornerShape = RoundedCornerShape(4.dp)
8 | val largeRoundedCornerShape = RoundedCornerShape(8.dp)
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.theme
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 | import android.os.Build
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.darkColorScheme
9 | import androidx.compose.material3.dynamicDarkColorScheme
10 | import androidx.compose.material3.dynamicLightColorScheme
11 | import androidx.compose.material3.lightColorScheme
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.platform.LocalContext
15 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
16 |
17 | val canUseDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
18 |
19 | fun isSystemInNightMode(context: Context): Boolean {
20 | return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
21 | Configuration.UI_MODE_NIGHT_YES -> true
22 | Configuration.UI_MODE_NIGHT_NO -> false
23 | else -> false
24 | }
25 | }
26 |
27 | enum class ThemingOptions {
28 | LIGHT, DARK, YOU
29 | }
30 |
31 | private val darkColorScheme = darkColorScheme(
32 | primary = LingvaGreen,
33 | primaryContainer = LingvaGreenLighter,
34 | secondary = LingvaDarkGray,
35 | secondaryContainer = AccentMagenta,
36 | onPrimary = Color.Black,
37 | onSecondary = Color.White,
38 | surface = Color.Black,
39 | onSurface = Color.White,
40 | background = Color.Black,
41 | onBackground = Color.White,
42 | )
43 |
44 | private val lightColorScheme = lightColorScheme(
45 | primary = LingvaGreenDarker,
46 | primaryContainer = LingvaGreenDarker,
47 | secondary = LingvaGray,
48 | secondaryContainer = AccentMagenta,
49 | onPrimary = Color.Black,
50 | onSecondary = Color.Black,
51 | surface = Color.White,
52 | onSurface = Color.Black,
53 | background = Color.White,
54 | onBackground = Color.Black,
55 | )
56 |
57 | @Composable
58 | fun LingvaAndroidTheme(
59 | appTheme: ThemingOptions,
60 | content: @Composable () -> Unit
61 | ) {
62 | val systemUiController = rememberSystemUiController()
63 |
64 | val colorScheme = when (appTheme) {
65 | ThemingOptions.LIGHT -> lightColorScheme
66 | ThemingOptions.DARK -> darkColorScheme
67 | ThemingOptions.YOU -> {
68 | if (isSystemInDarkTheme()) dynamicDarkColorScheme(LocalContext.current) else dynamicLightColorScheme(LocalContext.current)
69 | }
70 | }
71 |
72 | systemUiController.setSystemBarsColor(
73 | color = colorScheme.background
74 | )
75 |
76 | MaterialTheme(
77 | colorScheme = colorScheme,
78 | typography = material3Typography,
79 | content = content
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | val material3Typography = Typography(
10 | bodyLarge = TextStyle(
11 | fontFamily = FontFamily.Default,
12 | fontWeight = FontWeight.Normal,
13 | fontSize = 16.sp
14 | )
15 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/FetchSupportedLanguagesUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases
2 |
3 | import dev.atajan.lingva_android.common.domain.results.LanguagesRepositoryResponse
4 |
5 | interface FetchSupportedLanguagesUseCase {
6 | /**
7 | * Requests for a list of supported languages.
8 | */
9 | suspend operator fun invoke(): LanguagesRepositoryResponse
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ObserveAudioDataUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases
2 |
3 | import dev.atajan.lingva_android.common.domain.results.AudioRepositoryResponse
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface ObserveAudioDataUseCase {
7 | operator fun invoke(): Flow
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ObserveTranslationResultUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases
2 |
3 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface ObserveTranslationResultUseCase {
7 | /**
8 | * Observes the translation result.
9 | */
10 | operator fun invoke(): Flow
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/PlayByteArrayAudioUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases
2 |
3 | interface PlayByteArrayAudioUseCase {
4 | operator fun invoke(audio: ByteArray)
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/RequestAudioDataUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases
2 |
3 | interface RequestAudioDataUseCase {
4 |
5 | operator fun invoke(
6 | language: String,
7 | query: String
8 | )
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/TranslateUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases
2 |
3 | interface TranslateUseCase {
4 |
5 | /**
6 | * Translates a [textToTranslate] from [sourceLanguageCode] language to [targetLanguageCode] language.
7 | * The result must be observed via [ObserveTranslationResultUseCase].
8 | */
9 | operator fun invoke(
10 | sourceLanguageCode: String,
11 | targetLanguageCode: String,
12 | textToTranslate: String
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ktorimpl/AudioPlayerPlayByteArrayAudioUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases.ktorimpl
2 |
3 | import dev.atajan.lingva_android.common.media.AudioPlayer
4 | import dev.atajan.lingva_android.common.usecases.PlayByteArrayAudioUseCase
5 |
6 | class AudioPlayerPlayByteArrayAudioUseCase(
7 | private val audioPlayer: AudioPlayer
8 | ) : PlayByteArrayAudioUseCase {
9 |
10 | override fun invoke(audio: ByteArray) {
11 | audioPlayer.playAudio(audio)
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ktorimpl/KtorFetchSupportedLanguagesUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases.ktorimpl
2 |
3 | import dev.atajan.lingva_android.common.data.datasource.LanguagesRepository
4 | import dev.atajan.lingva_android.common.usecases.FetchSupportedLanguagesUseCase
5 |
6 | class KtorFetchSupportedLanguagesUseCase(
7 | private val languagesRepository: LanguagesRepository
8 | ) : FetchSupportedLanguagesUseCase {
9 |
10 | override suspend fun invoke() = languagesRepository.fetchSupportedLanguages()
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ktorimpl/KtorObserveAudioDataUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases.ktorimpl
2 |
3 | import dev.atajan.lingva_android.common.data.datasource.AudioRepository
4 | import dev.atajan.lingva_android.common.usecases.ObserveAudioDataUseCase
5 |
6 | class KtorObserveAudioDataUseCase(
7 | private val audioRepository: AudioRepository
8 | ) : ObserveAudioDataUseCase {
9 |
10 | override operator fun invoke() = audioRepository.audioRequestResult
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ktorimpl/KtorObserveTranslationResultUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases.ktorimpl
2 |
3 | import dev.atajan.lingva_android.common.data.datasource.TranslationRepository
4 | import dev.atajan.lingva_android.common.usecases.ObserveTranslationResultUseCase
5 |
6 | class KtorObserveTranslationResultUseCase(
7 | private val translationRepository: TranslationRepository
8 | ) : ObserveTranslationResultUseCase {
9 |
10 | override fun invoke() = translationRepository.translationResult
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ktorimpl/KtorRequestAudioDataUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases.ktorimpl
2 |
3 | import dev.atajan.lingva_android.common.data.datasource.AudioRepository
4 | import dev.atajan.lingva_android.common.usecases.RequestAudioDataUseCase
5 |
6 | class KtorRequestAudioDataUseCase(
7 | private val audioRepository: AudioRepository
8 | ) : RequestAudioDataUseCase {
9 |
10 | override operator fun invoke(
11 | language: String,
12 | query: String
13 | ) {
14 | audioRepository.requestAudio(
15 | language = language,
16 | query = query
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/common/usecases/ktorimpl/KtorTranslateUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.usecases.ktorimpl
2 |
3 | import dev.atajan.lingva_android.common.data.datasource.TranslationRepository
4 | import dev.atajan.lingva_android.common.usecases.TranslateUseCase
5 |
6 | class KtorTranslateUseCase(
7 | private val translationRepository: TranslationRepository
8 | ) : TranslateUseCase {
9 |
10 | override fun invoke(
11 | sourceLanguageCode: String,
12 | targetLanguageCode: String,
13 | textToTranslate: String
14 | ) {
15 | translationRepository.translate(
16 | source = sourceLanguageCode,
17 | target = targetLanguageCode,
18 | query = textToTranslate
19 | )
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/quicktranslatefeature/redux/QuickTranslateScreenIntention.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.quicktranslatefeature.redux
2 |
3 | import dev.atajan.lingva_android.common.domain.models.language.Language
4 |
5 | sealed interface QuickTranslateScreenIntention {
6 | data class OnTextToTranslateChange(val newValue: String) : QuickTranslateScreenIntention
7 | data class SetDefaultTargetLanguage(val languageName: String) : QuickTranslateScreenIntention
8 | data class SetNewSourceLanguage(val language: Language) : QuickTranslateScreenIntention
9 | data class SetNewTargetLanguage(val language: Language) : QuickTranslateScreenIntention
10 | data class ShowErrorDialog(val show: Boolean) : QuickTranslateScreenIntention
11 | data class SupportedLanguagesReceived(val languages: List) : QuickTranslateScreenIntention
12 | data class TranslationSuccess(val result: String) : QuickTranslateScreenIntention
13 | object CopyTextToClipboard : QuickTranslateScreenIntention
14 | object Translate : QuickTranslateScreenIntention
15 | object TranslationFailure : QuickTranslateScreenIntention
16 |
17 | object ReadTextOutLoud : QuickTranslateScreenIntention
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/quicktranslatefeature/redux/QuickTranslateScreenSideEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.quicktranslatefeature.redux
2 |
3 | sealed interface QuickTranslateScreenSideEffect
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/quicktranslatefeature/redux/QuickTranslateScreenState.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.quicktranslatefeature.redux
2 |
3 | import dev.atajan.lingva_android.common.domain.models.language.Language
4 |
5 | data class QuickTranslateScreenState(
6 | val supportedLanguages: List = emptyList(),
7 | val translatedText: String = "",
8 | val sourceLanguage: Language = Language("auto", "Detect"),
9 | val targetLanguage: Language = Language("es", "Spanish"),
10 | val textToTranslate: String = "",
11 | val errorDialogState: Boolean = false,
12 | val defaultTargetLanguage: String = ""
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/quicktranslatefeature/screens/QuickTranslateScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.quicktranslatefeature.screens
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.text.selection.SelectionContainer
16 | import androidx.compose.foundation.verticalScroll
17 | import androidx.compose.material.Card
18 | import androidx.compose.material.ContentAlpha
19 | import androidx.compose.material.ExperimentalMaterialApi
20 | import androidx.compose.material.OutlinedTextField
21 | import androidx.compose.material.TextFieldDefaults
22 | import androidx.compose.material.icons.Icons
23 | import androidx.compose.material.icons.rounded.ArrowRightAlt
24 | import androidx.compose.material.icons.rounded.ContentCopy
25 | import androidx.compose.material.icons.rounded.VolumeUp
26 | import androidx.compose.material3.Icon
27 | import androidx.compose.material3.IconButton
28 | import androidx.compose.material3.MaterialTheme
29 | import androidx.compose.material3.Text
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.collectAsState
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.platform.LocalContext
36 | import androidx.compose.ui.unit.dp
37 | import dev.atajan.lingva_android.R
38 | import dev.atajan.lingva_android.common.ui.components.ErrorNotificationDialog
39 | import dev.atajan.lingva_android.common.ui.components.LanguageSelectionBar
40 | import dev.atajan.lingva_android.common.ui.theme.mediumRoundedCornerShape
41 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.CopyTextToClipboard
42 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.OnTextToTranslateChange
43 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.ReadTextOutLoud
44 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.SetNewSourceLanguage
45 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.SetNewTargetLanguage
46 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.ShowErrorDialog
47 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.Translate
48 |
49 | @ExperimentalMaterialApi
50 | @Composable
51 | fun QuickTranslateScreen(
52 | textToTranslate: String,
53 | viewModel: QuickTranslateScreenViewModel
54 | ) {
55 | val context = LocalContext.current
56 | val scrollState = rememberScrollState(0)
57 | val quickTranslateScreenState by viewModel.states.collectAsState()
58 |
59 | viewModel.apply {
60 | send(OnTextToTranslateChange(textToTranslate))
61 | send(Translate)
62 | }
63 |
64 | Column(
65 | modifier = Modifier
66 | .fillMaxSize()
67 | .background(MaterialTheme.colorScheme.background)
68 | ) {
69 | LanguageSelectionBar(
70 | modifier = Modifier
71 | .height(80.dp)
72 | .fillMaxSize()
73 | .padding(all = 16.dp),
74 | supportedLanguages = quickTranslateScreenState.supportedLanguages,
75 | sourceLanguage = quickTranslateScreenState.sourceLanguage,
76 | targetLanguage = quickTranslateScreenState.targetLanguage,
77 | toggleErrorDialogState = {
78 | viewModel.send(ShowErrorDialog(it))
79 | },
80 | middleIcon = Icons.Rounded.ArrowRightAlt,
81 | onMiddleIconTap = {
82 | // No action necessary for this user flow
83 | },
84 | onNewSourceLanguageSelected = {
85 | viewModel.send(SetNewSourceLanguage(it))
86 | viewModel.send(Translate)
87 | },
88 | onNewTargetLanguageSelected = {
89 | viewModel.send(SetNewTargetLanguage(it))
90 | viewModel.send(Translate)
91 | }
92 | )
93 |
94 | Box(
95 | modifier = Modifier
96 | .fillMaxWidth()
97 | .fillMaxHeight(0.40f)
98 | .padding(all = 16.dp)
99 | ) {
100 | OutlinedTextField(
101 | value = textToTranslate,
102 | onValueChange = { },
103 | label = {
104 | Text(
105 | text = context.getString(R.string.source_text),
106 | color = MaterialTheme.colorScheme.primary
107 | )
108 | },
109 | modifier = Modifier.fillMaxSize(),
110 | textStyle = MaterialTheme.typography.titleMedium,
111 | colors = TextFieldDefaults.outlinedTextFieldColors(
112 | focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
113 | unfocusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
114 | focusedLabelColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
115 | unfocusedLabelColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
116 | cursorColor = MaterialTheme.colorScheme.primary,
117 | textColor = MaterialTheme.colorScheme.onBackground,
118 | ),
119 | readOnly = true
120 | )
121 | }
122 |
123 | Box(
124 | modifier = Modifier
125 | .fillMaxWidth()
126 | .fillMaxHeight(0.95f)
127 | .padding(all = 16.dp)
128 | ) {
129 | Card(
130 | shape = mediumRoundedCornerShape,
131 | border = BorderStroke(
132 | width = 2.dp,
133 | color = MaterialTheme.colorScheme.primary
134 | ),
135 | elevation = 0.dp,
136 | backgroundColor = MaterialTheme.colorScheme.background,
137 | modifier = Modifier.fillMaxSize(),
138 | ) {
139 | SelectionContainer {
140 | Text(
141 | quickTranslateScreenState.translatedText,
142 | color = MaterialTheme.colorScheme.onBackground,
143 | style = MaterialTheme.typography.titleMedium,
144 | modifier = Modifier
145 | .padding(16.dp)
146 | .verticalScroll(scrollState)
147 | )
148 | }
149 | }
150 |
151 | Column(
152 | modifier = Modifier.fillMaxSize(),
153 | horizontalAlignment = Alignment.End,
154 | verticalArrangement = Arrangement.Bottom
155 | ) {
156 | Row(
157 | modifier = Modifier.fillMaxWidth(),
158 | horizontalArrangement = Arrangement.SpaceBetween
159 | ) {
160 | IconButton(
161 | onClick = {
162 | viewModel.send(ReadTextOutLoud)
163 | },
164 | modifier = Modifier
165 | .padding(bottom = 16.dp)
166 | ) {
167 | Icon(
168 | imageVector = Icons.Rounded.VolumeUp,
169 | contentDescription = context.getString(R.string.text_to_speech_icon_ax),
170 | tint = MaterialTheme.colorScheme.primary,
171 | )
172 | }
173 |
174 | IconButton(
175 | onClick = {
176 | viewModel.send(CopyTextToClipboard)
177 | },
178 | modifier = Modifier
179 | .padding(bottom = 16.dp),
180 | ) {
181 | Icon(
182 | imageVector = Icons.Rounded.ContentCopy,
183 | contentDescription = context.getString(R.string.copy_icon_ax),
184 | tint = MaterialTheme.colorScheme.primary,
185 | )
186 | }
187 | }
188 | }
189 | }
190 | }
191 |
192 | ErrorNotificationDialog(quickTranslateScreenState.errorDialogState) {
193 | viewModel.send(ShowErrorDialog(false))
194 | }
195 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/quicktranslatefeature/screens/QuickTranslateScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.quicktranslatefeature.screens
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import androidx.datastore.core.DataStore
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.lifecycle.viewModelScope
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import dev.atajan.lingva_android.common.data.datasource.impl.DEFAULT_TARGET_LANGUAGE
10 | import dev.atajan.lingva_android.common.domain.models.language.Language
11 | import dev.atajan.lingva_android.common.domain.models.translation.TranslationWithInfo.Companion.toTranslation
12 | import dev.atajan.lingva_android.common.domain.results.AudioRepositoryResponse
13 | import dev.atajan.lingva_android.common.domain.results.LanguagesRepositoryResponse
14 | import dev.atajan.lingva_android.common.domain.results.TranslationRepositoryResponse
15 | import dev.atajan.lingva_android.common.redux.MVIViewModel
16 | import dev.atajan.lingva_android.common.redux.MiddleWare
17 | import dev.atajan.lingva_android.common.usecases.FetchSupportedLanguagesUseCase
18 | import dev.atajan.lingva_android.common.usecases.ObserveAudioDataUseCase
19 | import dev.atajan.lingva_android.common.usecases.ObserveTranslationResultUseCase
20 | import dev.atajan.lingva_android.common.usecases.PlayByteArrayAudioUseCase
21 | import dev.atajan.lingva_android.common.usecases.RequestAudioDataUseCase
22 | import dev.atajan.lingva_android.common.usecases.TranslateUseCase
23 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention
24 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.CopyTextToClipboard
25 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.OnTextToTranslateChange
26 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.ReadTextOutLoud
27 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.SetDefaultTargetLanguage
28 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.SetNewSourceLanguage
29 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.SetNewTargetLanguage
30 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.ShowErrorDialog
31 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.SupportedLanguagesReceived
32 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.Translate
33 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.TranslationFailure
34 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenIntention.TranslationSuccess
35 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenSideEffect
36 | import dev.atajan.lingva_android.quicktranslatefeature.redux.QuickTranslateScreenState
37 | import kotlinx.coroutines.CoroutineScope
38 | import kotlinx.coroutines.flow.distinctUntilChanged
39 | import kotlinx.coroutines.flow.launchIn
40 | import kotlinx.coroutines.flow.mapNotNull
41 | import kotlinx.coroutines.flow.onEach
42 | import kotlinx.coroutines.launch
43 | import javax.inject.Inject
44 |
45 | @HiltViewModel
46 | class QuickTranslateScreenViewModel @Inject constructor(
47 | applicationScope: CoroutineScope,
48 | translationResult: ObserveTranslationResultUseCase,
49 | audioData: ObserveAudioDataUseCase,
50 | private val clipboardManager: ClipboardManager,
51 | private val dataStore: DataStore,
52 | private val supportedLanguages: FetchSupportedLanguagesUseCase,
53 | private val translate: TranslateUseCase,
54 | private val playByteArrayAudio: PlayByteArrayAudioUseCase,
55 | private val requestAudioData: RequestAudioDataUseCase,
56 | ) : MVIViewModel(
57 | scope = applicationScope,
58 | initialState = QuickTranslateScreenState()
59 | ) {
60 |
61 | init {
62 | observeTranslationResults(translationResult)
63 | viewModelScope.launch {
64 | // These operations need to be sequential
65 | getSupportedLanguages()
66 | observeDefaultLanguages(this)
67 | }
68 | observeAudioData(audioData)
69 | }
70 |
71 | override fun reduce(
72 | currentState: QuickTranslateScreenState,
73 | intention: QuickTranslateScreenIntention,
74 | middleWares: List>
75 | ): QuickTranslateScreenState {
76 | return when (intention) {
77 | is SetDefaultTargetLanguage -> {
78 | if (currentState.defaultTargetLanguage != intention.languageName) {
79 | getDefaultLanguageIfProvided(
80 | supportedLanguages = currentState.supportedLanguages,
81 | lookUpLanguage = intention.languageName
82 | ).let { language ->
83 | currentState.copy(
84 | targetLanguage = language ?: currentState.targetLanguage,
85 | defaultTargetLanguage = language?.name ?: currentState.defaultTargetLanguage
86 | )
87 | }
88 | } else {
89 | currentState
90 | }
91 | }
92 | is SetNewSourceLanguage -> currentState.copy(sourceLanguage = intention.language)
93 | is SetNewTargetLanguage -> currentState.copy(targetLanguage = intention.language)
94 | is ShowErrorDialog -> currentState.copy(errorDialogState = intention.show)
95 | is SupportedLanguagesReceived -> currentState.copy(supportedLanguages = intention.languages)
96 | is TranslationSuccess -> currentState.copy(translatedText = intention.result)
97 | Translate -> {
98 | requestTranslation(
99 | sourceLanguageCode = currentState.sourceLanguage.code,
100 | targetLanguageCode = currentState.targetLanguage.code,
101 | textToTranslate = currentState.textToTranslate
102 | )
103 | currentState
104 | }
105 | TranslationFailure -> currentState
106 | CopyTextToClipboard -> {
107 | copyTextToClipboard(currentState.translatedText)
108 | currentState
109 | }
110 |
111 | is OnTextToTranslateChange -> {
112 | currentState.copy(textToTranslate = intention.newValue)
113 | }
114 | ReadTextOutLoud -> {
115 | requestAudioData(
116 | language = currentState.targetLanguage.code,
117 | query = currentState.translatedText
118 | )
119 | currentState
120 | }
121 | }
122 | }
123 |
124 | private suspend fun getSupportedLanguages() {
125 | supportedLanguages().let { result ->
126 | when (result) {
127 | is LanguagesRepositoryResponse.Success -> {
128 | send(SupportedLanguagesReceived(result.languageList))
129 | }
130 | is LanguagesRepositoryResponse.Failure -> {
131 | send(ShowErrorDialog(true))
132 | }
133 | }
134 | }
135 | }
136 |
137 | private fun observeDefaultLanguages(scope: CoroutineScope) {
138 | dataStore.data.mapNotNull {
139 | it[DEFAULT_TARGET_LANGUAGE]
140 | }
141 | .distinctUntilChanged()
142 | .onEach { send(SetDefaultTargetLanguage(it)) }
143 | .launchIn(scope)
144 | }
145 |
146 | private fun observeTranslationResults(translationResult: ObserveTranslationResultUseCase) {
147 | translationResult().onEach {
148 | when (it) {
149 | is TranslationRepositoryResponse.Success -> {
150 | val responseWithInfo = it.response
151 | val pureTranslationResult = responseWithInfo.toTranslation().result
152 |
153 | send(TranslationSuccess(pureTranslationResult))
154 | }
155 | is TranslationRepositoryResponse.Failure -> {
156 | send(TranslationFailure)
157 | }
158 | TranslationRepositoryResponse.Loading -> {
159 | // Loading UI?
160 | }
161 | }
162 | }.launchIn(viewModelScope)
163 | }
164 |
165 | private fun requestTranslation(
166 | sourceLanguageCode: String,
167 | targetLanguageCode: String,
168 | textToTranslate: String,
169 | ) {
170 | viewModelScope.launch {
171 | translate(
172 | sourceLanguageCode = sourceLanguageCode,
173 | targetLanguageCode = targetLanguageCode,
174 | textToTranslate = textToTranslate
175 | )
176 | }
177 | }
178 |
179 | private fun copyTextToClipboard(translatedText: String) {
180 | val clipData = ClipData.newPlainText("Translation", translatedText)
181 |
182 | clipboardManager.setPrimaryClip(clipData)
183 | }
184 |
185 | private fun getDefaultLanguageIfProvided(
186 | supportedLanguages: List,
187 | lookUpLanguage: String
188 | ): Language? {
189 | return supportedLanguages.find { it.name == lookUpLanguage }
190 | }
191 |
192 | private fun observeAudioData(audioRepository: ObserveAudioDataUseCase) {
193 | audioRepository().onEach {
194 | when (it) {
195 | is AudioRepositoryResponse.Success -> {
196 | playByteArrayAudio(it.audio.audioByteArray)
197 | }
198 | is AudioRepositoryResponse.Failure -> {
199 | send(ShowErrorDialog(true))
200 | }
201 | }
202 | }.launchIn(viewModelScope)
203 | }
204 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/translatefeature/redux/TranslateScreenIntention.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.translatefeature.redux
2 |
3 | import dev.atajan.lingva_android.common.domain.models.language.Language
4 | import dev.atajan.lingva_android.common.domain.models.translation.TranslationWithInfo
5 | import dev.atajan.lingva_android.common.ui.theme.ThemingOptions
6 |
7 | sealed interface TranslateScreenIntention {
8 | data class DefaultSourceLanguageSelected(val language: Language) : TranslateScreenIntention
9 | data class DefaultTargetLanguageSelected(val language: Language) : TranslateScreenIntention
10 | data class SetDefaultSourceLanguage(val languageName: String) : TranslateScreenIntention
11 | data class SetDefaultTargetLanguage(val languageName: String) : TranslateScreenIntention
12 | data class UserToggleLiveTranslate(val enabled: Boolean) : TranslateScreenIntention
13 | data class SetLiveTranslate(val enabled: Boolean) : TranslateScreenIntention
14 | data class SetNewSourceLanguage(val language: Language) : TranslateScreenIntention
15 | data class SetNewTargetLanguage(val language: Language) : TranslateScreenIntention
16 | data class ShowErrorDialog(val show: Boolean) : TranslateScreenIntention
17 | data class SupportedLanguagesReceived(val languages: List) : TranslateScreenIntention
18 | data class ToggleAppTheme(val newTheme: ThemingOptions) : TranslateScreenIntention
19 | data class TranslationSuccess(val translationWithInfo: TranslationWithInfo) : TranslateScreenIntention
20 | data class UserUpdateCustomLingvaServerUrl(val url: String) : TranslateScreenIntention
21 | data class SetCustomLingvaServerUrl(val url: String) : TranslateScreenIntention
22 | object ClearCustomLingvaServerUrl : TranslateScreenIntention
23 | object ClearInputField : TranslateScreenIntention
24 | object CopyTextToClipboard : TranslateScreenIntention
25 | object DisplayPronunciation : TranslateScreenIntention
26 | object ReadTextOutLoud : TranslateScreenIntention
27 | object Translate : TranslateScreenIntention
28 | object TranslationFailure : TranslateScreenIntention
29 | object TrySwapLanguages : TranslateScreenIntention
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/translatefeature/redux/TranslateScreenSideEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.translatefeature.redux
2 |
3 | sealed interface TranslateScreenSideEffect
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/translatefeature/redux/TranslateScreenState.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.translatefeature.redux
2 |
3 | import dev.atajan.lingva_android.common.domain.models.language.Language
4 |
5 | data class TranslateScreenState(
6 | val defaultSourceLanguage: String = "",
7 | val defaultTargetLanguage: String = "",
8 | val displayPronunciation: Boolean = false,
9 | val errorDialogState: Boolean = false, // TODO: Should be a side effect.
10 | val liveTranslationEnabled: Boolean = true,
11 | val sourceLanguage: Language = Language("auto", "Detect"),
12 | val supportedLanguages: List = emptyList(),
13 | val targetLanguage: Language = Language("es", "Spanish"),
14 | val translatedText: String = "",
15 | val translatedTextPronunciation: String = "",
16 | val customLingvaServerUrl: String = "",
17 | )
18 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/atajan/lingva_android/translatefeature/screens/TranslateScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.translatefeature.screens
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.BorderStroke
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxHeight
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.text.KeyboardOptions
16 | import androidx.compose.foundation.text.selection.SelectionContainer
17 | import androidx.compose.foundation.verticalScroll
18 | import androidx.compose.material.Card
19 | import androidx.compose.material.ContentAlpha
20 | import androidx.compose.material.ExperimentalMaterialApi
21 | import androidx.compose.material.ModalBottomSheetValue
22 | import androidx.compose.material.OutlinedTextField
23 | import androidx.compose.material.TextFieldDefaults
24 | import androidx.compose.material.icons.Icons
25 | import androidx.compose.material.icons.rounded.ContentCopy
26 | import androidx.compose.material.icons.rounded.Delete
27 | import androidx.compose.material.icons.rounded.SpeakerNotes
28 | import androidx.compose.material.icons.rounded.SwapHoriz
29 | import androidx.compose.material.icons.rounded.Translate
30 | import androidx.compose.material.icons.rounded.VolumeUp
31 | import androidx.compose.material.rememberModalBottomSheetState
32 | import androidx.compose.material3.Icon
33 | import androidx.compose.material3.IconButton
34 | import androidx.compose.material3.MaterialTheme
35 | import androidx.compose.material3.Text
36 | import androidx.compose.runtime.Composable
37 | import androidx.compose.runtime.LaunchedEffect
38 | import androidx.compose.runtime.MutableState
39 | import androidx.compose.runtime.collectAsState
40 | import androidx.compose.runtime.getValue
41 | import androidx.compose.runtime.mutableStateOf
42 | import androidx.compose.runtime.remember
43 | import androidx.compose.runtime.rememberCoroutineScope
44 | import androidx.compose.ui.Alignment
45 | import androidx.compose.ui.Modifier
46 | import androidx.compose.ui.platform.LocalContext
47 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController
48 | import androidx.compose.ui.text.input.ImeAction
49 | import androidx.compose.ui.unit.dp
50 | import dev.atajan.lingva_android.R
51 | import dev.atajan.lingva_android.common.ui.components.ErrorNotificationDialog
52 | import dev.atajan.lingva_android.common.ui.components.LanguageSelectionAndSettingsBar
53 | import dev.atajan.lingva_android.common.ui.components.SettingsBottomSheet
54 | import dev.atajan.lingva_android.common.ui.theme.ThemingOptions
55 | import dev.atajan.lingva_android.common.ui.theme.mediumRoundedCornerShape
56 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.ClearInputField
57 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.CopyTextToClipboard
58 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.DefaultSourceLanguageSelected
59 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.DefaultTargetLanguageSelected
60 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.DisplayPronunciation
61 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.ReadTextOutLoud
62 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.SetNewSourceLanguage
63 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.SetNewTargetLanguage
64 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.ShowErrorDialog
65 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.ToggleAppTheme
66 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.Translate
67 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.TrySwapLanguages
68 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.UserToggleLiveTranslate
69 | import dev.atajan.lingva_android.translatefeature.redux.TranslateScreenIntention.UserUpdateCustomLingvaServerUrl
70 | import kotlinx.coroutines.launch
71 |
72 | @ExperimentalMaterialApi
73 | @Composable
74 | fun TranslationScreen(
75 | viewModel: TranslateScreenViewModel,
76 | currentTheme: MutableState
77 | ) {
78 | val context = LocalContext.current
79 | val scrollState = rememberScrollState(0)
80 | val softwareKeyboardController = LocalSoftwareKeyboardController.current
81 | val scope = rememberCoroutineScope()
82 | val modalBottomSheetState = rememberModalBottomSheetState(
83 | initialValue = ModalBottomSheetValue.Hidden,
84 | confirmStateChange = { it != ModalBottomSheetValue.HalfExpanded }
85 | )
86 | val translationScreenState by viewModel.states.collectAsState()
87 | val customLingvaServerUrl = remember { mutableStateOf("") }
88 | val textToTranslate by viewModel.textToTranslate.collectAsState()
89 |
90 | Column(
91 | modifier = Modifier
92 | .fillMaxSize()
93 | .background(MaterialTheme.colorScheme.background)
94 | ) {
95 | LanguageSelectionAndSettingsBar(
96 | supportedLanguages = translationScreenState.supportedLanguages,
97 | sourceLanguage = translationScreenState.sourceLanguage,
98 | targetLanguage = translationScreenState.targetLanguage,
99 | toggleErrorDialogState = {
100 | viewModel.send(ShowErrorDialog(it))
101 | },
102 | middleIcon = Icons.Rounded.SwapHoriz,
103 | onMiddleIconTap = { viewModel.send(TrySwapLanguages) },
104 | onNewSourceLanguageSelected = { viewModel.send(SetNewSourceLanguage(it)) },
105 | onNewTargetLanguageSelected = { viewModel.send(SetNewTargetLanguage(it)) },
106 | onEndIconTap = {
107 | scope.launch {
108 | if (modalBottomSheetState.isVisible) {
109 | modalBottomSheetState.hide()
110 | } else {
111 | modalBottomSheetState.show()
112 | }
113 | }
114 | },
115 | modifier = Modifier
116 | .padding(horizontal = 16.dp)
117 | .padding(top = 16.dp)
118 | )
119 |
120 | Box(
121 | modifier = Modifier
122 | .fillMaxWidth()
123 | .fillMaxHeight(0.4f)
124 | .padding(all = 16.dp)
125 | ) {
126 | OutlinedTextField(
127 | value = textToTranslate,
128 | onValueChange = {
129 | viewModel.onTextToTranslateChange(it)
130 | },
131 | label = {
132 | Text(
133 | text = context.getString(R.string.source_text),
134 | color = MaterialTheme.colorScheme.primary
135 | )
136 | },
137 | modifier = Modifier.fillMaxSize(),
138 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Default),
139 | textStyle = MaterialTheme.typography.titleMedium,
140 | colors = TextFieldDefaults.outlinedTextFieldColors(
141 | focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
142 | unfocusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.disabled),
143 | focusedLabelColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.high),
144 | unfocusedLabelColor = MaterialTheme.colorScheme.primary.copy(alpha = ContentAlpha.disabled),
145 | cursorColor = MaterialTheme.colorScheme.primary,
146 | textColor = MaterialTheme.colorScheme.onBackground,
147 | )
148 | )
149 |
150 | Column(
151 | modifier = Modifier.fillMaxSize(),
152 | horizontalAlignment = Alignment.End,
153 | verticalArrangement = Arrangement.Bottom
154 | ) {
155 | AnimatedVisibility(textToTranslate.isNotEmpty()) {
156 | Row(
157 | modifier = Modifier.fillMaxWidth(),
158 | horizontalArrangement = Arrangement.SpaceBetween
159 | ) {
160 | IconButton(
161 | onClick = {
162 | softwareKeyboardController?.hide()
163 | viewModel.send(ClearInputField)
164 | },
165 | modifier = Modifier
166 | .padding(bottom = 16.dp)
167 | ) {
168 | Icon(
169 | imageVector = Icons.Rounded.Delete,
170 | contentDescription = context.getString(R.string.delete_icon_ax),
171 | tint = MaterialTheme.colorScheme.primary,
172 | )
173 | }
174 |
175 | IconButton(
176 | onClick = {
177 | softwareKeyboardController?.hide()
178 | viewModel.send(Translate)
179 | },
180 | modifier = Modifier
181 | .padding(bottom = 16.dp)
182 | ) {
183 | Icon(
184 | imageVector = Icons.Rounded.Translate,
185 | contentDescription = context.getString(R.string.translate_icon_ax),
186 | tint = MaterialTheme.colorScheme.primary,
187 | )
188 | }
189 | }
190 | }
191 | }
192 | }
193 |
194 | AnimatedVisibility(translationScreenState.translatedText.isNotEmpty()) {
195 | Box(
196 | modifier = Modifier
197 | .fillMaxWidth()
198 | .fillMaxHeight()
199 | .padding(all = 16.dp)
200 | ) {
201 | Card(
202 | shape = mediumRoundedCornerShape,
203 | border = BorderStroke(
204 | width = 2.dp,
205 | color = MaterialTheme.colorScheme.primary
206 | ),
207 | elevation = 0.dp,
208 | backgroundColor = MaterialTheme.colorScheme.background,
209 | modifier = Modifier.fillMaxSize(),
210 | ) {
211 | SelectionContainer {
212 | Text(
213 | text = if (translationScreenState.displayPronunciation) {
214 | translationScreenState.translatedTextPronunciation
215 | } else {
216 | translationScreenState.translatedText
217 | },
218 | color = MaterialTheme.colorScheme.onBackground,
219 | style = MaterialTheme.typography.titleMedium,
220 | modifier = Modifier
221 | .padding(16.dp)
222 | .verticalScroll(scrollState)
223 | )
224 | }
225 | }
226 |
227 | Column(
228 | modifier = Modifier.fillMaxSize(),
229 | horizontalAlignment = Alignment.End,
230 | verticalArrangement = Arrangement.Bottom
231 | ) {
232 | Row(
233 | modifier = Modifier.fillMaxWidth(),
234 | horizontalArrangement = Arrangement.SpaceBetween
235 | ) {
236 | IconButton(
237 | onClick = {
238 | viewModel.send(ReadTextOutLoud)
239 | },
240 | modifier = Modifier.padding(bottom = 16.dp)
241 | ) {
242 | Icon(
243 | imageVector = Icons.Rounded.VolumeUp,
244 | contentDescription = context.getString(R.string.text_to_speech_icon_ax),
245 | tint = MaterialTheme.colorScheme.primary,
246 | )
247 | }
248 |
249 | IconButton(
250 | onClick = {
251 | viewModel.send(DisplayPronunciation)
252 | },
253 | modifier = Modifier.padding(bottom = 16.dp)
254 | ) {
255 | Icon(
256 | imageVector = Icons.Rounded.SpeakerNotes,
257 | contentDescription = context.getString(R.string.text_to_speech_icon_ax),
258 | tint = MaterialTheme.colorScheme.primary,
259 | )
260 | }
261 |
262 | IconButton(
263 | onClick = {
264 | viewModel.send(CopyTextToClipboard)
265 | },
266 | modifier = Modifier.padding(bottom = 16.dp),
267 | ) {
268 | Icon(
269 | imageVector = Icons.Rounded.ContentCopy,
270 | contentDescription = context.getString(R.string.copy_icon_ax),
271 | tint = MaterialTheme.colorScheme.primary,
272 | )
273 | }
274 | }
275 | }
276 | }
277 | }
278 | }
279 |
280 | SettingsBottomSheet(
281 | modalBottomSheetState = modalBottomSheetState,
282 | toggleTheme = {
283 | viewModel.send(ToggleAppTheme(it))
284 | },
285 | currentTheme = currentTheme,
286 | setDefaultSourceLanguage = {
287 | viewModel.send(DefaultSourceLanguageSelected(it))
288 | },
289 | setDefaultTargetLanguage = {
290 | viewModel.send(DefaultTargetLanguageSelected(it))
291 | },
292 | supportedLanguages = translationScreenState.supportedLanguages,
293 | defaultSourceLanguage = translationScreenState.defaultSourceLanguage,
294 | defaultTargetLanguage = translationScreenState.defaultTargetLanguage,
295 | toggleErrorDialogState = {
296 | viewModel.send(ShowErrorDialog(it))
297 | },
298 | customLingvaServerUrl = customLingvaServerUrl,
299 | liveTranslateEnabled = translationScreenState.liveTranslationEnabled,
300 | onToggleLiveTranslate = { viewModel.send(UserToggleLiveTranslate(it)) },
301 | currentCustomLingvaServerUrl = translationScreenState.customLingvaServerUrl,
302 | context = context,
303 | )
304 |
305 | LaunchedEffect(!modalBottomSheetState.isVisible) {
306 | if (customLingvaServerUrl.value.isNotEmpty()) {
307 | viewModel.send(UserUpdateCustomLingvaServerUrl(customLingvaServerUrl.value))
308 | customLingvaServerUrl.value = ""
309 | }
310 | }
311 |
312 | ErrorNotificationDialog(translationScreenState.errorDialogState) {
313 | viewModel.send(ShowErrorDialog(false))
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
19 |
23 |
27 |
31 |
35 |
39 |
43 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
19 |
23 |
27 |
31 |
35 |
40 |
44 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Lentil Translate
4 | Quellentext
5 | App-Thema
6 | Basierend auf dem Hintergrundbild und Thema Ihres Geräts
7 | Etwas ist schief gelaufen. Bitte versuchen Sie es später erneut.
8 | Okay
9 | Standardsprachen
10 | Ausgangssprache:
11 | Zielsprache:
12 | Tippen Sie hier, um auszuwählen
13 | Erkennen
14 | Erweiterte Einstellungen
15 | Benutzerdefinierte Übersetzungsserver-URL
16 | Benutzerdefinierte Lingva-Server-URL
17 | Live-Übersetzung umschalten
18 |
19 |
20 | Ausgangs- und Zielsprache tauschen
21 | Einstellung
22 | Eingabetext löschen
23 | Übersetzen
24 | Kopieren
25 | Übersetzten Text abspielen
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Texte source
4 | Thème de l\'application
5 | Basé sur le fond d\'écran et le thème de votre appareil
6 | Quelque chose s\'est mal passé. Veuillez réessayer plus tard.
7 | D\'accord
8 | Langues par défaut
9 | Langue originelle:
10 | Langue cible:
11 | Appuyez ici pour sélectionner
12 | Détecter
13 | Paramètres avancés
14 | URL de serveur de traduction personnalisé
15 | URL de serveur Lingva personnalisé
16 | Basculer la traduction en direct
17 |
18 |
19 | Permuter les langues source et cible
20 | Paramètre
21 | Effacer le texte saisi
22 | Traduire
23 | Copie
24 | Lire le texte traduit
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Testo sorgente
4 | Tema dell\'app
5 | In base allo sfondo e al tema del tuo dispositivo
6 | Qualcosa è andato storto. Per favore riprova più tardi.
7 | Bene
8 | Lingue predefinite
9 | Linguaggio del codice:
10 | Lingua di destinazione:
11 | Tocca qui per selezionare
12 | Rileva
13 | Impostazioni avanzate
14 | URL server di traduzione personalizzato
15 | URL server lingva personalizzato
16 | Attiva/Disattiva traduzione dal vivo
17 |
18 |
19 | Scambia le lingue di origine e di destinazione
20 | Ambientazione
21 | Cancella il testo di input
22 | Tradurre
23 | copia
24 | Riproduci il testo tradotto
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | ソーステキスト
4 | アプリのテーマ
5 | デバイスの壁紙とテーマに基づく
6 | エラーが発生しました。後でもう一度やり直してください。
7 | オッケー
8 | デフォルト言語
9 | ソース言語:
10 | 目標とする言語:
11 | ここをタップして選択
12 | 探知
13 | 詳細設定
14 | カスタム翻訳サーバーURL
15 | カスタムlingvaサーバーURL
16 | ライブ翻訳を切り替える
17 |
18 |
19 | ソース言語とターゲット言語を入れ替える
20 | 設定
21 | 入力テキストをクリア
22 | 翻訳
23 | コピー
24 | 翻訳されたテキストを再生
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Texto original
4 | Tema do aplicativo
5 | Com base no papel de parede e no tema do seu dispositivo
6 | Algo deu errado. Por favor, tente novamente mais tarde.
7 | Ok
8 | Idiomas padrão
9 | Idioma de origem:
10 | Idioma de destino:
11 | Toque aqui para selecionar
12 | Detectar
13 | Configurações avançadas
14 | URL do servidor de tradução personalizada
15 | URL do servidor lingva personalizado
16 | Alternar tradução ao vivo
17 |
18 |
19 | Troque os idiomas de origem e de destino
20 | Contexto
21 | Limpar texto de entrada
22 | Traduzir
23 | cópia de
24 | Reproduzir texto traduzido
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Texto original
4 | Tema do aplicativo
5 | Com base no papel de parede e no tema do seu dispositivo
6 | Algo deu errado. Por favor, tente novamente mais tarde.
7 | Ok
8 | Idiomas padrão
9 | Idioma de origem:
10 | Idioma de destino:
11 | Toque aqui para selecionar
12 | Detectar
13 | Configurações avançadas
14 | URL do servidor de tradução personalizada
15 | URL do servidor lingva personalizado
16 | Alternar tradução ao vivo
17 |
18 |
19 | Troque os idiomas de origem e de destino
20 | Contexto
21 | Limpar texto de entrada
22 | Traduzir
23 | cópia de
24 | Reproduzir texto traduzido
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Исходный текст
4 | Тема приложения
5 | На основе обоев и темы вашего устройства
6 | Что-то пошло не так. Пожалуйста, попробуйте позже.
7 | Хорошо
8 | Языки по умолчанию
9 | Исходный язык:
10 | Язык перевода:
11 | Нажмите здесь, чтобы выбрать
12 | Обнаружить
13 | Расширенные настройки
14 | URL-адрес пользовательского сервера перевода
15 | URL-адрес пользовательского сервера Lingva
16 | Переключить живой перевод
17 |
18 |
19 | Поменять местами исходный и целевой языки
20 | Параметр
21 | Очистить вводимый текст
22 | Перевести
23 | Копировать
24 | Воспроизвести переведенный текст
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Kaynak metin
4 | Uygulama Teması
5 | Aygıtınızın duvar kağıdına ve temasına göre
6 | Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyin.
7 | Tamam
8 | Öntanımlı Diller
9 | Kaynak Dil:
10 | Hedef Dil:
11 | Seçmek için buraya dokunun
12 | Algıla
13 | Gelişmiş Ayarlar
14 | Özel çeviri sunucusu URL\'si
15 | Özel lingva sunucusu URL\'si
16 | Canlı çeviriyi aç/kapat
17 |
18 |
19 | Kaynak ve hedef dilleri değiştir
20 | Ayar
21 | Giriş metnini temizle
22 | Çevir
23 | Kopyala
24 | Çevrilen metni oku
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #287F8F
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lentil Translate
3 | Source text
4 | App Theme
5 | Based on your device wallpaper and theme
6 | Something went wrong. Please try again later.
7 | Okay
8 | Default Languages
9 | Source Language:
10 | Target Language:
11 | Tap here to select
12 | Detect
13 | Advanced Settings
14 | Custom translation server URL
15 | Custom lingva server URL
16 | Toggle live translate
17 |
18 |
19 | Swap source and target languages
20 | Setting
21 | Clear input text
22 | Translate
23 | Copy
24 | Playback translated text
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/atajan/lingva_android/common/domain/language/LanguageTest.kt:
--------------------------------------------------------------------------------
1 | package dev.atajan.lingva_android.common.domain.language
2 |
3 | import dev.atajan.lingva_android.common.data.api.lingvadto.language.LanguageDTO
4 | import dev.atajan.lingva_android.common.domain.errors.DTOToDomainModelMappingError.NullValue
5 | import dev.atajan.lingva_android.common.domain.models.language.Language
6 | import dev.atajan.lingva_android.common.domain.models.language.Language.Companion.toDomainModel
7 | import dev.atajan.lingva_android.common.domain.models.language.containsLanguageOrNull
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Assert.assertNull
10 | import org.junit.Assert.assertThrows
11 | import org.junit.Test
12 |
13 | class LanguageTest {
14 |
15 | @Test
16 | fun `when DTO contains null code then throws NullValue exception`() {
17 | assertThrows("language code can't be null", NullValue::class.java) {
18 | LanguageDTO(
19 | code = null,
20 | name = "English"
21 | ).toDomainModel()
22 | }
23 | }
24 |
25 | @Test
26 | fun `when DTO contains null name then throws NullValue exception`() {
27 | assertThrows("language name can't be null", NullValue::class.java) {
28 | LanguageDTO(
29 | code = "en",
30 | name = null
31 | ).toDomainModel()
32 | }
33 | }
34 |
35 | @Test
36 | fun `containsLanguageOrNull returns language with matching code`() {
37 | val languages = listOf(
38 | Language(code = "en", name = "English"),
39 | Language(code = "jpn", name = "Japanese"),
40 | )
41 | val actual = languages.containsLanguageOrNull("jpn")
42 |
43 | assertEquals(languages[1], actual)
44 | }
45 |
46 | @Test
47 | fun `containsLanguageOrNull returns null with no matching code`() {
48 | val languages = listOf(
49 | Language(code = "en", name = "English"),
50 | Language(code = "jpn", name = "Japanese"),
51 | )
52 | val actual = languages.containsLanguageOrNull("es")
53 |
54 | assertNull(actual)
55 | }
56 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 |
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 |
9 | dependencies {
10 | classpath("com.android.tools.build:gradle:7.4.2")
11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
12 | classpath("com.google.dagger:hilt-android-gradle-plugin:2.44")
13 | classpath ("org.jetbrains.kotlin:kotlin-serialization:1.8.10")
14 | }
15 | }
16 |
17 | tasks.register("clean", Delete::class) {
18 | delete(rootProject.buildDir)
19 | }
20 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/15.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | * Fixed issue where kotlinx serializer considered nullable field required even though you marked as nullable.
3 | * Removed shutdown API endpoints and added few new ones.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/16.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | * Fixed issue were translation would error out when forward slash was used in the translation string. https://github.com/yaxarat/lingvaandroid/pull/26
3 | * Enter key will now create new line. https://github.com/yaxarat/lingvaandroid/pull/27
4 | * Added quick translate shortcut. You can now highlight a text and directly translate from context action. https://github.com/yaxarat/lingvaandroid/pull/28
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/17.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | * Fixed issue that affected translations in the release build.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/18.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | What's New in 1.3.0:
3 | * Fixed Keyboard Bug: We have resolved a frustrating bug where text insertion and deletion were not smooth.
4 | * Audio Playback Feature: We have added an audio playback feature that allows you to listen to translations.
5 | * Pronunciation Feature: We have added a pronunciation feature. Custom Lingva
6 | * Endpoint Feature: We have introduced a custom Lingva endpoint feature.
7 | * Fixed Other Bugs: We have also addressed other minor bugs, including inconsistent settings.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/19.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | What's New in 1.3.1:
3 | * Fixed crashes seen in Android devices with API level 30 or below.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/20.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | What's New in 1.3.2:
3 | *New feature! Live translate toggle added. It's enabled by default but can be toggled off in the settings. Bug that didn't show currently set custom endpoint has been fixed. More contrast to language selection dialog.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/21.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | What's New in 1.3.3:
3 | Hot fix for translation failures caused by a bad response.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/22.txt:
--------------------------------------------------------------------------------
1 | # Changelog
2 | What's New in 1.3.4:
3 | * Stability improvements
4 | * Library updated
5 | * Target SDK bumped to 34
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | An unofficial Android client for Lingva Translate. Lingva translator is an open-source translator with over one hundred languages available.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaxarat/lingvaandroid/a159c29933e037f31b11f68b4a243b82bef0be52/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaxarat/lingvaandroid/a159c29933e037f31b11f68b4a243b82bef0be52/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaxarat/lingvaandroid/a159c29933e037f31b11f68b4a243b82bef0be52/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaxarat/lingvaandroid/a159c29933e037f31b11f68b4a243b82bef0be52/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Privacy focused open source translator.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Lentil Translate
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yaxarat/lingvaandroid/a159c29933e037f31b11f68b4a243b82bef0be52/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/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/master/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 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || 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 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/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 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | rootProject.name = "lingva-android"
9 | include(":app")
10 |
--------------------------------------------------------------------------------