├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
└── vcs.xml
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── ic_launcher_purple-playstore.png
│ ├── ic_launcher_purpleeeeeee-playstore.png
│ ├── ic_launcher_red-playstore.png
│ ├── ic_launcher_white-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── ifeanyi
│ │ │ └── read
│ │ │ ├── ReadActivity.kt
│ │ │ ├── app
│ │ │ ├── MainActivity.kt
│ │ │ ├── data
│ │ │ │ ├── LibraryRepository.kt
│ │ │ │ ├── SettingsRepository.kt
│ │ │ │ ├── models
│ │ │ │ │ ├── FileModel.kt
│ │ │ │ │ ├── FolderModel.kt
│ │ │ │ │ └── WhatsNewModel.kt
│ │ │ │ └── source
│ │ │ │ │ ├── FileDao.kt
│ │ │ │ │ ├── FolderDao.kt
│ │ │ │ │ └── WhatsNewDao.kt
│ │ │ └── presentation
│ │ │ │ ├── components
│ │ │ │ ├── AppButton.kt
│ │ │ │ ├── BottomNavigationBarComponent.kt
│ │ │ │ ├── CustomSliderSheet.kt
│ │ │ │ ├── GridTileComponent.kt
│ │ │ │ ├── ListTileComponent.kt
│ │ │ │ ├── LoaderComponent.kt
│ │ │ │ ├── PlayerComponent.kt
│ │ │ │ ├── SettingsItem.kt
│ │ │ │ ├── TextFieldComponent.kt
│ │ │ │ ├── VoiceSelectorSheet.kt
│ │ │ │ └── WaveForm.kt
│ │ │ │ ├── viewmodel
│ │ │ │ ├── LibraryViewModel.kt
│ │ │ │ └── SettingsViewModel.kt
│ │ │ │ └── views
│ │ │ │ ├── home
│ │ │ │ ├── EnterTextScreen.kt
│ │ │ │ ├── EnterUrlSheet.kt
│ │ │ │ └── HomeScreen.kt
│ │ │ │ ├── library
│ │ │ │ ├── CreateFolderSheet.kt
│ │ │ │ ├── DefaultDialog.kt
│ │ │ │ ├── FolderScreen.kt
│ │ │ │ ├── LibraryScreen.kt
│ │ │ │ ├── MoveFilesSheet.kt
│ │ │ │ ├── RenameSheet.kt
│ │ │ │ ├── SelectingDialog.kt
│ │ │ │ ├── SelectingTopBar.kt
│ │ │ │ └── SortDialog.kt
│ │ │ │ ├── setting
│ │ │ │ ├── AboutAppScreen.kt
│ │ │ │ ├── AppearanceScreen.kt
│ │ │ │ ├── DisplayDialog.kt
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── TextToSpeechScreen.kt
│ │ │ │ ├── ThemeDialog.kt
│ │ │ │ └── WhatsNewSheet.kt
│ │ │ │ └── speech
│ │ │ │ ├── GoToPageSheet.kt
│ │ │ │ ├── HighlightedText.kt
│ │ │ │ └── SpeechScreen.kt
│ │ │ └── core
│ │ │ ├── di
│ │ │ └── AppModule.kt
│ │ │ ├── enums
│ │ │ ├── ActivityType.kt
│ │ │ ├── AppTheme.kt
│ │ │ └── DisplayStyle.kt
│ │ │ ├── route
│ │ │ ├── BottomRouter.kt
│ │ │ ├── Router.kt
│ │ │ └── Routes.kt
│ │ │ ├── services
│ │ │ ├── AnalyticService.kt
│ │ │ ├── DatabaseService.kt
│ │ │ ├── NotificationService.kt
│ │ │ ├── PreferenceService.kt
│ │ │ ├── SnackbarService.kt
│ │ │ └── SpeechService.kt
│ │ │ ├── theme
│ │ │ ├── AppIcons.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ └── util
│ │ │ ├── Constants.kt
│ │ │ ├── Extentions.kt
│ │ │ ├── RoomConverters.kt
│ │ │ └── TextParser.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_launcher_purple_background.xml
│ │ ├── ic_launcher_red_background.xml
│ │ ├── ic_launcher_white_background.xml
│ │ ├── purple_logo.png
│ │ ├── red_logo.png
│ │ ├── round_forward_10_24.xml
│ │ ├── round_pause_24.xml
│ │ ├── round_play_arrow_24.xml
│ │ ├── round_record_voice_over_24.xml
│ │ ├── round_replay_10_24.xml
│ │ └── white_logo.png
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher_purple.xml
│ │ ├── ic_launcher_purple_round.xml
│ │ ├── ic_launcher_red.xml
│ │ ├── ic_launcher_red_round.xml
│ │ ├── ic_launcher_white.xml
│ │ └── ic_launcher_white_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher_purple.webp
│ │ ├── ic_launcher_purple_foreground.webp
│ │ ├── ic_launcher_purple_round.webp
│ │ ├── ic_launcher_red.webp
│ │ ├── ic_launcher_red_foreground.webp
│ │ ├── ic_launcher_red_round.webp
│ │ ├── ic_launcher_white.webp
│ │ ├── ic_launcher_white_foreground.webp
│ │ └── ic_launcher_white_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher_purple.webp
│ │ ├── ic_launcher_purple_foreground.webp
│ │ ├── ic_launcher_purple_round.webp
│ │ ├── ic_launcher_red.webp
│ │ ├── ic_launcher_red_foreground.webp
│ │ ├── ic_launcher_red_round.webp
│ │ ├── ic_launcher_white.webp
│ │ ├── ic_launcher_white_foreground.webp
│ │ └── ic_launcher_white_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher_purple.webp
│ │ ├── ic_launcher_purple_foreground.webp
│ │ ├── ic_launcher_purple_round.webp
│ │ ├── ic_launcher_red.webp
│ │ ├── ic_launcher_red_foreground.webp
│ │ ├── ic_launcher_red_round.webp
│ │ ├── ic_launcher_white.webp
│ │ ├── ic_launcher_white_foreground.webp
│ │ └── ic_launcher_white_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher_purple.webp
│ │ ├── ic_launcher_purple_foreground.webp
│ │ ├── ic_launcher_purple_round.webp
│ │ ├── ic_launcher_red.webp
│ │ ├── ic_launcher_red_foreground.webp
│ │ ├── ic_launcher_red_round.webp
│ │ ├── ic_launcher_white.webp
│ │ ├── ic_launcher_white_foreground.webp
│ │ └── ic_launcher_white_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher_purple.webp
│ │ ├── ic_launcher_purple_foreground.webp
│ │ ├── ic_launcher_purple_round.webp
│ │ ├── ic_launcher_red.webp
│ │ ├── ic_launcher_red_foreground.webp
│ │ ├── ic_launcher_red_round.webp
│ │ ├── ic_launcher_white.webp
│ │ ├── ic_launcher_white_foreground.webp
│ │ └── ic_launcher_white_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── font_certs.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── android
│ └── ifeanyi
│ └── read
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | app/src/main/java/com/ifeanyi/read/core/util/Secrets.kt
17 | /app/release
18 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.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 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("kotlin-kapt")
5 | id("com.google.dagger.hilt.android")
6 | id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
7 | }
8 |
9 | android {
10 | signingConfigs {
11 | create("release") {
12 | storeFile = file("/Users/ifeanyionuoha/read_keystore.jks")
13 | storePassword = "readkeystore"
14 | keyPassword = "readkeystore"
15 | keyAlias = "release"
16 | }
17 | }
18 | namespace = "com.ifeanyi.read"
19 | compileSdk = 34
20 |
21 | defaultConfig {
22 | applicationId = "com.ifeanyi.read"
23 | minSdk = 26
24 | targetSdk = 34
25 | versionCode = 13
26 | versionName = "1.1.3"
27 |
28 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
29 | vectorDrawables {
30 | useSupportLibrary = true
31 | }
32 | }
33 |
34 | buildTypes {
35 | release {
36 | isMinifyEnabled = false
37 | proguardFiles(
38 | getDefaultProguardFile("proguard-android-optimize.txt"),
39 | "proguard-rules.pro"
40 | )
41 | signingConfig = signingConfigs.getByName("debug")
42 | }
43 | }
44 | compileOptions {
45 | sourceCompatibility = JavaVersion.VERSION_1_8
46 | targetCompatibility = JavaVersion.VERSION_1_8
47 | }
48 | kotlinOptions {
49 | jvmTarget = "1.8"
50 | }
51 | buildFeatures {
52 | compose = true
53 | buildConfig = true
54 | }
55 | composeOptions {
56 | kotlinCompilerExtensionVersion = "1.5.5"
57 | }
58 | packaging {
59 | resources {
60 | excludes.add("/META-INF/{AL2.0,LGPL2.1}")
61 | excludes.add("META-INF/DEPENDENCIES")
62 | excludes.add("META-INF/LICENSE.md")
63 | excludes.add("META-INF/LICENSE-notice.md")
64 | }
65 | }
66 | }
67 |
68 | dependencies {
69 | // Room Database
70 | implementation("androidx.room:room-runtime:2.6.1")
71 | // noinspection KaptUsageInsteadOfKsp
72 | kapt("androidx.room:room-compiler:2.6.1")
73 | implementation("androidx.room:room-ktx:2.6.1")
74 | // PDF to text
75 | implementation("com.tom-roush:pdfbox-android:2.0.27.0")
76 | // Image to text
77 | implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0")
78 | // URL to text
79 | implementation("org.jsoup:jsoup:1.14.3")
80 | // Font
81 | implementation("androidx.compose.ui:ui-text-google-fonts:1.6.8")
82 | // Navigation
83 | implementation("androidx.navigation:navigation-compose:2.7.7")
84 | // Coroutines
85 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
86 | // Hilt
87 | implementation("com.google.dagger:hilt-android:2.49")
88 | kapt("com.google.dagger:hilt-android-compiler:2.48")
89 | implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
90 | // Material icons
91 | implementation("androidx.compose.material:material-icons-extended:1.6.8")
92 | // Preferences
93 | implementation("androidx.datastore:datastore-preferences:1.1.1")
94 | // GSON
95 | implementation("com.google.code.gson:gson:2.10.1")
96 | // Media notification
97 | implementation("androidx.media:media:1.7.0")
98 | // Mixpanel
99 | implementation("com.mixpanel.android:mixpanel-android:7.5.0")
100 | // Gemini
101 | implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
102 | // Scanner
103 | implementation("com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1")
104 |
105 | implementation("androidx.core:core-ktx:1.13.1")
106 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
107 | implementation("androidx.activity:activity-compose:1.9.0")
108 | implementation(platform("androidx.compose:compose-bom:2023.03.00"))
109 | implementation("androidx.compose.ui:ui")
110 | implementation("androidx.compose.ui:ui-graphics")
111 | implementation("androidx.compose.ui:ui-tooling-preview")
112 | implementation("androidx.compose.material3:material3:1.2.1")
113 | testImplementation("junit:junit:4.13.2")
114 | androidTestImplementation("androidx.test.ext:junit:1.2.1")
115 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
116 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
117 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
118 | debugImplementation("androidx.compose.ui:ui-tooling")
119 | debugImplementation("androidx.compose.ui:ui-test-manifest")
120 | }
--------------------------------------------------------------------------------
/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.
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/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
20 |
21 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher_purple-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/ic_launcher_purple-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher_purpleeeeeee-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/ic_launcher_purpleeeeeee-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher_red-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/ic_launcher_red-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher_white-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/ic_launcher_white-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/ReadActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class ReadActivity: Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app
2 |
3 | import com.ifeanyi.read.app.presentation.components.BottomNavigationBarComponent
4 | import com.ifeanyi.read.app.presentation.components.PlayerComponent
5 | import com.ifeanyi.read.app.presentation.views.speech.SpeechScreen
6 | import com.ifeanyi.read.core.route.Router
7 | import com.ifeanyi.read.core.services.AnalyticService
8 | import com.ifeanyi.read.core.services.AppStateService
9 | import android.os.Bundle
10 | import androidx.activity.ComponentActivity
11 | import androidx.activity.compose.setContent
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.material3.Text
14 | import androidx.compose.ui.Modifier
15 | import com.ifeanyi.read.core.theme.ReadTheme
16 | import android.os.Build
17 | import androidx.annotation.RequiresApi
18 | import androidx.compose.animation.AnimatedContent
19 | import androidx.compose.animation.AnimatedVisibility
20 | import androidx.compose.animation.core.tween
21 | import androidx.compose.animation.fadeIn
22 | import androidx.compose.animation.fadeOut
23 | import androidx.compose.animation.slideInVertically
24 | import androidx.compose.animation.slideOutVertically
25 | import androidx.compose.animation.togetherWith
26 | import androidx.compose.foundation.layout.Box
27 | import androidx.compose.foundation.layout.Column
28 | import androidx.compose.foundation.layout.padding
29 | import androidx.compose.material3.Snackbar
30 | import androidx.compose.runtime.collectAsState
31 | import androidx.compose.runtime.getValue
32 | import androidx.compose.runtime.mutableStateOf
33 | import androidx.compose.runtime.remember
34 | import androidx.compose.runtime.setValue
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.unit.dp
37 | import androidx.navigation.compose.currentBackStackEntryAsState
38 | import androidx.navigation.compose.rememberNavController
39 | import com.ifeanyi.read.app.presentation.components.LoaderComponent
40 | import com.ifeanyi.read.core.enums.ActivityType
41 | import com.ifeanyi.read.core.services.notificationService
42 | import com.ifeanyi.read.core.util.changeIcon
43 | import dagger.hilt.android.AndroidEntryPoint
44 |
45 | @AndroidEntryPoint
46 | class MainActivity : ComponentActivity() {
47 | override fun onDestroy() {
48 | notificationService.destroy()
49 | super.onDestroy()
50 | }
51 |
52 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
53 | override fun onCreate(savedInstanceState: Bundle?) {
54 | super.onCreate(savedInstanceState)
55 |
56 | AnalyticService.init(this)
57 | notificationService.init(this)
58 |
59 |
60 |
61 | setContent {
62 | ReadTheme {
63 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
64 | val controller = rememberNavController()
65 | val navBackStackEntry = controller.currentBackStackEntryAsState()
66 | val currentDestination = navBackStackEntry.value?.destination
67 |
68 | var expanded by remember { mutableStateOf(false) }
69 |
70 | Router(
71 | controller = controller,
72 | onIconChangeRed = { this@MainActivity.changeIcon(ActivityType.MainActivity) },
73 | onIconChangePurple = { this@MainActivity.changeIcon(ActivityType.MainActivityPurple) },
74 | onIconChangeWhite = { this@MainActivity.changeIcon(ActivityType.MainActivityWhite) }
75 | )
76 |
77 | Column(modifier = Modifier.align(Alignment.BottomCenter)) {
78 | AnimatedContent(
79 | targetState = expanded,
80 | label = "Animated Player",
81 | transitionSpec = {
82 | slideInVertically(
83 | animationSpec = tween(300),
84 | initialOffsetY = { 0 }) togetherWith
85 | slideOutVertically(animationSpec = tween(300))
86 | }
87 | ) { isExpanded ->
88 | if (isExpanded) {
89 | SpeechScreen { expanded = false }
90 | }
91 | }
92 |
93 | PlayerComponent(expanded = expanded) { expanded = true }
94 |
95 | BottomNavigationBarComponent(controller, currentDestination)
96 | }
97 |
98 | val snackBar = AppStateService.snackBar.collectAsState().value
99 |
100 | AnimatedVisibility(
101 | modifier = Modifier.align(Alignment.TopCenter),
102 | visible = snackBar.hasMessage,
103 | enter = fadeIn(),
104 | exit = fadeOut(),
105 | ) {
106 | Snackbar(modifier = Modifier.padding(15.dp)) {
107 | Text(text = snackBar.message)
108 | }
109 | }
110 |
111 | val loader = AppStateService.loader.collectAsState().value
112 |
113 | AnimatedVisibility(
114 | modifier = Modifier.align(Alignment.Center),
115 | visible = loader.isLoading,
116 | enter = fadeIn(),
117 | exit = fadeOut(),
118 | ) {
119 | LoaderComponent(text = loader.message)
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/LibraryRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data
2 |
3 | import com.ifeanyi.read.app.data.models.FileModel
4 | import com.ifeanyi.read.app.data.models.FolderModel
5 | import com.ifeanyi.read.app.data.source.FileDao
6 | import com.ifeanyi.read.app.data.source.FolderDao
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.flow.conflate
9 | import kotlinx.coroutines.flow.flowOn
10 | import java.util.UUID
11 | import javax.inject.Inject
12 |
13 | class LibraryRepository @Inject constructor(private val fileDao: FileDao, private val folderDao: FolderDao) {
14 | suspend fun insertItem(item: FileModel) = fileDao.insert(item)
15 | suspend fun updateItem(item: FileModel) = fileDao.update(item)
16 | suspend fun deleteItem(item: FileModel) = fileDao.delete(item)
17 | fun getAllFiles() = fileDao.getAllFiles().flowOn(Dispatchers.IO).conflate()
18 | fun getFolderFiles(id: UUID) = fileDao.getFolderFiles(id).flowOn(Dispatchers.IO).conflate()
19 | fun getFolderFilesCount(id: UUID) = fileDao.getFolderFilesCount(id).flowOn(Dispatchers.IO).conflate()
20 |
21 | suspend fun insertItem(item: FolderModel) = folderDao.insert(item)
22 | suspend fun updateItem(item: FolderModel) = folderDao.update(item)
23 | suspend fun deleteItem(item: FolderModel) = folderDao.delete(item)
24 | fun getAllFolders() = folderDao.getAllFolders().flowOn(Dispatchers.IO).conflate()
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data
2 |
3 | import com.ifeanyi.read.app.data.models.WhatsNewModel
4 | import com.ifeanyi.read.app.data.source.WhatsNewDao
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.flow.conflate
7 | import kotlinx.coroutines.flow.flowOn
8 | import javax.inject.Inject
9 |
10 | class SettingsRepository @Inject constructor(private val whatsNewDao: WhatsNewDao) {
11 | suspend fun insertItem(item: WhatsNewModel) = whatsNewDao.insert(item)
12 | suspend fun getUpdate(id: String) = whatsNewDao.getUpdate(id)
13 | fun getAllUpdates() = whatsNewDao.getAllUpdates().flowOn(Dispatchers.IO).conflate()
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/models/FileModel.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data.models
2 |
3 | import android.content.Context
4 | import com.ifeanyi.read.core.theme.AppIcons
5 | import androidx.compose.ui.graphics.vector.ImageVector
6 | import androidx.core.net.toUri
7 | import androidx.room.ColumnInfo
8 | import androidx.room.Entity
9 | import androidx.room.PrimaryKey
10 | import com.ifeanyi.read.core.util.formatted
11 | import java.io.File
12 | import java.time.Instant
13 | import java.util.Date
14 | import java.util.UUID
15 |
16 | enum class LibraryType { Pdf, Img, Scan, Txt, Url }
17 |
18 | @Entity(tableName = "file_table")
19 | data class FileModel(
20 | @PrimaryKey
21 | val id: UUID = UUID.randomUUID(),
22 | @ColumnInfo
23 | val name: String,
24 | @ColumnInfo
25 | val type: LibraryType,
26 | @ColumnInfo
27 | val date: Date = Date.from(Instant.now()),
28 | @ColumnInfo
29 | val path: String,
30 | @ColumnInfo
31 | var cache: String? = null,
32 | @ColumnInfo
33 | val wordRange: IntRange = IntRange(0, 0),
34 | @ColumnInfo
35 | val wordIndex: Int = 0,
36 | @ColumnInfo
37 | val progress: Int = 0,
38 | @ColumnInfo
39 | val currentPage: Int = 1,
40 | @ColumnInfo
41 | val totalPages: Int = 1,
42 | @ColumnInfo
43 | val parent: UUID? = null,
44 | ) {
45 | fun icon(): ImageVector {
46 | return when (type) {
47 | LibraryType.Pdf -> AppIcons.doc
48 | LibraryType.Img -> AppIcons.image
49 | LibraryType.Scan -> AppIcons.scan
50 | LibraryType.Txt -> AppIcons.text
51 | LibraryType.Url -> AppIcons.link
52 | }
53 | }
54 |
55 | val absProgress: Int
56 | get() {
57 | val value = (currentPage.toDouble() / totalPages.toDouble()) * 100
58 | if (totalPages > 1) {
59 | return value.toInt()
60 | }
61 | return progress
62 | }
63 |
64 | fun readCache(): String? {
65 | return try {
66 | if (this.cache == null) return null
67 | val file = File(this.cache!!)
68 | file.readText().formatted
69 | } catch (_: Exception) {
70 | null
71 | }
72 | }
73 |
74 | fun writeCache(context: Context, text: String) {
75 | try {
76 | val path = "${context.filesDir.path}/${Instant.now()}.txt"
77 | val outputFile = File(path)
78 | outputFile.writeText(text, Charsets.UTF_8)
79 |
80 | this.cache = outputFile.toUri().path
81 | } catch (_: Exception) {
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/models/FolderModel.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data.models
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import java.time.Instant
7 | import java.util.Date
8 | import java.util.UUID
9 |
10 | @Entity(tableName = "folder_table")
11 | data class FolderModel(
12 | @PrimaryKey
13 | val id: UUID = UUID.randomUUID(),
14 | @ColumnInfo
15 | val name: String,
16 | @ColumnInfo
17 | val date: Date = Date.from(Instant.now()),
18 | @ColumnInfo
19 | val parent: UUID? = null,
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/models/WhatsNewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data.models
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "whats_new")
8 | data class WhatsNewModel(
9 | @PrimaryKey
10 | val id: String,
11 | @ColumnInfo
12 | val features: List
13 | )
14 |
15 | data class NewFeature(
16 | val id: Int,
17 | val title: String,
18 | val body: String,
19 | )
20 |
21 | val latestUpdate = WhatsNewModel(
22 | id = "1.1.1",
23 | features = listOf(
24 | NewFeature(
25 | id = 0,
26 | title = "Scan page 📖",
27 | body = "Scan the pages of your favourite books with your camera and listen to them"
28 | ),
29 | )
30 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/source/FileDao.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data.source
2 |
3 | import com.ifeanyi.read.app.data.models.FileModel
4 | import androidx.room.Dao
5 | import androidx.room.Delete
6 | import androidx.room.Insert
7 | import androidx.room.OnConflictStrategy
8 | import androidx.room.Query
9 | import androidx.room.Update
10 | import kotlinx.coroutines.flow.Flow
11 | import java.util.UUID
12 |
13 | @Dao
14 | interface FileDao {
15 | @Query(value = "SELECT * FROM file_table WHERE parent IS null")
16 | fun getAllFiles() : Flow>
17 |
18 | @Query(value = "SELECT * FROM file_table WHERE parent IS :id")
19 | fun getFolderFiles(id: UUID) : Flow>
20 |
21 | @Query(value = "SELECT COUNT(*) FROM file_table WHERE parent IS :id")
22 | fun getFolderFilesCount(id: UUID) : Flow
23 |
24 | @Insert(entity = FileModel::class, onConflict = OnConflictStrategy.REPLACE)
25 | suspend fun insert(item: FileModel)
26 |
27 | @Update(entity = FileModel::class, onConflict = OnConflictStrategy.REPLACE)
28 | suspend fun update(item: FileModel)
29 |
30 | @Delete(entity = FileModel::class)
31 | suspend fun delete(item: FileModel)
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/source/FolderDao.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data.source
2 |
3 | import com.ifeanyi.read.app.data.models.FolderModel
4 | import androidx.room.Dao
5 | import androidx.room.Delete
6 | import androidx.room.Insert
7 | import androidx.room.OnConflictStrategy
8 | import androidx.room.Query
9 | import androidx.room.Update
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @Dao
13 | interface FolderDao {
14 | @Query(value = "SELECT * FROM folder_table")
15 | fun getAllFolders() : Flow>
16 |
17 | @Insert(entity = FolderModel::class, onConflict = OnConflictStrategy.REPLACE)
18 | suspend fun insert(item: FolderModel)
19 |
20 | @Update(entity = FolderModel::class, onConflict = OnConflictStrategy.REPLACE)
21 | suspend fun update(item: FolderModel)
22 |
23 | @Delete(entity = FolderModel::class)
24 | suspend fun delete(item: FolderModel)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/data/source/WhatsNewDao.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.data.source
2 |
3 | import com.ifeanyi.read.app.data.models.WhatsNewModel
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface WhatsNewDao {
12 | @Query(value = "SELECT * FROM whats_new")
13 | fun getAllUpdates() : Flow>
14 |
15 | @Query(value = "SELECT * FROM whats_new WHERE id IS :id")
16 | suspend fun getUpdate(id: String) : WhatsNewModel?
17 |
18 | @Insert(entity = WhatsNewModel::class, onConflict = OnConflictStrategy.REPLACE)
19 | suspend fun insert(item: WhatsNewModel)
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/AppButton.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.material3.ElevatedButton
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.Dp
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun AppButton(
16 | modifier: Modifier = Modifier,
17 | text: String,
18 | enabled: Boolean = true,
19 | height: Dp = 60.dp,
20 | onClick: () -> Unit
21 | ) {
22 | ElevatedButton(
23 | modifier = modifier
24 | .fillMaxWidth()
25 | .height(height),
26 | shape = MaterialTheme.shapes.small,
27 | onClick = onClick,
28 | enabled = enabled,
29 | ) {
30 | Text(text = text, fontWeight = FontWeight.SemiBold)
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/BottomNavigationBarComponent.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import com.ifeanyi.read.core.route.bottomNavItems
4 | import com.ifeanyi.read.core.route.parentRoute
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.derivedStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.navigation.NavDestination
10 | import androidx.navigation.NavGraph.Companion.findStartDestination
11 | import androidx.navigation.NavHostController
12 |
13 | @Composable
14 | fun BottomNavigationBarComponent(controller: NavHostController, currentDestination: NavDestination?) {
15 | NavigationBar {
16 |
17 | val parentRoute = remember(controller.currentBackStackEntry) {
18 | derivedStateOf {
19 | controller.parentRoute
20 | }
21 | }
22 |
23 | bottomNavItems.forEach { screen ->
24 | val selected = parentRoute.value.name == screen.route
25 | NavigationBarItem(
26 | selected = selected,
27 | icon = if (selected) screen.icon else screen.inactiveIcon,
28 | label = { Text(text = screen.label) },
29 | onClick = {
30 | if (selected && currentDestination?.route != parentRoute.value.name) {
31 | controller.popBackStack()
32 | }else {
33 | controller.navigate(screen.route) {
34 | popUpTo(controller.graph.findStartDestination().id) {
35 | saveState = true
36 | }
37 | launchSingleTop = true
38 | restoreState = true
39 | }
40 | }
41 | },
42 | )
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/CustomSliderSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.animation.core.spring
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.gestures.detectHorizontalDragGestures
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.fillMaxHeight
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.material3.ExperimentalMaterial3Api
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.ModalBottomSheet
18 | import androidx.compose.material3.SheetState
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.MutableState
22 | import androidx.compose.runtime.mutableFloatStateOf
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.input.pointer.pointerInput
28 | import androidx.compose.ui.layout.onGloballyPositioned
29 | import androidx.compose.ui.unit.IntSize
30 | import androidx.compose.ui.unit.dp
31 | import kotlin.math.max
32 | import kotlin.math.min
33 |
34 | @OptIn(ExperimentalMaterial3Api::class)
35 | @Composable
36 | fun CustomSliderSheet(
37 | showRateSheet: MutableState,
38 | modalSheetState: SheetState,
39 | initialProgress: Float,
40 | onDone: (result: Float) -> Unit
41 | ) {
42 | val progress = remember { mutableFloatStateOf(initialProgress) }
43 | val position = animateFloatAsState(
44 | targetValue = progress.floatValue,
45 | animationSpec = spring(),
46 | label = ""
47 | )
48 | val size = remember { mutableStateOf(IntSize.Zero) }
49 |
50 | ModalBottomSheet(
51 | onDismissRequest = { showRateSheet.value = false },
52 | sheetState = modalSheetState
53 | ) {
54 | Column(
55 | modifier = Modifier
56 | .fillMaxWidth()
57 | .fillMaxHeight(0.5f)
58 | .padding(20.dp), verticalArrangement = Arrangement.SpaceBetween
59 | ) {
60 |
61 | Text(text = "Speech Rate", style = MaterialTheme.typography.titleLarge)
62 |
63 | Column(verticalArrangement = Arrangement.spacedBy(15.dp)) {
64 | Row(
65 | modifier = Modifier.fillMaxWidth(),
66 | horizontalArrangement = Arrangement.SpaceBetween
67 | ) {
68 | Text(text = "slow")
69 | Text(text = "normal")
70 | Text(text = "fast")
71 | }
72 |
73 | Box(modifier = Modifier
74 | .fillMaxWidth()
75 | .onGloballyPositioned {
76 | size.value = it.size
77 | }
78 | .clip(shape = MaterialTheme.shapes.small)
79 | .pointerInput(Unit) {
80 | detectHorizontalDragGestures(
81 | onHorizontalDrag = { change, _ ->
82 | val pos = min(change.position.x, size.value.width.toFloat()) / size.value.width
83 | progress.floatValue = max(0.05f, pos)
84 | },
85 | onDragEnd = {
86 | onDone(progress.floatValue * 2)
87 | }
88 | )
89 | }
90 | ) {
91 | Box(
92 | modifier = Modifier
93 | .height(50.dp)
94 | .fillMaxWidth()
95 | .background(color = MaterialTheme.colorScheme.inversePrimary)
96 | )
97 |
98 | Box(
99 | modifier = Modifier
100 | .height(50.dp)
101 | .fillMaxWidth(position.value)
102 | .background(color = MaterialTheme.colorScheme.primary)
103 | )
104 | }
105 | }
106 |
107 | Box(modifier = Modifier.height(60.dp))
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/GridTileComponent.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import com.ifeanyi.read.core.theme.AppIcons
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.combinedClickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Surface
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.clip
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.unit.Dp
22 | import androidx.compose.ui.unit.dp
23 |
24 | @OptIn(ExperimentalFoundationApi::class)
25 | @Composable
26 | fun GridTileComponent(
27 | modifier: Modifier = Modifier,
28 | asset: @Composable (() -> Unit?)? = null,
29 | title: String? = null,
30 | subtitle: String,
31 | tonalElevation: Dp = 1.dp,
32 | onClick: () -> Unit,
33 | onLongPress: (() -> Unit)? = null,
34 | ) {
35 | Surface(
36 | modifier = modifier
37 | .clip(shape = MaterialTheme.shapes.small)
38 | .combinedClickable(
39 | onClick = { onClick.invoke() },
40 | onLongClick = { onLongPress?.invoke() }
41 | ),
42 | shape = MaterialTheme.shapes.small,
43 | tonalElevation = tonalElevation,
44 | ) {
45 | Column(
46 | modifier = Modifier.padding(horizontal = 10.dp, vertical = 15.dp).fillMaxWidth(),
47 | horizontalAlignment = Alignment.CenterHorizontally,
48 | verticalArrangement = Arrangement.spacedBy(5.dp)
49 | ) {
50 | if (asset?.invoke() == null) Icon(
51 | imageVector = AppIcons.flag,
52 | contentDescription = "Flag",
53 | modifier = Modifier.size(30.dp)
54 | )
55 | if (title != null) Text(
56 | text = title, maxLines = 1,
57 | textAlign = TextAlign.Center,
58 | style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold)
59 | )
60 | Text(
61 | text = subtitle,
62 | maxLines = 2,
63 | textAlign = TextAlign.Center,
64 | style = MaterialTheme.typography.bodySmall
65 | )
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/ListTileComponent.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.combinedClickable
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.material3.MaterialTheme
11 | import androidx.compose.material3.Surface
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.clip
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.unit.dp
19 |
20 | @OptIn(ExperimentalFoundationApi::class)
21 | @Composable
22 | fun ListTileComponent(
23 | modifier: Modifier = Modifier,
24 | asset: @Composable (() -> Unit?)? = null,
25 | title: String,
26 | subtitle: String,
27 | onClick: () -> Unit,
28 | onLongPress: (() -> Unit)? = null,
29 | ) {
30 | Surface(
31 | modifier = modifier
32 | .clip(shape = MaterialTheme.shapes.small)
33 | .combinedClickable(
34 | onClick = { onClick.invoke() },
35 | onLongClick = { onLongPress?.invoke() }
36 | ),
37 | shape = MaterialTheme.shapes.small,
38 | tonalElevation = 1.dp,
39 | ) {
40 | Row(
41 | modifier = Modifier.padding(horizontal = 10.dp, vertical = 15.dp),
42 | horizontalArrangement = Arrangement.spacedBy(15.dp),
43 | verticalAlignment = Alignment.CenterVertically
44 | ) {
45 | asset?.invoke()
46 | Column(
47 | modifier = Modifier.fillMaxWidth(),
48 | verticalArrangement = Arrangement.spacedBy(5.dp)
49 | ) {
50 | Text(text = title, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold))
51 | Text(text = subtitle, style = MaterialTheme.typography.bodySmall)
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/LoaderComponent.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material3.AlertDialog
7 | import androidx.compose.material3.CircularProgressIndicator
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 |
12 | @Composable
13 | fun LoaderComponent(text: String) {
14 | AlertDialog(onDismissRequest = {}, confirmButton = {}, title = {
15 | Row(
16 | Modifier.fillMaxWidth(),
17 | horizontalArrangement = Arrangement.Center
18 | ) {
19 | CircularProgressIndicator()
20 | }
21 | }, text = {
22 | Row(
23 | Modifier.fillMaxWidth(),
24 | horizontalArrangement = Arrangement.Center
25 | ) {
26 | Text(text = text)
27 | }
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/PlayerComponent.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import android.annotation.SuppressLint
4 | import com.ifeanyi.read.core.services.SpeechService
5 | import com.ifeanyi.read.core.theme.AppIcons
6 | import androidx.compose.animation.AnimatedVisibility
7 | import androidx.compose.animation.slideInVertically
8 | import androidx.compose.animation.slideOutVertically
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.PaddingValues
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.Spacer
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.layout.size
17 | import androidx.compose.foundation.shape.RoundedCornerShape
18 | import androidx.compose.material3.ElevatedButton
19 | import androidx.compose.material3.Icon
20 | import androidx.compose.material3.IconButton
21 | import androidx.compose.material3.LinearProgressIndicator
22 | import androidx.compose.material3.MaterialTheme
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.collectAsState
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.text.style.TextOverflow
30 | import androidx.compose.ui.unit.dp
31 |
32 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
33 | @Composable
34 | fun PlayerComponent(expanded: Boolean, onClick: () -> Unit) {
35 | val state = SpeechService.state.collectAsState().value
36 |
37 | AnimatedVisibility(
38 | visible = state.canPlay,
39 | enter = slideInVertically { it },
40 | exit = slideOutVertically { it },
41 | ) {
42 | ElevatedButton(
43 | modifier = Modifier.padding(8.dp),
44 | shape = RoundedCornerShape(5.dp),
45 | contentPadding = PaddingValues(8.dp),
46 | onClick = { onClick.invoke() }
47 | ) {
48 | Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
49 | Row(
50 | modifier = Modifier.fillMaxWidth(),
51 | verticalAlignment = Alignment.CenterVertically,
52 | horizontalArrangement = Arrangement.spacedBy(15.dp)
53 | ) {
54 | WaveForm(animating = !expanded && state.isPlaying)
55 |
56 | if (state.model != null) {
57 | Text(
58 | text = state.model.name,
59 | fontWeight = FontWeight.SemiBold,
60 | maxLines = 1,
61 | overflow = TextOverflow.Ellipsis,
62 | modifier = Modifier.weight(1f)
63 | )
64 | } else Spacer(modifier = Modifier.weight(1f))
65 |
66 | IconButton(
67 | modifier = Modifier.size(35.dp),
68 | onClick = { SpeechService.stop(true) },
69 | ) {
70 | Icon(
71 | modifier = Modifier.size(35.dp),
72 | imageVector = AppIcons.stop,
73 | contentDescription = "Stop Button",
74 | tint = MaterialTheme.colorScheme.primary
75 | )
76 | }
77 |
78 | IconButton(
79 | modifier = Modifier.size(40.dp),
80 | onClick = {
81 | if (state.isPlaying) SpeechService.pause() else SpeechService.play()
82 | },
83 | ) {
84 | Icon(
85 | modifier = Modifier.size(40.dp),
86 | imageVector = if (state.isPlaying) AppIcons.pause else AppIcons.play,
87 | contentDescription = "Play/Pause Button",
88 | tint = MaterialTheme.colorScheme.primary
89 | )
90 | }
91 | }
92 | LinearProgressIndicator(
93 | progress = { state.progress },
94 | modifier = Modifier.fillMaxWidth(),
95 | )
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/SettingsItem.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material3.ButtonDefaults
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TextButton
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.vector.ImageVector
21 | import androidx.compose.ui.unit.dp
22 |
23 | @Composable
24 | fun SettingsItem(
25 | title: String,
26 | icon: ImageVector,
27 | color: Color,
28 | trailing: (@Composable () -> Unit)? = null,
29 | onClick: () -> Unit,
30 | ) {
31 | TextButton(
32 | shape = MaterialTheme.shapes.small,
33 | contentPadding = PaddingValues(0.dp),
34 | colors = ButtonDefaults.textButtonColors(
35 | contentColor = MaterialTheme.colorScheme.onSurface,
36 | ),
37 | onClick = { onClick.invoke() }) {
38 | Row(
39 | modifier = Modifier
40 | .fillMaxWidth()
41 | .padding(vertical = 6.dp),
42 | horizontalArrangement = Arrangement.spacedBy(15.dp),
43 | verticalAlignment = Alignment.CenterVertically
44 | ) {
45 | Box(modifier = Modifier.background(color = color, shape = MaterialTheme.shapes.small)) {
46 | Icon(
47 | imageVector = icon,
48 | contentDescription = "Settings Item - $title",
49 | modifier = Modifier.padding(8.dp),
50 | tint = Color.White
51 | )
52 | }
53 | Text(text = title, style = MaterialTheme.typography.bodyMedium)
54 |
55 | Spacer(modifier = Modifier.weight(1f))
56 |
57 | if (trailing?.invoke() == null) Box {
58 |
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/TextFieldComponent.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.text.KeyboardActions
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.TextField
8 | import androidx.compose.material3.TextFieldDefaults
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.MutableState
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.focus.FocusRequester
14 | import androidx.compose.ui.focus.focusRequester
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController
17 | import androidx.compose.ui.text.input.ImeAction
18 |
19 | @Composable
20 | fun TextFieldComponent(
21 | value: MutableState,
22 | modifier: Modifier = Modifier,
23 | onValueChange: ((String) -> Unit)? = null,
24 | supportingText: @Composable (() -> Unit)? = null,
25 | onImeAction: (() -> Unit)? = null,
26 | label: @Composable (() -> Unit)? = null,
27 | placeholder: @Composable (() -> Unit)? = null,
28 | leadingIcon: @Composable (() -> Unit)? = null,
29 | trailingIcon: @Composable (() -> Unit)? = null,
30 | maxLines: Int = 1,
31 | textLimit: Int? = null,
32 | keyboardOptions: KeyboardOptions? = null,
33 | keyboardActions: KeyboardActions? = null,
34 | ) {
35 | val keyboardController = LocalSoftwareKeyboardController.current
36 | val focus = remember { FocusRequester() }
37 |
38 | TextField(
39 | modifier = modifier
40 | .fillMaxWidth()
41 | .focusRequester(focus),
42 | value = value.value,
43 | textStyle = MaterialTheme.typography.bodyMedium,
44 | onValueChange = {
45 | value.value = it.take(textLimit ?: it.length)
46 | onValueChange?.invoke(it)
47 | },
48 | supportingText = supportingText,
49 | keyboardOptions = keyboardOptions ?: KeyboardOptions.Default.copy(
50 | imeAction = ImeAction.Search
51 | ),
52 | keyboardActions = keyboardActions ?: KeyboardActions(
53 | onSearch = {
54 | onImeAction?.invoke()
55 | keyboardController?.hide()
56 | },
57 | onGo = {
58 | onImeAction?.invoke()
59 | keyboardController?.hide()
60 | },
61 | onDone = {
62 | focus.freeFocus()
63 | keyboardController?.hide()
64 | }
65 | ),
66 | label = label,
67 | placeholder = placeholder,
68 | leadingIcon = leadingIcon,
69 | trailingIcon = trailingIcon,
70 | maxLines = maxLines,
71 | shape = MaterialTheme.shapes.medium,
72 | colors = TextFieldDefaults.colors(
73 | focusedIndicatorColor = Color.Transparent,
74 | unfocusedIndicatorColor = Color.Transparent,
75 | )
76 | )
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/VoiceSelectorSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import com.ifeanyi.read.core.services.SpeechService
4 | import com.ifeanyi.read.core.theme.AppIcons
5 | import com.ifeanyi.read.core.util.flagEmoji
6 | import android.speech.tts.Voice
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.lazy.grid.GridCells
12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
13 | import androidx.compose.foundation.lazy.grid.items
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.ModalBottomSheet
18 | import androidx.compose.material3.SheetState
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.MutableState
22 | import androidx.compose.runtime.collectAsState
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.unit.dp
26 |
27 | @OptIn(ExperimentalMaterial3Api::class)
28 | @Composable
29 | fun VoiceSelectorSheet(
30 | showVoicesSheet: MutableState,
31 | modalSheetState: SheetState,
32 | initial: Voice?,
33 | onDone: (voice: Voice) -> Unit
34 | ) {
35 | val state = SpeechService.state.collectAsState().value
36 |
37 | ModalBottomSheet(
38 | onDismissRequest = { showVoicesSheet.value = false },
39 | sheetState = modalSheetState
40 | ) {
41 | Column(
42 | modifier = Modifier.padding(20.dp),
43 | verticalArrangement = Arrangement.spacedBy(15.dp)
44 | ) {
45 | Text(text = "Select Voice", style = MaterialTheme.typography.titleLarge)
46 |
47 | LazyVerticalGrid(
48 | columns = GridCells.Fixed(3),
49 | verticalArrangement = Arrangement.spacedBy(10.dp),
50 | horizontalArrangement = Arrangement.spacedBy(10.dp),
51 | ) {
52 | items(state.voices) { voice ->
53 | Box {
54 | GridTileComponent(
55 | asset = {
56 | if (voice.locale.flagEmoji != null) Text(
57 | text = voice.locale.flagEmoji!!,
58 | style = MaterialTheme.typography.titleMedium
59 | ) else null
60 | },
61 | tonalElevation = 2.dp,
62 | subtitle = voice.locale.displayName,
63 | onClick = { onDone.invoke(voice) },
64 | )
65 | if (voice.name == initial?.name) {
66 | Icon(
67 | imageVector = AppIcons.checkbox,
68 | contentDescription = "",
69 | modifier = Modifier.align(
70 | Alignment.TopEnd
71 | )
72 | )
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/components/WaveForm.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.components
2 |
3 | import androidx.compose.animation.core.RepeatMode
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.animation.core.infiniteRepeatable
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.width
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.unit.dp
18 | import kotlin.random.Random
19 |
20 | @Composable
21 | fun WaveForm(animating: Boolean) {
22 | val random = Random(1)
23 |
24 | Row(modifier = Modifier.height(50.dp), verticalAlignment = Alignment.CenterVertically) {
25 | for (i in 1.. 6) {
26 | val animation = animateFloatAsState(
27 | targetValue = if (animating) 1.0f else 0.1f,
28 | animationSpec = if (animating) infiniteRepeatable(animation = tween(durationMillis = random.nextInt(400, 1000)), repeatMode = RepeatMode.Reverse) else tween(durationMillis = 300),
29 | label = "Height Animation"
30 | )
31 | val height = if (i < 3 || i > 4) random.nextInt(10, 25) else random.nextInt(25, 50)
32 | Box(
33 | modifier = Modifier
34 | .padding(horizontal = 2.dp)
35 | .background(color = MaterialTheme.colorScheme.primary, shape = MaterialTheme.shapes.small)
36 | .height((height * animation.value).dp)
37 | .width(4.dp)
38 |
39 | )
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/viewmodel/LibraryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.viewmodel
2 |
3 | import com.ifeanyi.read.app.data.LibraryRepository
4 | import com.ifeanyi.read.app.data.models.FileModel
5 | import com.ifeanyi.read.app.data.models.FolderModel
6 | import com.ifeanyi.read.app.presentation.views.library.SortType
7 | import android.net.Uri
8 | import androidx.compose.runtime.MutableIntState
9 | import androidx.compose.runtime.mutableIntStateOf
10 | import androidx.lifecycle.ViewModel
11 | import androidx.lifecycle.viewModelScope
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.delay
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.asStateFlow
16 | import kotlinx.coroutines.flow.distinctUntilChanged
17 | import kotlinx.coroutines.flow.update
18 | import kotlinx.coroutines.launch
19 | import java.io.File
20 | import java.util.UUID
21 | import javax.inject.Inject
22 |
23 | data class LibraryState(
24 | val files: List = emptyList(),
25 | val searchedFiles: List = emptyList(),
26 | val folders: List = emptyList(),
27 | val searchedFolders: List = emptyList(),
28 | val folderFiles: List = emptyList(),
29 | )
30 |
31 | @HiltViewModel
32 | class LibraryViewModel @Inject constructor(private val libraryRepository: LibraryRepository) :
33 | ViewModel() {
34 | private val _state = MutableStateFlow(LibraryState())
35 | val state = _state.asStateFlow()
36 |
37 | init {
38 | getAllFiles()
39 | getAllFolders()
40 | }
41 |
42 | private fun getAllFiles() = viewModelScope.launch {
43 | libraryRepository.getAllFiles().distinctUntilChanged().collect { items ->
44 | _state.update { it.copy(files = items) }
45 | }
46 | }
47 |
48 | fun insertItem(file: FileModel) = viewModelScope.launch {
49 | libraryRepository.insertItem(file)
50 | }
51 |
52 | fun updateItem(file: FileModel) = viewModelScope.launch {
53 | libraryRepository.updateItem(file)
54 | }
55 |
56 | fun deleteItem(file: FileModel) = viewModelScope.launch {
57 | val fileUri = Uri.parse(file.path)
58 | if (File(fileUri.path ?: "").exists()) {
59 | File(fileUri.path ?: "").delete()
60 | }
61 |
62 | if (file.cache != null) {
63 | File(file.cache ?: "").delete()
64 | }
65 | libraryRepository.deleteItem(file)
66 | }
67 |
68 | private fun getAllFolders() = viewModelScope.launch {
69 | libraryRepository.getAllFolders().distinctUntilChanged().collect { items ->
70 | _state.update { it.copy(folders = items) }
71 | }
72 | }
73 |
74 | fun insertItem(folder: FolderModel) = viewModelScope.launch {
75 | libraryRepository.insertItem(folder)
76 | }
77 |
78 | fun updateItem(folder: FolderModel) = viewModelScope.launch {
79 | libraryRepository.updateItem(folder)
80 | }
81 |
82 | fun deleteItem(folder: FolderModel) = viewModelScope.launch {
83 | libraryRepository.deleteItem(folder)
84 |
85 | getFolderFiles(folder.id)
86 | delay(500)
87 |
88 | if (_state.value.folderFiles.isNotEmpty()) {
89 | for (file in _state.value.folderFiles) {
90 | deleteItem(file)
91 | }
92 | }
93 | }
94 |
95 | fun moveToFolder(id: UUID, files: List) = viewModelScope.launch {
96 | for (file in files) {
97 | libraryRepository.updateItem(file.copy(parent = id))
98 | }
99 | }
100 |
101 | fun getFolderFiles(id: UUID) = viewModelScope.launch {
102 | libraryRepository.getFolderFiles(id).distinctUntilChanged().collect { items ->
103 | _state.update { it.copy(folderFiles = items) }
104 | }
105 | }
106 |
107 | fun getFolderFilesCount(id: UUID): MutableIntState {
108 | val count = mutableIntStateOf(0)
109 | viewModelScope.launch {
110 | libraryRepository.getFolderFilesCount(id).distinctUntilChanged().collect { item ->
111 | count.intValue = item
112 | }
113 | }
114 | return count
115 | }
116 |
117 | fun search(query: String) = viewModelScope.launch {
118 | val value = _state.value
119 | _state.update {
120 | it.copy(
121 | searchedFiles = value.files.filter { file ->
122 | file.name.lowercase().contains(query.lowercase())
123 | },
124 | searchedFolders = value.folders.filter { folder ->
125 | folder.name.lowercase().contains(query.lowercase())
126 | }
127 | )
128 | }
129 | }
130 |
131 | fun sort(type: SortType) = viewModelScope.launch {
132 | val value = _state.value
133 | _state.update {
134 | it.copy(
135 | files = value.files.sortedBy { file ->
136 | when (type) {
137 | SortType.Date -> file.date
138 | SortType.Name -> file.name
139 | }.toString()
140 | },
141 | folders = value.folders.sortedBy { folder ->
142 | when (type) {
143 | SortType.Date -> folder.date
144 | SortType.Name -> folder.name
145 | }.toString()
146 | },
147 | )
148 | }
149 | }
150 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/viewmodel/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.viewmodel
2 |
3 | import com.ifeanyi.read.app.data.SettingsRepository
4 | import com.ifeanyi.read.app.data.models.WhatsNewModel
5 | import com.ifeanyi.read.app.data.models.latestUpdate
6 | import com.ifeanyi.read.core.enums.AppTheme
7 | import com.ifeanyi.read.core.enums.DisplayStyle
8 | import com.ifeanyi.read.core.services.PreferenceService
9 | import com.ifeanyi.read.core.util.Constants
10 | import android.speech.tts.Voice
11 | import androidx.compose.runtime.MutableState
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.lifecycle.ViewModel
14 | import androidx.lifecycle.viewModelScope
15 | import dagger.hilt.android.lifecycle.HiltViewModel
16 | import kotlinx.coroutines.flow.MutableStateFlow
17 | import kotlinx.coroutines.flow.asStateFlow
18 | import kotlinx.coroutines.flow.distinctUntilChanged
19 | import kotlinx.coroutines.flow.update
20 | import kotlinx.coroutines.launch
21 | import javax.inject.Inject
22 |
23 | data class SettingsState(
24 | val theme: AppTheme = AppTheme.System,
25 | val displayStyle: DisplayStyle = DisplayStyle.Grid,
26 | val speechRate: Float = 1.0f,
27 | val voice: Voice? = null,
28 | val whatsNew: List = emptyList(),
29 | val showWhatsNew: MutableState = mutableStateOf(false)
30 | )
31 |
32 | @HiltViewModel
33 | class SettingsViewModel @Inject constructor(
34 | private val settingsRepository: SettingsRepository,
35 | private val preferenceService: PreferenceService
36 | ) : ViewModel() {
37 | private val _state = MutableStateFlow(SettingsState())
38 | val state = _state.asStateFlow()
39 |
40 | init {
41 | getTheme()
42 | checkWhatsNew()
43 | getAllUpdates()
44 | getDisplayStyle()
45 | getSpeechRate()
46 | getVoice()
47 | }
48 |
49 | private fun getTheme() = viewModelScope.launch {
50 | preferenceService.getTheme().distinctUntilChanged().collect { theme ->
51 | _state.update { it.copy(theme = theme) }
52 | }
53 | }
54 |
55 | fun setTheme(theme: AppTheme) = viewModelScope.launch {
56 | preferenceService.saveString(key = Constants.theme, value = theme.name)
57 | }
58 |
59 | private fun checkWhatsNew() = viewModelScope.launch {
60 | val latest = settingsRepository.getUpdate(latestUpdate.id)
61 | if (latest == null) {
62 | settingsRepository.insertItem(latestUpdate)
63 | _state.update { it.copy(showWhatsNew = mutableStateOf(true)) }
64 | }
65 | }
66 |
67 | private fun getAllUpdates() = viewModelScope.launch {
68 | settingsRepository.getAllUpdates().distinctUntilChanged().collect { whatsNew ->
69 | _state.update { it.copy(whatsNew = whatsNew.reversed()) }
70 | }
71 | }
72 |
73 | private fun getDisplayStyle() = viewModelScope.launch {
74 | preferenceService.getDisplayStyle().distinctUntilChanged().collect { style ->
75 | _state.update { it.copy(displayStyle = style) }
76 | }
77 | }
78 |
79 | fun setDisplayStyle(style: DisplayStyle) = viewModelScope.launch {
80 | preferenceService.saveString(key = Constants.displayStyle, value = style.name)
81 | }
82 |
83 | private fun getSpeechRate() = viewModelScope.launch {
84 | preferenceService.getString(Constants.speechRate).distinctUntilChanged().collect { rate ->
85 | val speechRate = (rate ?: "1.0").toDouble()
86 | _state.update { it.copy(speechRate = String.format("%.1f", speechRate).toFloat()) }
87 | }
88 | }
89 |
90 | fun setSpeechRate(rate: Float) = viewModelScope.launch {
91 | preferenceService.saveString(key = Constants.speechRate, value = "$rate")
92 | }
93 |
94 | private fun getVoice() = viewModelScope.launch {
95 | preferenceService.getVoice().distinctUntilChanged().collect { voice ->
96 | _state.update { it.copy(voice = voice) }
97 | }
98 | }
99 |
100 | fun setVoice(voice: Voice) = viewModelScope.launch {
101 | preferenceService.saveString(
102 | key = Constants.voice,
103 | value = "${voice.name}/${voice.locale.language}/${voice.locale.country}"
104 | )
105 | }
106 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/home/EnterTextScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.home
2 |
3 | import android.annotation.SuppressLint
4 | import com.ifeanyi.read.app.data.models.FileModel
5 | import com.ifeanyi.read.app.data.models.LibraryType
6 | import com.ifeanyi.read.app.presentation.components.AppButton
7 | import com.ifeanyi.read.app.presentation.components.TextFieldComponent
8 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
9 | import com.ifeanyi.read.core.services.AnalyticService
10 | import com.ifeanyi.read.core.services.SpeechService
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.PaddingValues
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.height
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.text.KeyboardOptions
17 | import androidx.compose.material3.ExperimentalMaterial3Api
18 | import androidx.compose.material3.MaterialTheme
19 | import androidx.compose.material3.Scaffold
20 | import androidx.compose.material3.Text
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.platform.LocalConfiguration
27 | import androidx.compose.ui.platform.LocalContext
28 | import androidx.compose.ui.text.input.ImeAction
29 | import androidx.compose.ui.text.style.TextAlign
30 | import androidx.compose.ui.unit.dp
31 | import androidx.core.net.toUri
32 | import androidx.hilt.navigation.compose.hiltViewModel
33 | import androidx.navigation.NavHostController
34 | import java.io.File
35 | import java.time.Instant
36 |
37 | @OptIn(ExperimentalMaterial3Api::class)
38 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
39 | @Composable
40 | fun EnterTextScreen(
41 | controller: NavHostController,
42 | libraryVM: LibraryViewModel = hiltViewModel(),
43 | ) {
44 | val context = LocalContext.current
45 | val config = LocalConfiguration.current
46 | val textLimit = 4000
47 | val text = remember { mutableStateOf("") }
48 |
49 | Scaffold(
50 | topBar = {
51 | TopAppBar(
52 | title = { Text(text = "Enter Text", style = MaterialTheme.typography.titleLarge) },
53 | )
54 | }
55 | ) { padding ->
56 | LazyColumn(
57 | contentPadding = PaddingValues(
58 | top = padding.calculateTopPadding(),
59 | start = 20.dp, end = 20.dp,
60 | bottom = 200.dp
61 | ),
62 | verticalArrangement = Arrangement.spacedBy(15.dp)
63 | ) {
64 | item {
65 | TextFieldComponent(
66 | value = text,
67 | label = { Text("Paste or write something...") },
68 | modifier = Modifier.height((config.screenHeightDp * 0.5).dp),
69 | maxLines = 30,
70 | textLimit = textLimit,
71 | supportingText = {
72 | Text(
73 | text = "${text.value.length}/$textLimit",
74 | modifier = Modifier.fillMaxWidth(),
75 | textAlign = TextAlign.End,
76 | )
77 | },
78 | keyboardOptions = KeyboardOptions.Default.copy(
79 | imeAction = ImeAction.Done
80 | ),
81 | )
82 | }
83 | item {
84 | AppButton(text = "Continue", enabled = text.value.isNotEmpty()) {
85 | AnalyticService.track("enter_text")
86 | val path = "${context.filesDir.path}/${Instant.now()}.txt"
87 | val outputFile = File(path)
88 | outputFile.writeText(text.value, Charsets.UTF_8)
89 |
90 | val newUri = outputFile.toUri()
91 | val model = FileModel(
92 | name = outputFile.name,
93 | type = LibraryType.Txt,
94 | path = newUri.toString(),
95 | )
96 | SpeechService.updateModel(model) {
97 | libraryVM.insertItem(it)
98 | }
99 | controller.popBackStack()
100 | }
101 | }
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/home/EnterUrlSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.home
2 |
3 | import com.ifeanyi.read.app.data.models.FileModel
4 | import com.ifeanyi.read.app.data.models.LibraryType
5 | import com.ifeanyi.read.app.presentation.components.AppButton
6 | import com.ifeanyi.read.app.presentation.components.TextFieldComponent
7 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
8 | import com.ifeanyi.read.core.services.AnalyticService
9 | import com.ifeanyi.read.core.services.SpeechService
10 | import com.ifeanyi.read.core.util.trimUrl
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.fillMaxHeight
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.material3.ExperimentalMaterial3Api
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.ModalBottomSheet
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.rememberModalBottomSheetState
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.MutableState
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.rememberCoroutineScope
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.platform.LocalFocusManager
27 | import androidx.compose.ui.unit.dp
28 | import kotlinx.coroutines.launch
29 |
30 | @Composable
31 | @OptIn(ExperimentalMaterial3Api::class)
32 | fun EnterUrlSheet(
33 | showUrlSheet: MutableState,
34 | libraryVM: LibraryViewModel,
35 | ) {
36 | val focusManager = LocalFocusManager.current
37 |
38 | val url = remember { mutableStateOf("") }
39 | val coroutineScope = rememberCoroutineScope()
40 | val modalSheetState = rememberModalBottomSheetState()
41 |
42 | fun onContinue() {
43 | focusManager.clearFocus()
44 | val model = FileModel(
45 | name = url.value.trimUrl,
46 | type = LibraryType.Url,
47 | path = url.value,
48 | )
49 | AnalyticService.track("enter_url")
50 | coroutineScope.launch {
51 | SpeechService.updateModel(model) {
52 | libraryVM.insertItem(it)
53 | }
54 | modalSheetState.hide()
55 | }.invokeOnCompletion {
56 | showUrlSheet.value = false
57 | }
58 | }
59 |
60 | ModalBottomSheet(
61 | onDismissRequest = { showUrlSheet.value = false },
62 | sheetState = modalSheetState
63 | ) {
64 | Column(
65 | modifier = Modifier
66 | .fillMaxHeight(0.5f)
67 | .padding(20.dp),
68 | verticalArrangement = Arrangement.spacedBy(20.dp),
69 | ) {
70 | Text(text = "Enter link", style = MaterialTheme.typography.titleLarge)
71 |
72 | TextFieldComponent(
73 | value = url,
74 | label = { Text("Link") },
75 | onImeAction = { onContinue() }
76 | )
77 |
78 | AppButton(
79 | text = "Continue",
80 | enabled = url.value.isNotEmpty()
81 | ) { onContinue() }
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/CreateFolderSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 | import com.ifeanyi.read.app.data.models.FolderModel
4 | import com.ifeanyi.read.app.presentation.components.AppButton
5 | import com.ifeanyi.read.app.presentation.components.TextFieldComponent
6 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
7 | import com.ifeanyi.read.core.services.AnalyticService
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.fillMaxHeight
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.text.KeyboardActions
13 | import androidx.compose.foundation.text.KeyboardOptions
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.ModalBottomSheet
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.rememberModalBottomSheetState
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.MutableState
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.rememberCoroutineScope
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.text.input.ImeAction
26 | import androidx.compose.ui.unit.dp
27 | import kotlinx.coroutines.launch
28 |
29 | @Composable
30 | @OptIn(ExperimentalMaterial3Api::class)
31 | fun CreateFolderSheet(
32 | createFolder: MutableState,
33 | libraryVM: LibraryViewModel
34 | ) {
35 | val name = remember { mutableStateOf("") }
36 | val coroutineScope = rememberCoroutineScope()
37 | val modalSheetState = rememberModalBottomSheetState()
38 |
39 | fun onContinue() {
40 | val folder = FolderModel(name = name.value)
41 | AnalyticService.track("create_folder")
42 | coroutineScope.launch {
43 | libraryVM.insertItem(folder)
44 | modalSheetState.hide()
45 | }.invokeOnCompletion {
46 | createFolder.value = false
47 | }
48 | }
49 |
50 | ModalBottomSheet(
51 | onDismissRequest = { createFolder.value = false },
52 | sheetState = modalSheetState
53 | ) {
54 | Column(
55 | modifier = Modifier
56 | .fillMaxHeight(0.5f)
57 | .padding(20.dp),
58 | verticalArrangement = Arrangement.spacedBy(20.dp),
59 | ) {
60 | Text(text = "Create Folder", style = MaterialTheme.typography.titleLarge)
61 |
62 | TextFieldComponent(
63 | value = name,
64 | label = { Text("Name") },
65 | onImeAction = { onContinue() },
66 | keyboardOptions = KeyboardOptions.Default.copy(
67 | imeAction = ImeAction.Go
68 | ),
69 | keyboardActions = KeyboardActions(
70 | onGo = { onContinue() }
71 | )
72 | )
73 |
74 | AppButton(
75 | text = "Continue",
76 | enabled = name.value.isNotEmpty()
77 | ) { onContinue() }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/DefaultDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 | import com.ifeanyi.read.app.presentation.viewmodel.SettingsViewModel
4 | import com.ifeanyi.read.core.enums.DisplayStyle
5 | import com.ifeanyi.read.core.theme.AppIcons
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.wrapContentSize
11 | import androidx.compose.material3.DropdownMenu
12 | import androidx.compose.material3.DropdownMenuItem
13 | import androidx.compose.material3.HorizontalDivider
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.MutableState
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 |
22 | @Composable
23 | fun DefaultDialog(
24 | padding: PaddingValues,
25 | createFolder: MutableState,
26 | showMore: MutableState,
27 | showSort: MutableState,
28 | isSelecting: MutableState,
29 | settingsVM: SettingsViewModel
30 | ) {
31 | Box(
32 | modifier = Modifier
33 | .fillMaxWidth()
34 | .padding(
35 | top = padding.calculateTopPadding(),
36 | start = 20.dp, end = 20.dp,
37 | )
38 | .wrapContentSize(Alignment.TopEnd)
39 | ) {
40 | DropdownMenu(
41 | expanded = showMore.value,
42 | onDismissRequest = { showMore.value = !showMore.value },
43 | modifier = Modifier.fillMaxWidth(0.6f)
44 | ) {
45 | DropdownMenuItem(
46 | text = { Text(text = "New Folder") },
47 | onClick = {
48 | createFolder.value = true
49 | showMore.value = false
50 | },
51 | trailingIcon = {
52 | Icon(imageVector = AppIcons.newFolder, contentDescription = "")
53 | }
54 | )
55 | DropdownMenuItem(
56 | text = { Text(text = "Sort") },
57 | onClick = {
58 | showMore.value = false
59 | showSort.value = true
60 | },
61 | trailingIcon = {
62 | Icon(
63 | imageVector = AppIcons.sort,
64 | contentDescription = ""
65 | )
66 | }
67 | )
68 | DropdownMenuItem(
69 | text = { Text(text = "Select Multiple") },
70 | onClick = {
71 | showMore.value = false
72 | isSelecting.value = true
73 | },
74 | trailingIcon = {
75 | Icon(imageVector = AppIcons.checklist, contentDescription = "")
76 | }
77 | )
78 | HorizontalDivider()
79 | DropdownMenuItem(
80 | text = { Text(text = "List View") },
81 | onClick = {
82 | showMore.value = false
83 | settingsVM.setDisplayStyle(DisplayStyle.List)
84 | },
85 | trailingIcon = {
86 | Icon(
87 | imageVector = AppIcons.listView,
88 | contentDescription = ""
89 | )
90 | }
91 | )
92 | DropdownMenuItem(
93 | text = { Text(text = "Grid View") },
94 | onClick = {
95 | showMore.value = false
96 | settingsVM.setDisplayStyle(DisplayStyle.Grid)
97 | },
98 | trailingIcon = {
99 | Icon(imageVector = AppIcons.gridView, contentDescription = "")
100 | }
101 | )
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/MoveFilesSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 |
4 | import com.ifeanyi.read.app.data.models.FileModel
5 | import com.ifeanyi.read.app.data.models.FolderModel
6 | import com.ifeanyi.read.app.presentation.components.GridTileComponent
7 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
8 | import com.ifeanyi.read.core.services.AnalyticService
9 | import com.ifeanyi.read.core.theme.AppIcons
10 | import com.ifeanyi.read.core.util.dwdm
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.PaddingValues
15 | import androidx.compose.foundation.layout.fillMaxHeight
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.size
18 | import androidx.compose.foundation.lazy.grid.GridCells
19 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
20 | import androidx.compose.foundation.lazy.grid.items
21 | import androidx.compose.material3.ExperimentalMaterial3Api
22 | import androidx.compose.material3.Icon
23 | import androidx.compose.material3.MaterialTheme
24 | import androidx.compose.material3.ModalBottomSheet
25 | import androidx.compose.material3.Text
26 | import androidx.compose.material3.rememberModalBottomSheetState
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.MutableState
29 | import androidx.compose.runtime.collectAsState
30 | import androidx.compose.runtime.rememberCoroutineScope
31 | import androidx.compose.runtime.snapshots.SnapshotStateList
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.unit.dp
34 | import kotlinx.coroutines.launch
35 | import java.util.Locale
36 |
37 | @Composable
38 | @OptIn(ExperimentalMaterial3Api::class)
39 | fun MoveFilesSheet(
40 | moveFiles: MutableState,
41 | selectedFiles: SnapshotStateList,
42 | libraryVM: LibraryViewModel,
43 | onDone: () -> Unit,
44 | ) {
45 | val state = libraryVM.state.collectAsState().value
46 |
47 | val locale = Locale.getDefault()
48 | val coroutineScope = rememberCoroutineScope()
49 | val modalSheetState = rememberModalBottomSheetState()
50 |
51 | fun onSelect(folder: FolderModel) {
52 | coroutineScope.launch {
53 | AnalyticService.track("move_files")
54 | libraryVM.moveToFolder(id = folder.id, files = selectedFiles)
55 | modalSheetState.hide()
56 | }.invokeOnCompletion {
57 | onDone.invoke()
58 | }
59 | }
60 |
61 | ModalBottomSheet(
62 | onDismissRequest = { moveFiles.value = false },
63 | sheetState = modalSheetState
64 | ) {
65 | Column(
66 | modifier = Modifier
67 | .fillMaxHeight(0.8f)
68 | .padding(20.dp),
69 | verticalArrangement = Arrangement.spacedBy(20.dp),
70 | ) {
71 | Text(text = "Select Folder", style = MaterialTheme.typography.titleLarge)
72 |
73 | LazyVerticalGrid(
74 | columns = GridCells.Fixed(3),
75 | verticalArrangement = Arrangement.spacedBy(10.dp),
76 | horizontalArrangement = Arrangement.spacedBy(10.dp),
77 | contentPadding = PaddingValues(bottom = 200.dp)
78 | ) {
79 | items(state.folders) { folder ->
80 | Box {
81 | GridTileComponent(
82 | asset = {
83 | Icon(
84 | imageVector = AppIcons.folder,
85 | contentDescription = "Icon",
86 | modifier = Modifier.size(30.dp),
87 | )
88 | },
89 | title = folder.name,
90 | subtitle = folder.date.dwdm(locale),
91 | tonalElevation = 2.dp,
92 | onClick = { onSelect(folder) }
93 | )
94 | }
95 | }
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/RenameSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 |
4 | import com.ifeanyi.read.app.data.models.FileModel
5 | import com.ifeanyi.read.app.data.models.FolderModel
6 | import com.ifeanyi.read.app.presentation.components.AppButton
7 | import com.ifeanyi.read.app.presentation.components.TextFieldComponent
8 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.fillMaxHeight
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.text.KeyboardActions
14 | import androidx.compose.foundation.text.KeyboardOptions
15 | import androidx.compose.material3.ExperimentalMaterial3Api
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.ModalBottomSheet
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.rememberModalBottomSheetState
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.LaunchedEffect
22 | import androidx.compose.runtime.MutableState
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.rememberCoroutineScope
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.text.input.ImeAction
28 | import androidx.compose.ui.unit.dp
29 | import kotlinx.coroutines.launch
30 |
31 | @Composable
32 | @OptIn(ExperimentalMaterial3Api::class)
33 | fun RenameSheet(
34 | renameItem: MutableState,
35 | file: FileModel? = null,
36 | folder: FolderModel? =null,
37 | libraryVM: LibraryViewModel,
38 | onDone: () -> Unit,
39 | ) {
40 | val name = remember { mutableStateOf("") }
41 | val coroutineScope = rememberCoroutineScope()
42 | val modalSheetState = rememberModalBottomSheetState()
43 |
44 | fun onContinue() {
45 | coroutineScope.launch {
46 | if (file != null) {
47 | libraryVM.updateItem(file.copy(name = name.value))
48 | } else if (folder != null) {
49 | libraryVM.updateItem(folder.copy(name = name.value))
50 | }
51 | modalSheetState.hide()
52 | }.invokeOnCompletion {
53 | onDone.invoke()
54 | }
55 | }
56 |
57 | LaunchedEffect(key1 = Unit) {
58 | name.value = file?.name ?: folder?.name ?: ""
59 | }
60 |
61 | ModalBottomSheet(
62 | onDismissRequest = { renameItem.value = false },
63 | sheetState = modalSheetState
64 | ) {
65 | Column(
66 | modifier = Modifier
67 | .fillMaxHeight(0.5f)
68 | .padding(20.dp),
69 | verticalArrangement = Arrangement.spacedBy(20.dp),
70 | ) {
71 | Text(text = "Rename", style = MaterialTheme.typography.titleLarge)
72 |
73 | TextFieldComponent(
74 | value = name,
75 | label = { Text("Name") },
76 | onImeAction = { onContinue() },
77 | keyboardOptions = KeyboardOptions.Default.copy(
78 | imeAction = ImeAction.Go
79 | ),
80 | keyboardActions = KeyboardActions(
81 | onGo = { onContinue() }
82 | )
83 | )
84 |
85 | AppButton(
86 | text = "Continue",
87 | enabled = name.value.isNotEmpty()
88 | ) { onContinue() }
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/SelectingDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 | import com.ifeanyi.read.app.data.models.FileModel
4 | import com.ifeanyi.read.app.data.models.FolderModel
5 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
6 | import com.ifeanyi.read.core.theme.AppIcons
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.wrapContentSize
12 | import androidx.compose.material3.DropdownMenu
13 | import androidx.compose.material3.DropdownMenuItem
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.MutableState
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.runtime.snapshots.SnapshotStateList
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.unit.dp
23 |
24 | @Composable
25 | fun SelectingDialog(
26 | padding: PaddingValues,
27 | isSelecting: MutableState,
28 | moveFiles: MutableState,
29 | renameItem: MutableState,
30 | showSelectOptions: MutableState,
31 | selectedFiles: SnapshotStateList,
32 | selectedFolders: SnapshotStateList,
33 | libraryVM: LibraryViewModel,
34 | ) {
35 | val allCount = remember(selectedFiles.size, selectedFolders.size) {
36 | selectedFiles.size + selectedFolders.size
37 | }
38 |
39 | Box(
40 | modifier = Modifier
41 | .fillMaxWidth()
42 | .padding(
43 | top = padding.calculateTopPadding(),
44 | start = 20.dp, end = 20.dp,
45 | )
46 | .wrapContentSize(Alignment.TopEnd)
47 | ) {
48 | DropdownMenu(
49 | expanded = showSelectOptions.value,
50 | onDismissRequest = {
51 | showSelectOptions.value = false
52 | },
53 | modifier = Modifier.fillMaxWidth(0.6f)
54 | ) {
55 | DropdownMenuItem(
56 | text = { Text(text = "Delete") },
57 | onClick = {
58 | for (file in selectedFiles) {
59 | libraryVM.deleteItem(file)
60 | }
61 | for (folder in selectedFolders) {
62 | libraryVM.deleteItem(folder)
63 | }
64 | selectedFiles.clear()
65 | selectedFolders.clear()
66 | showSelectOptions.value = false
67 | isSelecting.value = false
68 | },
69 | trailingIcon = {
70 | Icon(
71 | imageVector = AppIcons.delete,
72 | contentDescription = ""
73 | )
74 | },
75 | )
76 |
77 | if (selectedFiles.isNotEmpty()) {
78 | DropdownMenuItem(
79 | text = { Text(text = "Move to folder") },
80 | onClick = {
81 | showSelectOptions.value = false
82 | moveFiles.value = true
83 | },
84 | trailingIcon = {
85 | Icon(
86 | imageVector = AppIcons.folderMove,
87 | contentDescription = ""
88 | )
89 | },
90 | )
91 | }
92 |
93 | if (allCount == 1) {
94 | DropdownMenuItem(
95 | text = { Text(text = "Rename") },
96 | onClick = {
97 | showSelectOptions.value = false
98 | renameItem.value = true
99 | },
100 | trailingIcon = {
101 | Icon(
102 | imageVector = AppIcons.rename,
103 | contentDescription = ""
104 | )
105 | },
106 | )
107 | }
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/SelectingTopBar.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 | import com.ifeanyi.read.app.data.models.FileModel
4 | import com.ifeanyi.read.app.data.models.FolderModel
5 | import com.ifeanyi.read.core.theme.AppIcons
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.width
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.Text
12 | import androidx.compose.material3.TextButton
13 | import androidx.compose.material3.TopAppBar
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.MutableState
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.snapshots.SnapshotStateList
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.unit.dp
20 |
21 | @Composable
22 | @OptIn(ExperimentalMaterial3Api::class)
23 | fun SelectingTopBar(
24 | isSelecting: MutableState,
25 | showSelectOptions: MutableState,
26 | selectedFiles: SnapshotStateList,
27 | selectedFolders: SnapshotStateList,
28 | files: List,
29 | folders: List,
30 | ) {
31 |
32 | val allCount = remember(selectedFiles.size, selectedFolders.size) {
33 | selectedFiles.size + selectedFolders.size
34 | }
35 | val allSelected = remember(allCount) {
36 | allCount > 0 && selectedFiles.size == files.size && selectedFolders.size == folders.size
37 | }
38 |
39 | TopAppBar(
40 | title = { Text(text = "$allCount Selected") },
41 | actions = {
42 | TextButton(onClick = {
43 | selectedFiles.clear()
44 | selectedFolders.clear()
45 | if (!allSelected) {
46 | selectedFiles.addAll(files)
47 | selectedFolders.addAll(folders)
48 | }
49 | }) {
50 | if (allSelected) Icon(
51 | imageVector = AppIcons.checkbox,
52 | contentDescription = ""
53 | ) else Icon(
54 | imageVector = AppIcons.checkboxOutline,
55 | contentDescription = ""
56 | )
57 | Spacer(modifier = Modifier.width(10.dp))
58 | Text(text = "All")
59 | }
60 | TextButton(onClick = {
61 | selectedFiles.clear()
62 | selectedFolders.clear()
63 | isSelecting.value = false
64 | }) {
65 | Text(text = "Cancel")
66 | }
67 | IconButton(
68 | onClick = { showSelectOptions.value = true },
69 | enabled = allCount > 0
70 | ) {
71 | Icon(imageVector = AppIcons.more, contentDescription = "")
72 | }
73 | }
74 | )
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/library/SortDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.library
2 |
3 | import com.ifeanyi.read.app.presentation.viewmodel.LibraryViewModel
4 | import com.ifeanyi.read.core.theme.AppIcons
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.wrapContentSize
10 | import androidx.compose.material3.DropdownMenu
11 | import androidx.compose.material3.DropdownMenuItem
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.MutableState
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.dp
19 |
20 | enum class SortType { Date, Name }
21 |
22 | @Composable
23 | fun SortDialog(
24 | padding: PaddingValues,
25 | showSort: MutableState,
26 | showMore: MutableState,
27 | libraryVM: LibraryViewModel,
28 | ) {
29 | Box(
30 | modifier = Modifier
31 | .fillMaxWidth()
32 | .padding(
33 | top = padding.calculateTopPadding(),
34 | start = 20.dp, end = 20.dp,
35 | )
36 | .wrapContentSize(Alignment.TopEnd)
37 | ) {
38 | DropdownMenu(
39 | expanded = showSort.value,
40 | onDismissRequest = {
41 | showSort.value = false
42 | },
43 | modifier = Modifier.fillMaxWidth(0.6f)
44 | ) {
45 | DropdownMenuItem(
46 | text = { Text(text = "Sort") },
47 | onClick = {
48 | showSort.value = false
49 | showMore.value = true
50 | },
51 | leadingIcon = {
52 | Icon(
53 | imageVector = AppIcons.keyLeft,
54 | contentDescription = ""
55 | )
56 | },
57 | trailingIcon = {
58 | Icon(
59 | imageVector = AppIcons.sort,
60 | contentDescription = ""
61 | )
62 | }
63 | )
64 | DropdownMenuItem(
65 | text = { Text(text = "Name") },
66 | onClick = {
67 | showSort.value = false
68 | libraryVM.sort(SortType.Name)
69 | },
70 | )
71 | DropdownMenuItem(
72 | text = { Text(text = "Date") },
73 | onClick = {
74 | showSort.value = false
75 | libraryVM.sort(SortType.Date)
76 | },
77 | )
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/setting/AboutAppScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.setting
2 |
3 | import com.ifeanyi.read.app.presentation.components.SettingsItem
4 | import com.ifeanyi.read.core.theme.AppIcons
5 | import com.ifeanyi.read.core.util.Constants
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.lazy.LazyColumn
13 | import androidx.compose.material3.ExperimentalMaterial3Api
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Scaffold
16 | import androidx.compose.material3.Surface
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.TopAppBar
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.platform.LocalUriHandler
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.unit.dp
25 | import com.ifeanyi.read.core.util.appVersion
26 |
27 | @OptIn(ExperimentalMaterial3Api::class)
28 | @Composable
29 | fun AboutAppScreen() {
30 | val uriHandler = LocalUriHandler.current
31 |
32 | Scaffold(
33 | topBar = {
34 | TopAppBar(title = { Text(text = "About App") })
35 | }
36 | ) { padding ->
37 | LazyColumn(
38 | contentPadding = PaddingValues(
39 | top = padding.calculateTopPadding(),
40 | start = 20.dp, end = 20.dp,
41 | bottom = 200.dp
42 | ),
43 | verticalArrangement = Arrangement.spacedBy(15.dp)
44 | ) {
45 | item {
46 | Surface(tonalElevation = 1.dp, shape = MaterialTheme.shapes.small) {
47 | Column(modifier = Modifier.padding(horizontal = 15.dp, vertical = 10.dp)) {
48 | SettingsItem(
49 | title = "Privacy Policy",
50 | icon = AppIcons.shield,
51 | color = Color(0XFFBF5AF2)
52 | ) {
53 | uriHandler.openUri(uri = Constants.privacyLink)
54 | }
55 | SettingsItem(
56 | title = "Terms of Service",
57 | icon = AppIcons.doc,
58 | color = Color(0xFF63D2FF)
59 | ) {
60 | uriHandler.openUri(uri = Constants.termsLink)
61 | }
62 | }
63 | }
64 | }
65 | item {
66 | Row(
67 | modifier = Modifier.fillMaxWidth(),
68 | horizontalArrangement = Arrangement.Center
69 | ) {
70 | Text(
71 | text = "VER $appVersion",
72 | textAlign = TextAlign.Center,
73 | style = MaterialTheme.typography.bodySmall
74 | )
75 | }
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/setting/DisplayDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.setting
2 |
3 | import com.ifeanyi.read.app.presentation.viewmodel.SettingsViewModel
4 | import com.ifeanyi.read.core.enums.DisplayStyle
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.wrapContentSize
10 | import androidx.compose.material3.DropdownMenu
11 | import androidx.compose.material3.DropdownMenuItem
12 | import androidx.compose.material3.Text
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 androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | fun DisplayDialog(
21 | padding: PaddingValues,
22 | showDisplay: MutableState,
23 | settingsVM: SettingsViewModel,
24 | ) {
25 | Box(
26 | modifier = Modifier
27 | .fillMaxWidth()
28 | .padding(
29 | top = padding.calculateTopPadding() + 60.dp,
30 | start = 20.dp, end = 20.dp,
31 | )
32 | .wrapContentSize(Alignment.TopEnd)
33 | ) {
34 | DropdownMenu(
35 | expanded = showDisplay.value,
36 | onDismissRequest = {
37 | showDisplay.value = false
38 | },
39 | modifier = Modifier.fillMaxWidth(0.6f)
40 | ) {
41 | DisplayStyle.entries.map {
42 | DropdownMenuItem(
43 | text = { Text(text = it.name) },
44 | onClick = {
45 | showDisplay.value = false
46 | settingsVM.setDisplayStyle(it)
47 | },
48 | )
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/setting/TextToSpeechScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.setting
2 |
3 | import com.ifeanyi.read.app.presentation.components.CustomSliderSheet
4 | import com.ifeanyi.read.app.presentation.components.SettingsItem
5 | import com.ifeanyi.read.app.presentation.components.VoiceSelectorSheet
6 | import com.ifeanyi.read.app.presentation.viewmodel.SettingsViewModel
7 | import com.ifeanyi.read.core.services.SpeechService
8 | import com.ifeanyi.read.core.theme.AppIcons
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.PaddingValues
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.material3.Surface
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TopAppBar
20 | import androidx.compose.material3.rememberModalBottomSheetState
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.collectAsState
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.rememberCoroutineScope
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.text.style.TextOverflow
29 | import androidx.compose.ui.unit.dp
30 | import androidx.hilt.navigation.compose.hiltViewModel
31 | import kotlinx.coroutines.launch
32 |
33 | @OptIn(ExperimentalMaterial3Api::class)
34 | @Composable
35 | fun TextToSpeechScreen(settingsVM: SettingsViewModel = hiltViewModel()) {
36 | val state = settingsVM.state.collectAsState().value
37 |
38 | val coroutineScope = rememberCoroutineScope()
39 | val modalSheetState = rememberModalBottomSheetState()
40 |
41 | val showVoicesSheet = remember { mutableStateOf(false) }
42 | val showRateSheet = remember { mutableStateOf(false) }
43 |
44 | Scaffold(
45 | topBar = {
46 | TopAppBar(title = { Text(text = "Text To Speech") })
47 | }
48 | ) { padding ->
49 | if (showVoicesSheet.value) {
50 | VoiceSelectorSheet(
51 | showVoicesSheet = showVoicesSheet,
52 | modalSheetState = modalSheetState,
53 | initial = state.voice,
54 | ) { voice ->
55 | coroutineScope.launch {
56 | settingsVM.setVoice(voice)
57 | modalSheetState.hide()
58 | }.invokeOnCompletion {
59 | showVoicesSheet.value = false
60 | if (SpeechService.state.value.model != null) {
61 | SpeechService.stopAndPlay()
62 | }
63 | }
64 | }
65 | }
66 |
67 | if (showRateSheet.value) {
68 | CustomSliderSheet(
69 | showRateSheet = showRateSheet,
70 | modalSheetState = modalSheetState,
71 | initialProgress = state.speechRate / 2
72 | ) { rate ->
73 | coroutineScope.launch {
74 | settingsVM.setSpeechRate(rate)
75 | modalSheetState.hide()
76 | }.invokeOnCompletion {
77 | showRateSheet.value = false
78 | if (SpeechService.state.value.model != null) {
79 | SpeechService.stopAndPlay()
80 | }
81 | }
82 | }
83 | }
84 |
85 | LazyColumn(
86 | contentPadding = PaddingValues(
87 | top = padding.calculateTopPadding(),
88 | start = 20.dp, end = 20.dp,
89 | bottom = 200.dp
90 | ),
91 | verticalArrangement = Arrangement.spacedBy(15.dp)
92 | ) {
93 | item {
94 | Surface(tonalElevation = 1.dp, shape = MaterialTheme.shapes.small) {
95 | Column(modifier = Modifier.padding(horizontal = 15.dp, vertical = 10.dp)) {
96 | SettingsItem(
97 | title = "Speaker Voice",
98 | icon = AppIcons.speaker,
99 | color = Color(0xFFFF9E08),
100 | trailing = {
101 | Text(
102 | text = state.voice?.locale?.displayCountry ?: "",
103 | maxLines = 1,
104 | overflow = TextOverflow.Ellipsis
105 | )
106 | }
107 | ) {
108 | showVoicesSheet.value = true
109 | }
110 | SettingsItem(
111 | title = "Speech Rate",
112 | icon = AppIcons.speechRate,
113 | color = Color(0xFF0F85FF),
114 | trailing = { Text(text = "${state.speechRate}") }
115 | ) {
116 | showRateSheet.value = true
117 | }
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/setting/ThemeDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.setting
2 |
3 | import com.ifeanyi.read.app.presentation.viewmodel.SettingsViewModel
4 | import com.ifeanyi.read.core.enums.AppTheme
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.wrapContentSize
10 | import androidx.compose.material3.DropdownMenu
11 | import androidx.compose.material3.DropdownMenuItem
12 | import androidx.compose.material3.Text
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 androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | fun ThemeDialog(
21 | padding: PaddingValues,
22 | showTheme: MutableState,
23 | settingsVM: SettingsViewModel,
24 | ) {
25 | Box(
26 | modifier = Modifier
27 | .fillMaxWidth()
28 | .padding(
29 | top = padding.calculateTopPadding(),
30 | start = 20.dp, end = 20.dp,
31 | )
32 | .wrapContentSize(Alignment.TopEnd)
33 | ) {
34 | DropdownMenu(
35 | expanded = showTheme.value,
36 | onDismissRequest = {
37 | showTheme.value = false
38 | },
39 | modifier = Modifier.fillMaxWidth(0.6f)
40 | ) {
41 | AppTheme.entries.map {
42 | DropdownMenuItem(
43 | text = { Text(text = it.name) },
44 | onClick = {
45 | showTheme.value = false
46 | settingsVM.setTheme(it)
47 | },
48 | )
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/setting/WhatsNewSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.setting
2 |
3 | import com.ifeanyi.read.app.presentation.viewmodel.SettingsViewModel
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.lazy.LazyColumn
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.ModalBottomSheet
13 | import androidx.compose.material3.Surface
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.rememberModalBottomSheetState
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.MutableState
18 | import androidx.compose.runtime.collectAsState
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.unit.dp
22 | import androidx.hilt.navigation.compose.hiltViewModel
23 |
24 | @OptIn(ExperimentalMaterial3Api::class)
25 | @Composable
26 | fun WhatsNewSheet(
27 | showWhatsNewSheet: MutableState,
28 | settingsVM: SettingsViewModel = hiltViewModel()
29 | ) {
30 | val state = settingsVM.state.collectAsState().value
31 | val modalSheetState = rememberModalBottomSheetState()
32 |
33 | ModalBottomSheet(
34 | onDismissRequest = { showWhatsNewSheet.value = false },
35 | sheetState = modalSheetState
36 | ) {
37 | Column(
38 | modifier = Modifier.padding(20.dp),
39 | verticalArrangement = Arrangement.spacedBy(15.dp)
40 | ) {
41 | Text(text = "What's New", style = MaterialTheme.typography.titleLarge)
42 |
43 | LazyColumn(
44 | verticalArrangement = Arrangement.spacedBy(15.dp),
45 | ) {
46 | items(state.whatsNew) { whatsNew ->
47 | Surface(tonalElevation = 2.dp, shape = MaterialTheme.shapes.small) {
48 | Column(
49 | modifier = Modifier
50 | .fillMaxWidth()
51 | .padding(15.dp),
52 | verticalArrangement = Arrangement.spacedBy(15.dp)
53 | ) {
54 | Text(
55 | text = "🚀 Version ${whatsNew.id}",
56 | style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)
57 | )
58 | whatsNew.features.map {
59 | Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
60 | Text(
61 | text = it.title,
62 | style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
63 | )
64 | Text(text = it.body, style = MaterialTheme.typography.bodySmall)
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/speech/GoToPageSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.speech
2 |
3 | import com.ifeanyi.read.app.presentation.components.AppButton
4 | import com.ifeanyi.read.app.presentation.components.TextFieldComponent
5 | import com.ifeanyi.read.core.services.AppStateService
6 | import com.ifeanyi.read.core.services.SpeechService
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.text.KeyboardOptions
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.ModalBottomSheet
15 | import androidx.compose.material3.SheetState
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.MutableState
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.runtime.rememberCoroutineScope
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.platform.LocalFocusManager
24 | import androidx.compose.ui.text.input.KeyboardType
25 | import androidx.compose.ui.unit.dp
26 | import kotlinx.coroutines.launch
27 | import java.lang.NumberFormatException
28 |
29 | @Composable
30 | @OptIn(ExperimentalMaterial3Api::class)
31 | fun GoToPageSheet(
32 | showPageSheet: MutableState,
33 | modalSheetState: SheetState,
34 | ) {
35 | val focusManager = LocalFocusManager.current
36 |
37 | val text = remember { mutableStateOf("") }
38 | val coroutineScope = rememberCoroutineScope()
39 |
40 | fun onContinue() {
41 | focusManager.clearFocus()
42 |
43 | coroutineScope.launch {
44 | try {
45 | SpeechService.goToPage(text.value.toInt())
46 | } catch (exc: NumberFormatException) {
47 | AppStateService.displayMessage("Enter a valid page number")
48 | }
49 | modalSheetState.hide()
50 | }.invokeOnCompletion {
51 | showPageSheet.value = false
52 | }
53 | }
54 |
55 | ModalBottomSheet(
56 | onDismissRequest = { showPageSheet.value = false },
57 | sheetState = modalSheetState
58 | ) {
59 | Column(
60 | modifier = Modifier
61 | .fillMaxHeight(0.5f)
62 | .padding(20.dp),
63 | verticalArrangement = Arrangement.spacedBy(20.dp),
64 | ) {
65 | Text(text = "Go To Page", style = MaterialTheme.typography.titleLarge)
66 |
67 | TextFieldComponent(
68 | value = text,
69 | label = { Text("Page") },
70 | onImeAction = { onContinue() },
71 | keyboardOptions = KeyboardOptions(
72 | keyboardType = KeyboardType.Number
73 | )
74 | )
75 |
76 | AppButton(
77 | text = "Continue",
78 | enabled = text.value.isNotEmpty()
79 | ) { onContinue() }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/app/presentation/views/speech/HighlightedText.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.app.presentation.views.speech
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.text.buildAnnotatedString
9 | import androidx.compose.ui.text.font.FontWeight
10 | import androidx.compose.ui.text.withStyle
11 |
12 | @Composable
13 | fun HighlightedText(text: String, range: IntRange) {
14 | val displayedText = remember { mutableStateOf(buildAnnotatedString { }) }
15 |
16 | val wordStyle = MaterialTheme.typography.bodyMedium.toSpanStyle().copy(
17 | fontWeight = FontWeight.Bold,
18 | background = MaterialTheme.colorScheme.tertiaryContainer,
19 | )
20 |
21 | val sentenceStyle = MaterialTheme.typography.bodyMedium.toSpanStyle()
22 |
23 | val spokenStyle = MaterialTheme.typography.bodyMedium.toSpanStyle().copy(
24 | color = MaterialTheme.colorScheme.outline
25 | )
26 |
27 |
28 | displayedText.value = buildAnnotatedString {
29 | withStyle(sentenceStyle) {
30 | append(text)
31 | }
32 |
33 | addStyle(spokenStyle, start = 0, end = range.first)
34 | addStyle(wordStyle, start = range.first, end = range.last)
35 | }
36 |
37 | Text(text = displayedText.value)
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.di
2 |
3 | import android.content.Context
4 | import com.ifeanyi.read.app.data.LibraryRepository
5 | import com.ifeanyi.read.app.data.SettingsRepository
6 | import com.ifeanyi.read.app.data.source.FileDao
7 | import com.ifeanyi.read.app.data.source.FolderDao
8 | import com.ifeanyi.read.app.data.source.WhatsNewDao
9 | import com.ifeanyi.read.core.services.DatabaseService
10 | import com.ifeanyi.read.core.services.PreferenceService
11 | import androidx.room.Room
12 | import dagger.Module
13 | import dagger.Provides
14 | import dagger.hilt.InstallIn
15 | import dagger.hilt.android.qualifiers.ApplicationContext
16 | import dagger.hilt.components.SingletonComponent
17 | import javax.inject.Singleton
18 |
19 | @InstallIn(SingletonComponent::class)
20 | @Module
21 | object AppModule {
22 | @Provides
23 | @Singleton
24 | fun providePreferenceService(@ApplicationContext context: Context): PreferenceService {
25 | return PreferenceService(context = context)
26 | }
27 |
28 | @Singleton
29 | @Provides
30 | fun provideFileDao(databaseService: DatabaseService): FileDao = databaseService.file()
31 |
32 | @Singleton
33 | @Provides
34 | fun provideFolderDao(databaseService: DatabaseService): FolderDao = databaseService.folder()
35 |
36 | @Singleton
37 | @Provides
38 | fun provideWhatsNewDao(databaseService: DatabaseService): WhatsNewDao = databaseService.whatsNew()
39 |
40 | @Singleton
41 | @Provides
42 | fun provideAppDatabase(@ApplicationContext context: Context): DatabaseService =
43 | Room.databaseBuilder(
44 | context,
45 | DatabaseService::class.java,
46 | name = "read_db"
47 | ).fallbackToDestructiveMigration().build()
48 |
49 | @Singleton
50 | @Provides
51 | fun provideLibraryRepository(fileDao: FileDao, folderDao: FolderDao): LibraryRepository =
52 | LibraryRepository(fileDao, folderDao)
53 |
54 | @Singleton
55 | @Provides
56 | fun provideSettingsRepository(whatsNewDao: WhatsNewDao): SettingsRepository =
57 | SettingsRepository(whatsNewDao)
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/enums/ActivityType.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.enums
2 |
3 | enum class ActivityType { MainActivity, MainActivityPurple, MainActivityWhite }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/enums/AppTheme.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.enums
2 |
3 | enum class AppTheme { System, Light, Dark }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/enums/DisplayStyle.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.enums
2 |
3 | enum class DisplayStyle { List, Grid }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/route/BottomRouter.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.route
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.filled.LibraryBooks
5 | import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
6 | import androidx.compose.material.icons.filled.Home
7 | import androidx.compose.material.icons.filled.Settings
8 | import androidx.compose.material.icons.outlined.Home
9 | import androidx.compose.material.icons.outlined.Settings
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.runtime.Composable
12 |
13 | sealed class BottomRouter(
14 | val route: String,
15 | val label: String,
16 | val icon: @Composable () -> Unit,
17 | val inactiveIcon: @Composable () -> Unit
18 | ) {
19 |
20 | data object Home : BottomRouter(
21 | route = Routes.HomeScreen.name,
22 | label = "Home",
23 | icon = {
24 | Icon(imageVector = Icons.Default.Home, contentDescription = "Home Tab")
25 | },
26 | inactiveIcon = {
27 | Icon(imageVector = Icons.Outlined.Home, contentDescription = "Home Tab")
28 | }
29 | )
30 |
31 | data object Library : BottomRouter(
32 | route = Routes.LibraryScreen.name,
33 | label = "Library",
34 | icon = {
35 | Icon(imageVector = Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = "Library Tab")
36 | },
37 | inactiveIcon = {
38 | Icon(imageVector = Icons.AutoMirrored.Outlined.LibraryBooks, contentDescription = "Library Tab")
39 | }
40 | )
41 |
42 | data object Settings : BottomRouter(
43 | route = Routes.SettingsScreen.name,
44 | label = "Settings",
45 | icon = {
46 | Icon(imageVector = Icons.Default.Settings, contentDescription = "Settings Tab")
47 | },
48 | inactiveIcon = {
49 | Icon(imageVector = Icons.Outlined.Settings, contentDescription = "Settings Tab")
50 | }
51 | )
52 | }
53 |
54 | val bottomNavItems = listOf(
55 | BottomRouter.Home,
56 | BottomRouter.Library,
57 | BottomRouter.Settings
58 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/route/Router.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.route
2 |
3 | import com.ifeanyi.read.app.presentation.views.home.HomeScreen
4 | import com.ifeanyi.read.app.presentation.views.home.EnterTextScreen
5 | import com.ifeanyi.read.app.presentation.views.library.FolderScreen
6 | import com.ifeanyi.read.app.presentation.views.library.LibraryScreen
7 | import com.ifeanyi.read.app.presentation.views.setting.AboutAppScreen
8 | import com.ifeanyi.read.app.presentation.views.setting.AppearanceScreen
9 | import com.ifeanyi.read.app.presentation.views.setting.SettingsScreen
10 | import com.ifeanyi.read.app.presentation.views.setting.TextToSpeechScreen
11 | import android.os.Build
12 | import androidx.annotation.RequiresApi
13 | import androidx.compose.runtime.Composable
14 | import androidx.navigation.NavHostController
15 | import androidx.navigation.compose.NavHost
16 | import androidx.navigation.compose.composable
17 | import java.util.UUID
18 |
19 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
20 | @Composable
21 | fun Router(
22 | controller: NavHostController,
23 | onIconChangeRed: () -> Unit,
24 | onIconChangePurple: () -> Unit,
25 | onIconChangeWhite: () -> Unit
26 | ) {
27 |
28 | NavHost(navController = controller, startDestination = Routes.HomeScreen.name) {
29 | composable(Routes.HomeScreen.name) {
30 | HomeScreen(controller = controller)
31 | }
32 | composable(Routes.EnterTextScreen.name) {
33 | EnterTextScreen(controller = controller)
34 | }
35 | composable(Routes.LibraryScreen.name) {
36 | LibraryScreen(controller = controller)
37 | }
38 | val folderRoute = "${Routes.FolderScreen.name}/{id}/{name}"
39 | composable(folderRoute) {
40 | val id = UUID.fromString(it.arguments?.getString("id"))
41 | val name = it.arguments?.getString("name")
42 | FolderScreen(id = id, name = name ?: "Folder")
43 | }
44 | composable(Routes.SettingsScreen.name) {
45 | SettingsScreen(controller = controller)
46 | }
47 | composable(Routes.AboutAppScreen.name) {
48 | AboutAppScreen()
49 | }
50 | composable(Routes.TextToSpeechScreen.name) {
51 | TextToSpeechScreen()
52 | }
53 | composable(Routes.AppearanceScreen.name) {
54 | AppearanceScreen(
55 | onIconChangeRed = onIconChangeRed,
56 | onIconChangePurple = onIconChangePurple,
57 | onIconChangeWhite = onIconChangeWhite
58 | )
59 | }
60 | }
61 | }
62 |
63 | val NavHostController.parentRoute: Routes
64 | get() = this.currentBackStackEntry.let {
65 | val route = it?.destination?.route?.split("/")?.first() ?: Routes.HomeScreen.name
66 |
67 | val parentRoute = when (Routes.valueOf(route)) {
68 | Routes.HomeScreen -> Routes.HomeScreen
69 | Routes.EnterTextScreen -> Routes.HomeScreen
70 | Routes.LibraryScreen -> Routes.LibraryScreen
71 | Routes.FolderScreen -> Routes.LibraryScreen
72 | Routes.SettingsScreen -> Routes.SettingsScreen
73 | Routes.AboutAppScreen -> Routes.SettingsScreen
74 | Routes.TextToSpeechScreen -> Routes.SettingsScreen
75 | Routes.AppearanceScreen -> Routes.SettingsScreen
76 | }
77 | return parentRoute
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/route/Routes.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.route
2 |
3 | enum class Routes {
4 | HomeScreen,
5 | EnterTextScreen,
6 | LibraryScreen,
7 | FolderScreen,
8 | SettingsScreen,
9 | AboutAppScreen,
10 | TextToSpeechScreen,
11 | AppearanceScreen
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/services/AnalyticService.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.services
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import com.ifeanyi.read.BuildConfig
6 | import com.mixpanel.android.mpmetrics.MixpanelAPI
7 |
8 | object AnalyticService {
9 | @SuppressLint("StaticFieldLeak")
10 | private var _mixpanel: MixpanelAPI? = null
11 |
12 | fun init(context: Context) {
13 | _mixpanel = MixpanelAPI.getInstance(context, BuildConfig.MIX_PANEL_KEY, true)
14 | }
15 |
16 | fun track(event: String) {
17 | if (BuildConfig.DEBUG) return
18 | _mixpanel?.track(event)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/services/DatabaseService.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.services
2 |
3 | import com.ifeanyi.read.app.data.models.FileModel
4 | import com.ifeanyi.read.app.data.models.FolderModel
5 | import com.ifeanyi.read.app.data.models.WhatsNewModel
6 | import com.ifeanyi.read.app.data.source.FileDao
7 | import com.ifeanyi.read.app.data.source.FolderDao
8 | import com.ifeanyi.read.app.data.source.WhatsNewDao
9 | import com.ifeanyi.read.core.util.RoomConverters
10 | import androidx.room.Database
11 | import androidx.room.RoomDatabase
12 | import androidx.room.TypeConverters
13 |
14 | @Database(
15 | entities = [FileModel::class, FolderModel::class, WhatsNewModel::class],
16 | version = 2,
17 | exportSchema = false
18 | )
19 | @TypeConverters(RoomConverters::class)
20 | abstract class DatabaseService : RoomDatabase() {
21 | abstract fun file(): FileDao
22 | abstract fun folder(): FolderDao
23 | abstract fun whatsNew(): WhatsNewDao
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/services/NotificationService.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.services
2 |
3 | import android.Manifest
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.app.Service
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.pm.PackageManager
10 | import com.ifeanyi.read.R
11 | import android.os.Bundle
12 | import android.os.IBinder
13 | import android.support.v4.media.MediaMetadataCompat
14 | import android.support.v4.media.session.MediaSessionCompat
15 | import android.support.v4.media.session.PlaybackStateCompat
16 | import androidx.core.app.ActivityCompat
17 | import androidx.core.app.NotificationCompat
18 | import androidx.core.app.NotificationManagerCompat
19 | import androidx.media.app.NotificationCompat as MediaNotification
20 |
21 | const val ACTION_FORWARD = "ACTION_FORWARD"
22 | const val ACTION_REWIND = "ACTION_REWIND"
23 | const val CHANNEL_ID = "TTS_NOTIFICATION_CHANNEL"
24 |
25 | val notificationService = NotificationService.getInstance
26 |
27 | class NotificationService: Service() {
28 | companion object {
29 | private var instance: NotificationService? = null
30 |
31 | val getInstance: NotificationService
32 | get() {
33 | return instance ?: synchronized(this) {
34 | instance ?: NotificationService().also { instance = it }
35 | }
36 | }
37 | }
38 |
39 | private lateinit var _appContext: Context
40 | private lateinit var _session: MediaSessionCompat
41 |
42 | private var _setup: Boolean = false
43 |
44 | fun init(context: Context) {
45 | _appContext = context
46 | _session = MediaSessionCompat(_appContext, "MediaSession")
47 | val serviceIntent = Intent(context, NotificationService::class.java)
48 | context.startService(serviceIntent)
49 | }
50 |
51 | fun destroy() {
52 | val notificationManagerCompat = NotificationManagerCompat.from(_appContext)
53 | notificationManagerCompat.cancel(0)
54 | _session.release()
55 | val serviceIntent = Intent(_appContext, NotificationService::class.java)
56 | _appContext.stopService(serviceIntent)
57 | }
58 |
59 | fun showMediaStyleNotification() {
60 | val state = SpeechService.state.value
61 | if (state.model == null) return
62 |
63 | if (ActivityCompat.checkSelfPermission(
64 | _appContext,
65 | Manifest.permission.POST_NOTIFICATIONS
66 | ) != PackageManager.PERMISSION_GRANTED
67 | ) {
68 | println("NOT GRANTED")
69 | return
70 | }
71 |
72 | val callback = object : MediaSessionCompat.Callback() {
73 | override fun onCustomAction(action: String?, extras: Bundle?) {
74 | when (action) {
75 | ACTION_FORWARD -> {
76 | SpeechService.forward()
77 | }
78 |
79 | ACTION_REWIND -> {
80 | SpeechService.rewind()
81 | }
82 | }
83 | }
84 |
85 | override fun onPlay() {
86 | SpeechService.play()
87 | }
88 |
89 | override fun onPause() {
90 | SpeechService.pause()
91 | }
92 | }
93 |
94 | val playbackStateBuilder = PlaybackStateCompat.Builder()
95 | .setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
96 | .setState(
97 | if (state.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED,
98 | (state.progress * 100).toLong(),
99 | 1f
100 | )
101 | .addCustomAction(
102 | ACTION_REWIND,
103 | "Rewind",
104 | R.drawable.round_replay_10_24
105 | )
106 | .addCustomAction(
107 | ACTION_FORWARD,
108 | "Forward",
109 | R.drawable.round_forward_10_24
110 | )
111 | .build()
112 |
113 | val file = state.model
114 | val metadata = MediaMetadataCompat.Builder()
115 | .putString(MediaMetadataCompat.METADATA_KEY_TITLE, file.name)
116 | .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, file.type.name.lowercase())
117 | .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, 100)
118 | .build()
119 |
120 | _session.setPlaybackState(playbackStateBuilder)
121 | _session.setCallback(callback)
122 | _session.setMetadata(metadata)
123 |
124 | if (_setup) return
125 |
126 | val builder = NotificationCompat.Builder(_appContext, CHANNEL_ID)
127 | .setSmallIcon(R.drawable.round_record_voice_over_24)
128 | .setStyle(MediaNotification.MediaStyle().setMediaSession(_session.sessionToken))
129 | .setOngoing(true)
130 | .setAutoCancel(false)
131 | .setShowWhen(false)
132 |
133 | val notification = builder.build()
134 | val notificationManagerCompat = NotificationManagerCompat.from(_appContext)
135 |
136 | createNotificationChannel(notificationManagerCompat)
137 |
138 | notificationManagerCompat.notify(0, notification)
139 |
140 | _setup = true
141 | }
142 |
143 | private fun createNotificationChannel(notificationManagerCompat: NotificationManagerCompat) {
144 | val channelName = "Text To Speech Notification Channel"
145 | val channelDescription = "Channel for TTS notifications"
146 | val importance = NotificationManager.IMPORTANCE_HIGH
147 | val notificationChannel = NotificationChannel(CHANNEL_ID, channelName, importance).apply {
148 | description = channelDescription
149 | }
150 |
151 | notificationManagerCompat.createNotificationChannel(notificationChannel)
152 | }
153 |
154 | override fun onBind(intent: Intent?): IBinder? {
155 | return null
156 | }
157 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/services/PreferenceService.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.services
2 |
3 | import android.content.Context
4 | import com.ifeanyi.read.core.enums.AppTheme
5 | import com.ifeanyi.read.core.enums.DisplayStyle
6 | import com.ifeanyi.read.core.util.Constants
7 | import android.speech.tts.Voice
8 | import androidx.datastore.core.DataStore
9 | import androidx.datastore.preferences.core.Preferences
10 | import androidx.datastore.preferences.core.edit
11 | import androidx.datastore.preferences.core.stringPreferencesKey
12 | import androidx.datastore.preferences.preferencesDataStore
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.map
15 | import java.util.Locale
16 |
17 | class PreferenceService(private val context: Context) {
18 | companion object {
19 | private val Context.dataStore: DataStore by preferencesDataStore(name = "store")
20 | }
21 |
22 | fun getString(key: String): Flow {
23 | return context.dataStore.data.map { pref ->
24 | pref[stringPreferencesKey(key)]
25 | }
26 | }
27 |
28 | suspend fun saveString(key: String, value: String) {
29 | context.dataStore.edit { preferences ->
30 | preferences[stringPreferencesKey(key)] = value
31 | }
32 | }
33 |
34 | fun getVoice(): Flow {
35 | return context.dataStore.data.map { pref ->
36 | val items = pref[stringPreferencesKey(Constants.voice)]?.split("/") ?: listOf("en-us-x-tpf-local", "en", "US")
37 | Voice(items[0], Locale(items[1], items[2]), 400, 200, false, emptySet())
38 | }
39 | }
40 |
41 | fun getTheme(): Flow {
42 | return context.dataStore.data.map { pref ->
43 | AppTheme.valueOf(pref[stringPreferencesKey(Constants.theme)] ?: "System")
44 | }
45 | }
46 |
47 | fun getDisplayStyle(): Flow {
48 | return context.dataStore.data.map { pref ->
49 | DisplayStyle.valueOf(pref[stringPreferencesKey(Constants.displayStyle)] ?: "Grid")
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/services/SnackbarService.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.services
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.flow.update
9 | import kotlinx.coroutines.launch
10 |
11 | data class SnackBarState(val hasMessage: Boolean = false, val message: String = "")
12 | data class LoadingState(val isLoading: Boolean = false, val message: String = "")
13 | object AppStateService: ViewModel() {
14 | private val _snackBar = MutableStateFlow(SnackBarState())
15 | private val _loader = MutableStateFlow(LoadingState())
16 | val snackBar = _snackBar.asStateFlow()
17 | val loader = _loader.asStateFlow()
18 |
19 | fun displayMessage(message: String) = viewModelScope.launch {
20 | _snackBar.update { it.copy(hasMessage = true, message = message) }
21 | delay(4000L)
22 | _snackBar.update { it.copy(hasMessage = false, message = "") }
23 | }
24 |
25 | fun displayLoader(message: String = "loading ...") = viewModelScope.launch {
26 | _loader.update { it.copy(isLoading = true, message = message) }
27 | }
28 |
29 | fun removeLoader() = viewModelScope.launch {
30 | _loader.update { it.copy(isLoading = false, message = "") }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/theme/AppIcons.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.theme
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
5 | import androidx.compose.material.icons.automirrored.outlined.InsertDriveFile
6 | import androidx.compose.material.icons.automirrored.outlined.Sort
7 | import androidx.compose.material.icons.automirrored.outlined.ViewList
8 | import androidx.compose.material.icons.automirrored.outlined.VolumeUp
9 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
10 | import androidx.compose.material.icons.outlined.Checklist
11 | import androidx.compose.material.icons.outlined.ColorLens
12 | import androidx.compose.material.icons.outlined.CreateNewFolder
13 | import androidx.compose.material.icons.outlined.DarkMode
14 | import androidx.compose.material.icons.outlined.Delete
15 | import androidx.compose.material.icons.outlined.DocumentScanner
16 | import androidx.compose.material.icons.outlined.DriveFileRenameOutline
17 | import androidx.compose.material.icons.outlined.Folder
18 | import androidx.compose.material.icons.outlined.GridView
19 | import androidx.compose.material.icons.outlined.Image
20 | import androidx.compose.material.icons.outlined.Info
21 | import androidx.compose.material.icons.outlined.LightMode
22 | import androidx.compose.material.icons.outlined.NewReleases
23 | import androidx.compose.material.icons.outlined.QuestionMark
24 | import androidx.compose.material.icons.outlined.RecordVoiceOver
25 | import androidx.compose.material.icons.outlined.Share
26 | import androidx.compose.material.icons.outlined.Shield
27 | import androidx.compose.material.icons.outlined.StarBorder
28 | import androidx.compose.material.icons.outlined.TextFields
29 | import androidx.compose.material.icons.rounded.CheckBox
30 | import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
31 | import androidx.compose.material.icons.rounded.FastForward
32 | import androidx.compose.material.icons.rounded.FastRewind
33 | import androidx.compose.material.icons.rounded.FlagCircle
34 | import androidx.compose.material.icons.rounded.Link
35 | import androidx.compose.material.icons.rounded.MoreVert
36 | import androidx.compose.material.icons.rounded.Pause
37 | import androidx.compose.material.icons.rounded.PlayArrow
38 | import androidx.compose.material.icons.rounded.Speed
39 | import androidx.compose.material.icons.rounded.Stop
40 |
41 | object AppIcons {
42 | val doc = Icons.AutoMirrored.Outlined.InsertDriveFile
43 | val image = Icons.Outlined.Image
44 | val scan = Icons.Outlined.DocumentScanner
45 | val text = Icons.Outlined.TextFields
46 | val link = Icons.Rounded.Link
47 | val speaker = Icons.Outlined.RecordVoiceOver
48 | val pause = Icons.Rounded.Pause
49 | val stop = Icons.Rounded.Stop
50 | val play = Icons.Rounded.PlayArrow
51 | val forward = Icons.Rounded.FastForward
52 | val rewind = Icons.Rounded.FastRewind
53 | val speechRate = Icons.Rounded.Speed
54 | val more = Icons.Rounded.MoreVert
55 | val newFolder = Icons.Outlined.CreateNewFolder
56 | val sort = Icons.AutoMirrored.Outlined.Sort
57 | val checklist = Icons.Outlined.Checklist
58 | val listView = Icons.AutoMirrored.Outlined.ViewList
59 | val gridView = Icons.Outlined.GridView
60 | val checkbox = Icons.Rounded.CheckBox
61 | val checkboxOutline = Icons.Rounded.CheckBoxOutlineBlank
62 | val folder = Icons.Outlined.Folder
63 | val delete = Icons.Outlined.Delete
64 | val folderMove = Icons.AutoMirrored.Outlined.DriveFileMove
65 | val rename = Icons.Outlined.DriveFileRenameOutline
66 | val keyLeft = Icons.AutoMirrored.Rounded.KeyboardArrowLeft
67 | val flag = Icons.Rounded.FlagCircle
68 | val theme = Icons.Outlined.ColorLens
69 | val dark = Icons.Outlined.DarkMode
70 | val light = Icons.Outlined.LightMode
71 | val star = Icons.Outlined.StarBorder
72 | val question = Icons.Outlined.QuestionMark
73 | val share = Icons.Outlined.Share
74 | val shield = Icons.Outlined.Shield
75 | val newRelease = Icons.Outlined.NewReleases
76 | val about = Icons.Outlined.Info
77 | val waveform = Icons.AutoMirrored.Outlined.VolumeUp
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.theme
2 |
3 | import android.app.Activity
4 | import com.ifeanyi.read.app.presentation.viewmodel.SettingsViewModel
5 | import com.ifeanyi.read.core.enums.AppTheme
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.darkColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.SideEffect
12 | import androidx.compose.runtime.collectAsState
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.toArgb
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 | import androidx.hilt.navigation.compose.hiltViewModel
18 |
19 | private val DarkColorScheme = darkColorScheme(
20 | primary = Color(0xff748bac),
21 | onPrimary = Color(0xfff8f9fc),
22 | primaryContainer = Color(0xff1b2e4b),
23 | onPrimaryContainer = Color(0xffe4e7eb),
24 | secondary = Color(0xff539eaf),
25 | onSecondary = Color(0xfff5fbfc),
26 | secondaryContainer = Color(0xff004e5d),
27 | onSecondaryContainer = Color(0xffdfecee),
28 | tertiary = Color(0xff219ab5),
29 | onTertiary = Color(0xfff1fbfd),
30 | tertiaryContainer = Color(0xff0f5b6a),
31 | onTertiaryContainer = Color(0xffe2eef0),
32 | error = Color(0xffcf6679),
33 | onError = Color(0xff140c0d),
34 | errorContainer = Color(0xffb1384e),
35 | onErrorContainer = Color(0xfffbe8ec),
36 | background = Color(0xff161718),
37 | onBackground = Color(0xffececec),
38 | surface = Color(0xff161718),
39 | onSurface = Color(0xffececec),
40 | surfaceVariant = Color(0xff383b3e),
41 | onSurfaceVariant = Color(0xffdfe0e0),
42 | outline = Color(0xff797979),
43 | outlineVariant = Color(0xff2d2d2d),
44 | scrim = Color(0xff000000),
45 | inverseSurface = Color(0xfff7f9fa),
46 | inversePrimary = Color(0xff404a58),
47 | surfaceTint = Color(0xff748bac),
48 | )
49 |
50 | private val LightColorScheme = lightColorScheme(
51 | primary = Color(0xff223a5e),
52 | onPrimary = Color(0xffffffff),
53 | primaryContainer = Color(0xff97baea),
54 | onPrimaryContainer = Color(0xff0d1013),
55 | secondary = Color(0xff144955),
56 | onSecondary = Color(0xffffffff),
57 | secondaryContainer = Color(0xffa9edff),
58 | onSecondaryContainer = Color(0xff0e1414),
59 | tertiary = Color(0xff208399),
60 | onTertiary = Color(0xffffffff),
61 | tertiaryContainer = Color(0xffccf3ff),
62 | onTertiaryContainer = Color(0xff111414),
63 | error = Color(0xffb00020),
64 | onError = Color(0xffffffff),
65 | errorContainer = Color(0xfffcd8df),
66 | onErrorContainer = Color(0xff141213),
67 | background = Color(0xfff8f9fa),
68 | onBackground = Color(0xff090909),
69 | surface = Color(0xfff8f9fa),
70 | onSurface = Color(0xff090909),
71 | surfaceVariant = Color(0xffe2e4e6),
72 | onSurfaceVariant = Color(0xff111112),
73 | outline = Color(0xff7c7c7c),
74 | outlineVariant = Color(0xffc8c8c8),
75 | scrim = Color(0xff000000),
76 | inverseSurface = Color(0xff111213),
77 | inversePrimary = Color(0xffaabbd5),
78 | surfaceTint = Color(0xff223a5e),
79 | )
80 |
81 | @Composable
82 | fun ReadTheme(
83 | darkTheme: Boolean = isSystemInDarkTheme(),
84 | content: @Composable () -> Unit
85 | ) {
86 | val state = hiltViewModel().state.collectAsState().value
87 |
88 | val colorScheme = when (state.theme) {
89 | AppTheme.Light -> LightColorScheme
90 | AppTheme.Dark -> DarkColorScheme
91 | AppTheme.System -> {
92 | if (darkTheme) DarkColorScheme else LightColorScheme
93 | }
94 | }
95 |
96 | val view = LocalView.current
97 | if (!view.isInEditMode) {
98 | SideEffect {
99 | val window = (view.context as Activity).window
100 | window.statusBarColor = colorScheme.background.toArgb()
101 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
102 | (!darkTheme && state.theme != AppTheme.Dark) || state.theme == AppTheme.Light
103 | }
104 | }
105 |
106 | MaterialTheme(
107 | colorScheme = colorScheme,
108 | typography = Typography,
109 | content = content,
110 | )
111 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.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.text.googlefonts.Font
8 | import androidx.compose.ui.text.googlefonts.GoogleFont
9 | import androidx.compose.ui.unit.sp
10 | import com.ifeanyi.read.R
11 |
12 | val provider = GoogleFont.Provider(
13 | providerAuthority = "com.google.android.gms.fonts",
14 | providerPackage = "com.google.android.gms",
15 | certificates = R.array.com_google_android_gms_fonts_certs
16 | )
17 |
18 | val font = GoogleFont("Sora")
19 |
20 | val regular = FontFamily(Font(googleFont = font, fontProvider = provider))
21 | val semiBold = FontFamily(Font(googleFont = font, fontProvider = provider))
22 | val bold = FontFamily(Font(googleFont = font, fontProvider = provider))
23 |
24 | val Typography = Typography(
25 | titleLarge = TextStyle(
26 | fontFamily = bold,
27 | fontWeight = FontWeight.Bold,
28 | fontSize = 28.sp,
29 | lineHeight = 28.sp,
30 | letterSpacing = 0.5.sp
31 | ),
32 | titleMedium = TextStyle(
33 | fontFamily = semiBold,
34 | fontWeight = FontWeight.SemiBold,
35 | fontSize = 25.sp,
36 | lineHeight = 25.sp,
37 | letterSpacing = 0.5.sp
38 | ),
39 | titleSmall = TextStyle(
40 | fontFamily = semiBold,
41 | fontWeight = FontWeight.SemiBold,
42 | fontSize = 22.sp,
43 | lineHeight = 22.sp,
44 | letterSpacing = 0.5.sp
45 | ),
46 | bodyLarge = TextStyle(
47 | fontFamily = regular,
48 | fontWeight = FontWeight.Normal,
49 | fontSize = 18.sp,
50 | lineHeight = 18.sp,
51 | letterSpacing = 0.5.sp
52 | ),
53 | bodyMedium = TextStyle(
54 | fontFamily = regular,
55 | fontWeight = FontWeight.Normal,
56 | fontSize = 16.sp,
57 | lineHeight = 16.sp,
58 | letterSpacing = 0.5.sp
59 | ),
60 | bodySmall = TextStyle(
61 | fontFamily = regular,
62 | fontWeight = FontWeight.Normal,
63 | fontSize = 14.sp,
64 | lineHeight = 14.sp,
65 | letterSpacing = 0.5.sp
66 | ),
67 | labelLarge = TextStyle(
68 | fontFamily = regular,
69 | fontWeight = FontWeight.Normal,
70 | fontSize = 14.sp,
71 | lineHeight = 14.sp,
72 | letterSpacing = 0.5.sp
73 | ),
74 | labelMedium = TextStyle(
75 | fontFamily = regular,
76 | fontWeight = FontWeight.Normal,
77 | fontSize = 12.sp,
78 | lineHeight = 12.sp,
79 | letterSpacing = 0.5.sp
80 | ),
81 | labelSmall = TextStyle(
82 | fontFamily = regular,
83 | fontWeight = FontWeight.Normal,
84 | fontSize = 10.sp,
85 | lineHeight = 10.sp,
86 | letterSpacing = 0.5.sp
87 | )
88 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.util
2 |
3 | object Constants {
4 | const val theme = "theme"
5 | const val displayStyle = "displayStyle"
6 | const val speechRate = "speechRate"
7 | const val voice = "voice"
8 | const val storeLink = "https://play.google.com/store/apps/details?id=com.ifeanyi.read"
9 | const val privacyLink = "https://read-web.web.app/#/privacy"
10 | const val termsLink = "https://read-web.web.app/#/terms"
11 | const val describeImagePrompt =
12 | "provide a detailed description of this image as if narrating it to someone who cannot see. Include details about the objects, colors, shapes, spatial relationships, and any relevant contextual information."
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/util/Extentions.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.util
2 |
3 | import android.app.Activity
4 | import android.content.ComponentName
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.pm.PackageManager
8 | import android.net.Uri
9 | import android.provider.OpenableColumns
10 | import com.ifeanyi.read.BuildConfig
11 | import com.ifeanyi.read.core.enums.ActivityType
12 | import java.net.URI
13 | import java.text.SimpleDateFormat
14 | import java.time.LocalDateTime
15 | import java.util.Date
16 | import java.util.Locale
17 |
18 |
19 | val Locale.flagEmoji: String?
20 | get() {
21 | return try {
22 | val firstLetter = Character.codePointAt(country, 0) - 0x41 + 0x1F1E6
23 | val secondLetter = Character.codePointAt(country, 1) - 0x41 + 0x1F1E6
24 | String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter))
25 | } catch (exception: Exception) {
26 | null
27 | }
28 | }
29 | val appVersion: String
30 | get() {
31 | return "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}"
32 | }
33 |
34 | fun Context.share(text: String) {
35 | val sendIntent = Intent(Intent.ACTION_SEND).apply {
36 | type = "text/plain"
37 | putExtra(Intent.EXTRA_TEXT, text)
38 | }
39 | val shareIntent = Intent.createChooser(sendIntent, null)
40 | this.startActivity(shareIntent, null)
41 | }
42 |
43 | fun Context.mailTo(to: String, subject: String) {
44 | val selectorIntent = Intent(Intent.ACTION_SENDTO)
45 | selectorIntent.setData(Uri.parse("mailto:"))
46 |
47 | val sendIntent = Intent(Intent.ACTION_SEND).apply {
48 | putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
49 | putExtra(Intent.EXTRA_SUBJECT, subject)
50 | selector = selectorIntent
51 | }
52 | val shareIntent = Intent.createChooser(sendIntent, null)
53 | this.startActivity(shareIntent)
54 | }
55 |
56 | fun Uri.getName(context: Context): String {
57 | val returnCursor = context.contentResolver.query(this, null, null, null, null)
58 | ?: return LocalDateTime.now().toString()
59 | val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
60 | returnCursor.moveToFirst()
61 | val fileName = returnCursor.getString(nameIndex)
62 | returnCursor.close()
63 | return fileName.split(".").first()
64 | }
65 |
66 | val String.trimUrl: String
67 | get() {
68 | return try {
69 | val host = URI(this).host
70 | val domain = if (host.startsWith("www.")) host.substring(4) else host
71 | return domain.split(".").first()
72 | } catch (ex: Exception) {
73 | ""
74 | }
75 | }
76 |
77 | val String.formatted: String
78 | get() {
79 | val first = this.replace(Regex("[*]"), "")
80 | return first.replace(Regex("[\\s+]"), " ")
81 | }
82 |
83 | fun Date.dwdm(locale: Locale): String {
84 | return SimpleDateFormat("E, d MMM", locale).format(this)
85 | }
86 |
87 | fun Activity.changeIcon(activityType: ActivityType) {
88 | ActivityType.entries.forEach {
89 | packageManager.setComponentEnabledSetting(
90 | ComponentName(
91 | this,
92 | "$packageName.app.${it.name}"
93 | ),
94 | if (it == activityType) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
95 | else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
96 | PackageManager.DONT_KILL_APP
97 | )
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ifeanyi/read/core/util/RoomConverters.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read.core.util
2 |
3 | import com.ifeanyi.read.app.data.models.NewFeature
4 | import androidx.room.TypeConverter
5 | import com.google.gson.Gson
6 | import com.google.gson.reflect.TypeToken
7 | import java.util.Date
8 |
9 | object RoomConverters {
10 | private val gson = Gson()
11 |
12 | @TypeConverter
13 | fun featuresFromJson(json: String): List {
14 | val type = object : TypeToken>() {}.type
15 | return gson.fromJson(json, type)
16 | }
17 |
18 | @TypeConverter
19 | fun featuresToJson(features: List): String {
20 | return gson.toJson(features)
21 | }
22 |
23 | @TypeConverter
24 | fun rangeFromJson(json: String): IntRange {
25 | val type = object : TypeToken() {}.type
26 | return gson.fromJson(json, type)
27 | }
28 |
29 | @TypeConverter
30 | fun rangeToJson(range: IntRange): String {
31 | return gson.toJson(range)
32 | }
33 |
34 | @TypeConverter
35 | fun dateFromTimeStamp(timestamp: Long?): Date? {
36 | return if (timestamp == null) null else Date(timestamp)
37 | }
38 |
39 | @TypeConverter
40 | fun dateToTimestamp(date: Date?): Long? {
41 | return date?.time
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_purple_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_red_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_white_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/purple_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/drawable/purple_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/red_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/drawable/red_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_forward_10_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_pause_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_play_arrow_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_record_voice_over_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_replay_10_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/white_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/drawable/white_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_purple_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_red_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_white_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_purple.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_purple.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_purple_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_purple_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_purple_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_purple_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_red.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_red.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_red_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_red_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_red_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_red_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_white.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_white_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_white_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_white_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-hdpi/ic_launcher_white_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_purple.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_purple.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_purple_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_purple_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_purple_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_purple_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_red.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_red.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_red_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_red_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_red_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_red_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_white.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_white_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_white_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_white_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-mdpi/ic_launcher_white_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_purple.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_purple.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_purple_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_purple_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_purple_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_purple_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_red.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_red.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_red_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_red_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_red_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_red_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_white.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_white_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_white_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_white_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xhdpi/ic_launcher_white_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_purple.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_purple.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_purple_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_purple_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_purple_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_purple_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_red.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_red.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_red_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_red_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_red_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_red_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_white.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_white_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_white_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_white_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxhdpi/ic_launcher_white_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_purple.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_purple.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_purple_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_purple_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_purple_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_purple_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_red.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_red.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_red_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_red_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_red_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_red_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_white.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_white_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_white_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_white_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/app/src/main/res/mipmap-xxxhdpi/ic_launcher_white_round.webp
--------------------------------------------------------------------------------
/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/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - @array/com_google_android_gms_fonts_certs_dev
6 | - @array/com_google_android_gms_fonts_certs_prod
7 |
8 |
9 | -
10 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
11 |
12 |
13 |
14 | -
15 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Read
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/android/ifeanyi/read/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.ifeanyi.read
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.2.1" apply false
4 | // Kotlin
5 | id("org.jetbrains.kotlin.android") version "1.9.20" apply false
6 | // Hilt
7 | id("com.google.dagger.hilt.android") version "2.48" apply false
8 | // Read stuff from local.properties
9 | id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false
10 | }
11 |
--------------------------------------------------------------------------------
/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 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-ifeanyi/read_kotlin/cb173da3f95827138be88113206fd92abaf5a707/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jan 20 23:07:26 WAT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/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 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Read"
17 | include(":app")
18 |
--------------------------------------------------------------------------------