(ResolverImpl.kt:152)
4 | at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:231)
5 | at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
6 | at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
7 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$7(KotlinToJVMBytecodeCompiler.kt:326)
8 | at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
9 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:317)
10 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:154)
11 | at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:75)
12 | at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:167)
13 | at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:36)
14 | at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:113)
15 | at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:337)
16 | at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1700)
17 | at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
18 | at java.base/java.lang.reflect.Method.invoke(Unknown Source)
19 | at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
20 | at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
21 | at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
22 | at java.base/java.security.AccessController.doPrivileged(Unknown Source)
23 | at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
24 | at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
25 | at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
26 | at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
27 | at java.base/java.security.AccessController.doPrivileged(Unknown Source)
28 | at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
29 | at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
30 | at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
31 | at java.base/java.lang.Thread.run(Unknown Source)
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | uRead
3 |
4 | ---
5 |
6 |
7 | An Ebook and AudioBook reader for Android supporting Epub and PDF books, implemented in a clean and minimalistic UI in Material You style.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ---
18 |
19 | ## 👀 Overview
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ---
32 |
33 | ## Features
34 |
35 | - Support for EPUB format
36 | - Material You design
37 | - Color picker functionality
38 | - Room database integration for local storage
39 | - Jetpack Compose UI
40 | - Hilt dependency injection
41 | - Coil for image loading
42 | - Datastore for preferences
43 | - Paging support
44 |
45 |
46 | ---
47 |
48 |
49 | ## Feature Request
50 |
51 | To request a feature for the app, [add a new issue](https://github.com/Rics-Dev/uRead/issues/new) with the label "feature"
52 |
53 | ---
54 |
55 | ## License
56 |
57 | [](https://www.gnu.org/licenses/gpl-3.0.en.html)
58 |
59 | This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
60 |
61 | ---
62 |
63 | ## Acknowledgments
64 |
65 | - [Readium](https://readium.org/) for their e-book toolkit
66 | - [Skydoves](https://github.com/skydoves) for the ColorPicker Compose library
67 | - [Shivamdhuria](https://github.com/Shivamdhuria) for the Palette library
68 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/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
22 |
23 | -dontwarn android.media.LoudnessCodecController
24 | -dontwarn android.media.LoudnessCodecController$OnLoudnessCodecUpdateListener
25 |
26 |
27 | ############################# Retrofit For ketch ################################
28 | #
29 | ## Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
30 | ## EnclosingMethod is required to use InnerClasses.
31 | #-keepattributes Signature, InnerClasses, EnclosingMethod
32 | #
33 | ## Retrofit does reflection on method and parameter annotations.
34 | #-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
35 | #
36 | ## Keep annotation default values (e.g., retrofit2.http.Field.encoded).
37 | #-keepattributes AnnotationDefault
38 | #
39 | ## Retain service method parameters when optimizing.
40 | #-keepclassmembers,allowshrinking,allowobfuscation interface * {
41 | # @retrofit2.http.* ;
42 | #}
43 | #
44 | ## Ignore annotation used for build tooling.
45 | #-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
46 | #
47 | ## Ignore JSR 305 annotations for embedding nullability information.
48 | #-dontwarn javax.annotation.**
49 | #
50 | ## Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
51 | #-dontwarn kotlin.Unit
52 | #
53 | ## Top-level functions that can only be used by Kotlin.
54 | #-dontwarn retrofit2.KotlinExtensions
55 | #-dontwarn retrofit2.KotlinExtensions$*
56 | #
57 | ## With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
58 | ## and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
59 | #-if interface * { @retrofit2.http.* ; }
60 | #-keep,allowobfuscation interface <1>
61 | #
62 | ## Keep inherited services.
63 | #-if interface * { @retrofit2.http.* ; }
64 | #-keep,allowobfuscation interface * extends <1>
65 | #
66 | ## With R8 full mode generic signatures are stripped for classes that are not
67 | ## kept. Suspend functions are wrapped in continuations where the type argument
68 | ## is used.
69 | #-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
70 | #
71 | ## R8 full mode strips generic signatures from return types if not kept.
72 | #-if interface * { @retrofit2.http.* public *** *(...); }
73 | #-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
74 | #
75 | ## With R8 full mode generic signatures are stripped for classes that are not kept.
76 | #-keep,allowobfuscation,allowshrinking class retrofit2.Response
77 | #
78 | #
79 | ############################# Retrofit For ketch ################################
--------------------------------------------------------------------------------
/app/release/app-release.aab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/release/app-release.aab
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/ricdev/uread/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.uread", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
40 |
41 |
42 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/assets/annotation-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/assets/books/alice_in_wonderlands.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/assets/books/alice_in_wonderlands.epub
--------------------------------------------------------------------------------
/app/src/main/assets/books/romeo_and_juliet.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/assets/books/romeo_and_juliet.epub
--------------------------------------------------------------------------------
/app/src/main/assets/broken-crown.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/src/main/assets/crown.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/src/main/assets/documentation/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 |
9 | ## [1.0.0](https://github.com/Rics-Dev/uRead/releases/tag/v1.0.0)
10 |
11 | ### Added
12 | - Initial Release for the App
13 |
14 | [Unreleased]: https://github.com/Rics-Dev/uRead/compare/v1.0.0...HEAD
15 | [1.0.0]: https://github.com/Rics-Dev/uRead/releases/tag/v1.0.0
--------------------------------------------------------------------------------
/app/src/main/assets/documentation/PRIVACY_POLICY.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | *Effective Date: 26-08-2024*
4 |
5 |
6 | Welcome to uRead. This Privacy Policy is designed to help you understand how we handle policies with the collection,
7 | use, and disclosure of Personal Information if anyone decided to use our App.
8 |
9 | ## Information We Collect and use
10 |
11 | uRead is designed with your privacy in mind. We do not directly collect or store any personal information from our users. However:
12 |
13 | - The app requires access to your device's storage to retrieve locally stored book files.
14 | - We store user preferences locally on your device using data stores.
15 | - Our third-party service providers may collect certain information as described below.
16 |
17 | ## Third-Party Services
18 |
19 | ### a) Google AdMob
20 | We use Google AdMob to display advertisements in uRead. Google AdMob may collect and process certain data to provide this service. Please refer to Google's Privacy Policy for more information on their data practices.
21 |
22 | ### b) Google Play Billing
23 | For in-app purchases, we use Google Play Billing API. All transaction data is handled directly by Google. We do not have access to your payment information. Please refer to Google's Privacy Policy for details on how they handle this data.
24 |
25 | ## Use of Information
26 |
27 | We do not collect or use any personal information directly. The app accesses your device storage solely to provide the core functionality of reading ebooks stored on your device.
28 |
29 | ## Data Sharing
30 |
31 | We do not share any user data with third parties, as we do not collect any.
32 |
33 | ## Data Security
34 |
35 | We implement appropriate technical and organizational measures to protect the information that is stored locally on your device, such as your reading preferences.
36 |
37 | ## Children's Privacy
38 |
39 | uRead is not intended for use by children under the age of 13. We do not knowingly collect personal information from children under 13.
40 |
41 | ## Changes to This Privacy Policy
42 |
43 | We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Effective Date" at the top.
44 |
45 | ## Contact Us
46 |
47 | If you have any questions or suggestions about this Privacy Policy, do not hesitate to reach out to us on:
48 |
49 | Email: ricdev.io@gmail.com
--------------------------------------------------------------------------------
/app/src/main/assets/github.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/BookApplication.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class BookApplication : Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.activity.result.ActivityResult
10 | import androidx.activity.result.contract.ActivityResultContracts
11 | import androidx.activity.viewModels
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.compose.runtime.getValue
14 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
15 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
16 | import androidx.navigation.compose.rememberNavController
17 | import com.google.android.gms.ads.MobileAds
18 | import com.ricdev.uread.data.model.AppLanguage
19 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
20 | import com.ricdev.uread.ui.theme.UReadTheme
21 | import com.ricdev.uread.navigation.SetupNavGraph
22 | import com.ricdev.uread.util.LanguageHelper
23 | import com.ricdev.uread.util.PurchaseHelper
24 | import dagger.hilt.android.AndroidEntryPoint
25 | import kotlinx.coroutines.CoroutineScope
26 | import kotlinx.coroutines.Dispatchers
27 | import kotlinx.coroutines.launch
28 |
29 | @AndroidEntryPoint
30 | class MainActivity : AppCompatActivity() {
31 |
32 |
33 | val viewModel: SplashViewModel by viewModels()
34 |
35 | // experimental
36 | private val languageHelper = LanguageHelper()
37 |
38 |
39 | override fun attachBaseContext(newBase: Context) {
40 | super.attachBaseContext(newBase)
41 | }
42 |
43 | override fun onCreate(savedInstanceState: Bundle?) {
44 | super.onCreate(savedInstanceState)
45 | val splashScreen = installSplashScreen()
46 | enableEdgeToEdge()
47 |
48 | val initialLanguage = AppLanguage.fromCode(AppPreferencesUtil.defaultPreferences.language)
49 | languageHelper.updateBaseContextLocale(this, initialLanguage)
50 |
51 | // Keep splash screen visible until loading is complete
52 | splashScreen.setKeepOnScreenCondition {
53 | viewModel.isLoading.value
54 | }
55 |
56 | // Initialize billing first
57 | val purchaseHelper = PurchaseHelper(this)
58 | purchaseHelper.billingSetup()
59 |
60 | // Initialize ads in background
61 | CoroutineScope(Dispatchers.IO).launch {
62 | MobileAds.initialize(this@MainActivity)
63 | }
64 |
65 |
66 |
67 | setContent {
68 | val screen by viewModel.startDestination.collectAsStateWithLifecycle()
69 |
70 |
71 | UReadTheme {
72 | val navController = rememberNavController()
73 |
74 | screen?.let {
75 | SetupNavGraph(
76 | navController = navController,
77 | startDestination = it,
78 | purchaseHelper = purchaseHelper,
79 | )
80 | }
81 | }
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.lifecycle.AndroidViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.ricdev.uread.data.model.AppLanguage
8 | import com.ricdev.uread.data.model.AppPreferences
9 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
10 | import com.ricdev.uread.navigation.Screens
11 | import com.ricdev.uread.util.LanguageHelper
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.asStateFlow
16 | import kotlinx.coroutines.flow.first
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class SplashViewModel @Inject constructor(
22 | private val appPreferencesUtil: AppPreferencesUtil,
23 | private val languageHelper: LanguageHelper,
24 | application: Application,
25 | ) : AndroidViewModel(application) {
26 |
27 |
28 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
29 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
30 |
31 | private val _startDestination = MutableStateFlow(null)
32 | val startDestination: StateFlow = _startDestination.asStateFlow()
33 |
34 | private val _isLoading = MutableStateFlow(true)
35 | val isLoading: StateFlow = _isLoading.asStateFlow()
36 |
37 |
38 | init {
39 | viewModelScope.launch {
40 | try {
41 | val initialPreferences = appPreferencesUtil.appPreferencesFlow.first()
42 | Log.d("SplashViewModel", "Initial preferences: $initialPreferences")
43 | _appPreferences.value = initialPreferences
44 | languageHelper.changeLanguage(
45 | getApplication(),
46 | AppLanguage.fromCode(initialPreferences.language)
47 | )
48 | determineStartDestination(initialPreferences)
49 | } catch (e: Exception) {
50 | Log.e("SplashViewModel", "Initialization error", e)
51 | }
52 | }
53 | }
54 |
55 |
56 |
57 |
58 | private fun determineStartDestination(prefs: AppPreferences) {
59 | _startDestination.value = if (prefs.isFirstLaunch) {
60 | Screens.GettingStartedScreen.route
61 | } else {
62 | Screens.HomeScreen.route
63 | }
64 | _isLoading.value = false
65 | }
66 | }
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/AppLanguage.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | enum class AppLanguage(val code: String, val displayName: String) {
4 | SYSTEM("system", "System Default"),
5 | ENGLISH("en", "English"),
6 | SWEDISH("sv", "Svenska"),
7 | FRENCH("fr", "Français"),
8 | GERMAN("de", "Deutsch"),
9 | DUTCH("nl", "Nederlands"),
10 | ITALIAN("it", "Italiano"),
11 | SPANISH("es", "Español"),
12 | PORTUGUESE("pt", "Português"),
13 | TURKISH("tr", "Türkçe"),
14 | CHINESE("zh", "中文"),
15 | JAPANESE("ja", "日本語"),
16 | KOREAN("ko", "한국어"),
17 | RUSSIAN("ru", "Русский"),
18 | ARABIC("ar", "العربية"),
19 | HINDI("hi", "हिन्दी");
20 |
21 | companion object {
22 | fun fromCode(code: String): AppLanguage =
23 | entries.find { it.code == code } ?: SYSTEM
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/AppPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | data class AppPreferences(
4 | //App Settings
5 | val isFirstLaunch: Boolean,
6 | val isAssetsBooksFetched: Boolean,
7 | val scanDirectories: Set,
8 | val enablePdfSupport: Boolean,
9 | val language: String,
10 |
11 |
12 |
13 | //Ui settings
14 | val appTheme: AppTheme,
15 | val colorScheme: String,
16 | val homeLayout: Layout,
17 | val homeBackgroundImage: String,
18 | val gridCount: Int,
19 | val showEntries: Boolean,
20 | val showRating: Boolean,
21 | val showReadingStatus: Boolean,
22 | val showReadingDates: Boolean,
23 | val showPdfLabel: Boolean,
24 |
25 |
26 | val sortBy: SortOption,
27 | val sortOrder: SortOrder,
28 |
29 |
30 |
31 | val readingStatus: Set = emptySet(),
32 | val fileTypes: Set = emptySet(),
33 |
34 |
35 |
36 | // premium unlocked
37 | val isPremium: Boolean
38 | )
39 |
40 |
41 | enum class SortOption {
42 | TITLE,
43 | AUTHOR,
44 | LAST_OPENED,
45 | LAST_ADDED,
46 | RATING,
47 | PROGRESSION,
48 | }
49 |
50 |
51 | enum class SortOrder {
52 | ASCENDING,
53 | DESCENDING
54 | }
55 |
56 |
57 | enum class Layout {
58 | Grid,
59 | CoverOnly,
60 | List,
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/AppTheme.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | enum class AppTheme {
4 | SYSTEM,
5 | LIGHT,
6 | DARK,
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/Book.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "books")
7 | data class Book(
8 | @PrimaryKey(autoGenerate = true)
9 | val id: Long = 0,
10 | val uri: String,
11 | val fileType: FileType,
12 | val title: String,
13 | val authors: String,
14 | val description: String?,
15 | val publishDate: String?, // New: Publication date
16 | val publisher: String?, // New: Publisher
17 | val language: String?, // New: Primary language
18 | val numberOfPages: Int?, // New: Total number of pages
19 | val subjects: String?, // New: Categories or genres
20 | val coverPath: String?,
21 | val locator: String,
22 | val progression: Float = 0f, // reading progression in %
23 | val lastOpened: Long? = null, // timestamp of the last time the book was opened
24 | val deleted: Boolean = false, // flag to mark the book as deleted
25 | val rating: Float = 0f, // rating of the book
26 | val isFavorite: Boolean = false, // flag to mark the book as favorite
27 | val readingStatus: ReadingStatus? = ReadingStatus.NOT_STARTED, // reading status of the book
28 | val readingTime: Long = 0, // total time spent reading the book in milliseconds
29 | val startReadingDate: Long? = null, // timestamp of when the user started reading the book
30 | val endReadingDate: Long? = null, // timestamp of when the user finished reading the book
31 | val review: String? = null,
32 | val duration: Long? = null, // Total duration of the audiobook in milliseconds
33 | val narrator: String? = null, // Name of the audiobook narrator
34 | )
35 |
36 | enum class FileType {
37 | EPUB,
38 | PDF,
39 | AUDIOBOOK
40 | }
41 |
42 | enum class ReadingStatus {
43 | NOT_STARTED,
44 | IN_PROGRESS,
45 | FINISHED
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/BookAnnotation.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.ForeignKey
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 |
8 | @Entity(
9 | tableName = "annotations",
10 | foreignKeys = [ForeignKey(
11 | entity = Book::class,
12 | parentColumns = ["id"],
13 | childColumns = ["bookId"],
14 | onDelete = ForeignKey.CASCADE
15 | )],
16 | indices = [Index(value = ["bookId"])]
17 | )
18 | data class BookAnnotation(
19 | @PrimaryKey(autoGenerate = true)
20 | val id: Long = 0,
21 | val bookId: Long,
22 | val locator: String,
23 | val color: String,
24 | val note: String?,
25 | val type: AnnotationType
26 | )
27 |
28 | enum class AnnotationType {
29 | HIGHLIGHT,
30 | UNDERLINE
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/BookShelf.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.ForeignKey
5 | import androidx.room.Index
6 |
7 | @Entity(
8 | tableName = "book_shelf",
9 | primaryKeys = ["bookId", "shelfId"],
10 | foreignKeys = [
11 | ForeignKey(
12 | entity = Book::class,
13 | parentColumns = ["id"],
14 | childColumns = ["bookId"],
15 | onDelete = ForeignKey.CASCADE
16 | ),
17 | ForeignKey(
18 | entity = Shelf::class,
19 | parentColumns = ["id"],
20 | childColumns = ["shelfId"],
21 | onDelete = ForeignKey.CASCADE
22 | )
23 | ],
24 | indices = [Index(value = ["bookId"]), Index(value = ["shelfId"])]
25 | )
26 | data class BookShelf(
27 | val bookId: Long,
28 | val shelfId: Long
29 | )
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/Bookmark.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.ForeignKey
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 |
8 | @Entity(
9 | tableName = "bookmarks",
10 | foreignKeys = [ForeignKey(
11 | entity = Book::class,
12 | parentColumns = ["id"],
13 | childColumns = ["bookId"],
14 | onDelete = ForeignKey.CASCADE
15 | )],
16 | indices = [Index(value = ["bookId"])]
17 | )
18 | data class Bookmark (
19 | @PrimaryKey(autoGenerate = true)
20 | val id: Long = 0,
21 | val bookId: Long,
22 | val locator: String,
23 | val dateAndTime: Long,
24 | val color: String? = null,
25 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/Note.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.ForeignKey
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 |
8 |
9 | @Entity(
10 | tableName = "notes",
11 | foreignKeys = [ForeignKey(
12 | entity = Book::class,
13 | parentColumns = ["id"],
14 | childColumns = ["bookId"],
15 | onDelete = ForeignKey.CASCADE
16 | )],
17 | indices = [Index(value = ["bookId"])]
18 | )
19 | data class Note(
20 | @PrimaryKey(autoGenerate = true)
21 | val id: Long = 0,
22 | val locator: String,
23 | val selectedText: String,
24 | val note: String,
25 | val color: String,
26 | val bookId: Long
27 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/ReaderPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.toArgb
5 | import org.readium.r2.navigator.epub.EpubPreferences
6 | import org.readium.r2.navigator.preferences.ReadingProgression
7 | import org.readium.r2.navigator.preferences.TextAlign
8 | import org.readium.r2.shared.ExperimentalReadiumApi
9 | import org.readium.r2.navigator.preferences.Color as ReadiumColor
10 |
11 |
12 | data class ReaderPreferences @OptIn(ExperimentalReadiumApi::class) constructor(
13 | //Font Settings
14 | val fontSize: Double,
15 | val letterSpacing: Double,
16 | val lineHeight: Double,
17 | val pageMargins: Double,
18 | val paragraphIndent: Double,
19 | val paragraphSpacing: Double,
20 | val wordSpacing: Double,
21 | val textAlign: TextAlign,
22 | //ui Settings
23 | val backgroundColor: Color,
24 | val textColor: Color,
25 | val colorHistory: List = emptyList(),
26 | //Reader Settings
27 | val keepScreenOn: Boolean,
28 | val tapNavigation: Boolean,
29 | val scroll: Boolean,
30 | val readingProgression: ReadingProgression,
31 | val verticalText: Boolean,
32 | val publisherStyles: Boolean,
33 | val textNormalization: Boolean,
34 | )
35 |
36 | // Extension function to convert ReaderPreferences to EpubPreferences
37 | @OptIn(ExperimentalReadiumApi::class)
38 | fun ReaderPreferences.toEpubPreferences(): EpubPreferences {
39 | return EpubPreferences(
40 | fontSize = this.fontSize,
41 | // fontWeight = this.fontWeight,
42 | letterSpacing = this.letterSpacing,
43 | lineHeight = this.lineHeight,
44 | pageMargins = this.pageMargins,
45 | paragraphIndent = this.paragraphIndent,
46 | paragraphSpacing = this.paragraphSpacing,
47 | wordSpacing = this.wordSpacing,
48 | textAlign = this.textAlign,
49 | //ui Settings
50 | backgroundColor = ReadiumColor(this.backgroundColor.toArgb()),
51 | textColor = ReadiumColor(this.textColor.toArgb()),
52 | //Reader Settings
53 | scroll = this.scroll,
54 | readingProgression = this.readingProgression,
55 | verticalText = this.verticalText,
56 | publisherStyles = this.publisherStyles,
57 | textNormalization = this.textNormalization,
58 | )
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/ReadingActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 |
7 | @Entity(tableName = "reading_activities")
8 | data class ReadingActivity(
9 | @PrimaryKey val date: Long, // Date in milliseconds
10 | val readingTime: Long, // Reading time in milliseconds
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/model/Shelf.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "shelves")
7 | data class Shelf(
8 | @PrimaryKey(autoGenerate = true)
9 | val id: Long = 0,
10 | val name: String,
11 | val order: Int
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/repository/ShelfRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.repository
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.data.model.BookShelf
5 | import com.ricdev.uread.data.model.Shelf
6 | import com.ricdev.uread.data.source.local.dao.BookDao
7 | import com.ricdev.uread.data.source.local.dao.BookShelfDao
8 | import com.ricdev.uread.data.source.local.dao.ShelfDao
9 | import com.ricdev.uread.domain.repository.ShelfRepository
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.flow
13 | import kotlinx.coroutines.flow.flowOn
14 | import kotlinx.coroutines.withContext
15 | import javax.inject.Inject
16 |
17 | class ShelfRepositoryImpl @Inject constructor(
18 | private val shelfDao: ShelfDao,
19 | private val bookShelfDao: BookShelfDao,
20 | private val bookDao: BookDao
21 | ) : ShelfRepository {
22 |
23 | override fun getShelves(): Flow> = shelfDao.getAllShelves().flowOn(Dispatchers.IO)
24 |
25 | override suspend fun getShelfById(shelfId: Long): Shelf? = withContext(Dispatchers.IO) {
26 | shelfDao.getShelfById(shelfId)
27 | }
28 |
29 | override suspend fun addShelf(shelf: Shelf): Long = withContext(Dispatchers.IO) {
30 | shelfDao.insert(shelf)
31 | }
32 |
33 | override suspend fun updateShelf(shelf: Shelf) = withContext(Dispatchers.IO) {
34 | shelfDao.update(shelf)
35 | }
36 |
37 | override suspend fun deleteShelf(shelf: Shelf) = withContext(Dispatchers.IO) {
38 | shelfDao.delete(shelf)
39 | }
40 |
41 | override suspend fun addBookToShelf(bookId: Long, shelfId: Long) = withContext(Dispatchers.IO) {
42 | bookShelfDao.insert(BookShelf(bookId, shelfId))
43 | }
44 |
45 | override suspend fun removeBookFromShelf(bookId: Long, shelfId: Long) = withContext(Dispatchers.IO) {
46 | bookShelfDao.delete(BookShelf(bookId, shelfId))
47 | }
48 |
49 | override fun getBooksForShelf(shelfId: Long): Flow> = flow {
50 | val bookIds = bookShelfDao.getBooksForShelf(shelfId).map { it.bookId }
51 | emit(bookDao.getBooksByIds(bookIds))
52 | }.flowOn(Dispatchers.IO)
53 |
54 | override fun getShelvesForBook(bookId: Long): Flow> = flow {
55 | val shelfIds = bookShelfDao.getShelvesForBook(bookId).map { it.shelfId }
56 | emit(shelfDao.getShelfsByIds(shelfIds))
57 | }.flowOn(Dispatchers.IO)
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.ricdev.uread.data.model.Book
6 | import com.ricdev.uread.data.model.BookAnnotation
7 | import com.ricdev.uread.data.model.BookShelf
8 | import com.ricdev.uread.data.model.Bookmark
9 | import com.ricdev.uread.data.model.Note
10 | import com.ricdev.uread.data.model.ReadingActivity
11 | import com.ricdev.uread.data.model.Shelf
12 | import com.ricdev.uread.data.source.local.dao.AnnotationDao
13 | import com.ricdev.uread.data.source.local.dao.BookDao
14 | import com.ricdev.uread.data.source.local.dao.BookShelfDao
15 | import com.ricdev.uread.data.source.local.dao.BookmarkDao
16 | import com.ricdev.uread.data.source.local.dao.NoteDao
17 | import com.ricdev.uread.data.source.local.dao.ReadingActivityDao
18 | import com.ricdev.uread.data.source.local.dao.ShelfDao
19 |
20 | @Database(
21 | entities = [
22 | Book::class,
23 | BookAnnotation::class,
24 | Note::class,
25 | Bookmark::class,
26 | Shelf::class,
27 | BookShelf::class,
28 | ReadingActivity::class
29 | ],
30 | version = 1,
31 | exportSchema = true,
32 | )
33 | abstract class AppDatabase : RoomDatabase() {
34 | abstract fun bookDao(): BookDao
35 | abstract fun annotationDao(): AnnotationDao
36 | abstract fun noteDao(): NoteDao
37 | abstract fun bookmarkDao(): BookmarkDao
38 | abstract fun shelfDao(): ShelfDao
39 | abstract fun bookShelfDao(): BookShelfDao
40 | abstract fun readingActivityDao(): ReadingActivityDao
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/AnnotationDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import androidx.room.Update
9 | import com.ricdev.uread.data.model.BookAnnotation
10 | import kotlinx.coroutines.flow.Flow
11 |
12 |
13 | @Dao
14 | interface AnnotationDao {
15 | @Query("SELECT * FROM annotations")
16 | fun getAllAnnotations(): Flow>
17 |
18 |
19 | @Insert(onConflict = OnConflictStrategy.REPLACE)
20 | suspend fun insert(annotation: BookAnnotation): Long
21 |
22 | @Update
23 | suspend fun update(annotation: BookAnnotation)
24 |
25 | @Delete
26 | suspend fun delete(annotation: BookAnnotation)
27 |
28 | @Query("SELECT * FROM annotations WHERE bookId = :bookId")
29 | fun getAnnotationsForBook(bookId: Long): Flow>
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/BookDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.paging.PagingSource
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.Transaction
10 | import androidx.room.Update
11 | import com.ricdev.uread.data.model.Book
12 | import com.ricdev.uread.data.model.FileType
13 | import com.ricdev.uread.data.model.ReadingStatus
14 | import kotlinx.coroutines.flow.Flow
15 |
16 | @Dao
17 | interface BookDao {
18 | @Query("SELECT * FROM books WHERE deleted = 0")
19 | fun getAllBooks(): Flow>
20 |
21 |
22 | @Query(
23 | """
24 | SELECT * FROM books
25 | WHERE deleted = 0
26 | AND (:readingStatuses IS NULL OR readingStatus IN (:readingStatuses))
27 | AND (:fileTypes IS NULL OR fileType IN (:fileTypes))
28 | ORDER BY
29 | CASE WHEN :sortBy = 'last_opened' AND :isAsc = 1 THEN lastOpened END ASC,
30 | CASE WHEN :sortBy = 'last_opened' AND :isAsc = 0 THEN lastOpened END DESC,
31 | CASE WHEN :sortBy = 'last_added' AND :isAsc = 1 THEN id END ASC,
32 | CASE WHEN :sortBy = 'last_added' AND :isAsc = 0 THEN id END DESC,
33 | CASE WHEN :sortBy = 'title' AND :isAsc = 1 THEN title END ASC,
34 | CASE WHEN :sortBy = 'title' AND :isAsc = 0 THEN title END DESC,
35 | CASE WHEN :sortBy = 'author' AND :isAsc = 1 THEN authors END ASC,
36 | CASE WHEN :sortBy = 'author' AND :isAsc = 0 THEN authors END DESC,
37 | CASE WHEN :sortBy = 'rating' AND :isAsc = 1 THEN rating END ASC,
38 | CASE WHEN :sortBy = 'rating' AND :isAsc = 0 THEN rating END DESC,
39 | CASE WHEN :sortBy = 'progression' AND :isAsc = 1 THEN progression END ASC,
40 | CASE WHEN :sortBy = 'progression' AND :isAsc = 0 THEN progression END DESC
41 | """
42 | )
43 | fun getAllBooksSorted(
44 | sortBy: String,
45 | isAsc: Boolean,
46 | readingStatuses: List?,
47 | fileTypes: List?
48 | ): PagingSource
49 |
50 |
51 | @Query("SELECT * FROM books WHERE deleted = 1")
52 | fun getDeletedBooks(): Flow>
53 |
54 |
55 | @Query("SELECT uri FROM books")
56 | suspend fun getAllBookUris(): List
57 |
58 |
59 | @Query("SELECT * FROM books WHERE uri = :uri")
60 | fun getBookByUri(uri: String): Book?
61 |
62 | @Query("SELECT * FROM books WHERE id = :bookId")
63 | fun getBookById(bookId: Long): Book?
64 |
65 | @Query("SELECT * FROM books WHERE id IN (:bookIds)")
66 | suspend fun getBooksByIds(bookIds: List): List
67 |
68 |
69 |
70 |
71 | @Insert(onConflict = OnConflictStrategy.REPLACE)
72 | fun insertBook(books: Book)
73 |
74 |
75 | @Transaction
76 | @Update
77 | suspend fun update(book: Book)
78 |
79 | @Delete
80 | suspend fun delete(book: Book)
81 |
82 |
83 | @Query("DELETE FROM books WHERE uri = :bookUri")
84 | fun deleteBookByUri(bookUri: String)
85 |
86 |
87 |
88 |
89 | @Query("SELECT locator FROM books WHERE id = :bookId")
90 | fun getReadingProgress(bookId: Long): String
91 |
92 | @Query("UPDATE books SET locator = :locator, progression = :progression WHERE id = :bookId")
93 | fun setReadingProgress(bookId: Long, locator: String, progression: Float)
94 |
95 |
96 | @Query("UPDATE books SET readingStatus = :status WHERE id = :bookId")
97 | suspend fun setReadingStatus(bookId: Long, status: ReadingStatus)
98 |
99 |
100 |
101 |
102 |
103 |
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/BookShelfDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.room.*
4 | import com.ricdev.uread.data.model.BookShelf
5 |
6 | @Dao
7 | interface BookShelfDao {
8 | @Insert(onConflict = OnConflictStrategy.REPLACE)
9 | suspend fun insert(bookShelf: BookShelf)
10 |
11 | @Delete
12 | suspend fun delete(bookShelf: BookShelf)
13 |
14 | @Query("SELECT * FROM book_shelf WHERE bookId = :bookId")
15 | suspend fun getShelvesForBook(bookId: Long): List
16 |
17 | @Query("SELECT * FROM book_shelf WHERE shelfId = :shelfId")
18 | suspend fun getBooksForShelf(shelfId: Long): List
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/BookmarkDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.ricdev.uread.data.model.Bookmark
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | @Dao
12 | interface BookmarkDao {
13 | @Query("SELECT * FROM bookmarks")
14 | fun getAllBookmarks(): Flow>
15 |
16 |
17 | @Insert
18 | suspend fun insert(bookmark: Bookmark)
19 |
20 | @Update
21 | suspend fun update(bookmark: Bookmark)
22 |
23 | @Delete
24 | suspend fun delete(bookmark: Bookmark)
25 |
26 | @Query("SELECT * FROM bookmarks WHERE bookId = :bookId")
27 | fun getBookmarksForBook(bookId: Long): Flow>
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/NoteDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.ricdev.uread.data.model.Note
9 | import kotlinx.coroutines.flow.Flow
10 |
11 |
12 | @Dao
13 | interface NoteDao {
14 | @Query("SELECT * FROM notes")
15 | fun getAllNotes(): Flow>
16 |
17 |
18 | @Insert
19 | suspend fun insert(note: Note)
20 |
21 | @Update
22 | suspend fun update(note: Note)
23 |
24 | @Delete
25 | suspend fun delete(note: Note)
26 |
27 | @Query("SELECT * FROM notes WHERE bookId = :bookId")
28 | fun getNotesForBook(bookId: Long): Flow>
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/ReadingActivityDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.ricdev.uread.data.model.ReadingActivity
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface ReadingActivityDao {
12 |
13 |
14 | @Insert(onConflict = OnConflictStrategy.REPLACE)
15 | suspend fun insertOrUpdate(readingActivity: ReadingActivity)
16 |
17 | @Query("SELECT * FROM reading_activities WHERE date = :date")
18 | suspend fun getReadingActivityByDate(date: Long): ReadingActivity?
19 |
20 | @Query("SELECT SUM(readingTime) FROM reading_activities")
21 | suspend fun getTotalReadingTime(): Long?
22 |
23 |
24 | @Query("SELECT * FROM reading_activities")
25 | fun getAllReadingActivities(): Flow>
26 |
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/data/source/local/dao/ShelfDao.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.data.source.local.dao
2 |
3 | import androidx.room.*
4 | import com.ricdev.uread.data.model.Shelf
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | @Dao
8 | interface ShelfDao {
9 | @Insert(onConflict = OnConflictStrategy.REPLACE)
10 | suspend fun insert(shelf: Shelf): Long
11 |
12 | @Update
13 | suspend fun update(shelf: Shelf)
14 |
15 | @Delete
16 | suspend fun delete(shelf: Shelf)
17 |
18 | @Query("SELECT * FROM shelves ORDER BY `order` ASC")
19 | fun getAllShelves(): Flow>
20 |
21 | @Query("SELECT * FROM shelves WHERE id = :shelfId")
22 | suspend fun getShelfById(shelfId: Long): Shelf?
23 |
24 | @Query("SELECT * FROM shelves WHERE id IN (:shelfIds)")
25 | suspend fun getShelfsByIds(shelfIds: List): List
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/di/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.di
2 |
3 | //import android.app.Activity
4 | //import android.content.Context
5 | //import com.ricdev.uread.util.PurchaseHelper
6 | //import dagger.Module
7 | //import dagger.Provides
8 | //import dagger.hilt.InstallIn
9 | //import dagger.hilt.android.components.ActivityComponent
10 | //import dagger.hilt.android.qualifiers.ActivityContext
11 |
12 | //@Module
13 | //@InstallIn(ActivityComponent::class)
14 | //object ActivityModule {
15 | // @Provides
16 | // fun providePurchaseHelper(@ActivityContext context: Context): PurchaseHelper {
17 | // return PurchaseHelper(context as Activity)
18 | // }
19 | //}
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/model/Author.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.model
2 | import com.ricdev.uread.data.model.Book
3 |
4 | data class Author(
5 | val name: String,
6 | val books: List
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/model/DecorationStyleAnnotationMark.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.model
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import androidx.annotation.ColorInt
6 | import org.readium.r2.navigator.Decoration
7 |
8 | data class DecorationStyleAnnotationMark(
9 | @ColorInt val tint: Int
10 | ) : Decoration.Style {
11 | override fun describeContents(): Int = 0
12 |
13 | override fun writeToParcel(parcel: Parcel, flags: Int) {
14 | parcel.writeInt(tint)
15 | }
16 |
17 | companion object CREATOR : Parcelable.Creator {
18 | override fun createFromParcel(parcel: Parcel): DecorationStyleAnnotationMark {
19 | return DecorationStyleAnnotationMark(parcel.readInt())
20 | }
21 |
22 | override fun newArray(size: Int): Array {
23 | return arrayOfNulls(size)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/model/Genre.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.model
2 |
3 | data class Genre(
4 | val name: String,
5 | val count: Int,
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/model/Statistics.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.model
2 |
3 | import com.ricdev.uread.data.model.ReadingActivity
4 |
5 |
6 | data class Statistics(
7 | val totalBooks: Int = 0,
8 | val booksRead: Int = 0,
9 | val booksReadThisYear: Int = 0,
10 | val booksReadThisMonth: Int = 0,
11 | val booksInProgress: Int = 0,
12 | val booksToRead: Int = 0,
13 | val totalReadingTime: Long = 0,
14 | val averageDailyReadingTime: Long = 0,
15 | val averageReadingTimePerBook: Long = 0,
16 | val longestReadingStreak: Int = 0,
17 | val currentReadingStreak: Int = 0,
18 | val favoriteBooks: Int = 0,
19 | val ratedBooks: Int = 0,
20 | val averageRating: Double = 0.0,
21 |
22 |
23 | val totalNotes: Int = 0,
24 | val totalHighlights: Int = 0,
25 | val totalUnderlines: Int = 0,
26 |
27 |
28 | val favoriteAuthors: List = emptyList(),
29 |
30 | val genreDistribution: List = emptyList(),
31 |
32 |
33 | val readingActivities: List = emptyList()
34 | )
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/repository/BooksRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.repository
2 |
3 | import androidx.paging.PagingSource
4 | import com.ricdev.uread.data.model.Book
5 | import com.ricdev.uread.data.model.BookAnnotation
6 | import com.ricdev.uread.data.model.Bookmark
7 | import com.ricdev.uread.data.model.FileType
8 | import com.ricdev.uread.data.model.Note
9 | import com.ricdev.uread.data.model.ReadingActivity
10 | import com.ricdev.uread.data.model.ReadingStatus
11 | import com.ricdev.uread.data.model.SortOption
12 | import kotlinx.coroutines.flow.Flow
13 |
14 | interface BooksRepository {
15 | fun getAllBooks(): Flow>
16 |
17 | fun getAllBooks(
18 | sortOption: SortOption,
19 | isAscending: Boolean,
20 | readingStatuses: Set,
21 | fileTypes: Set
22 | ): PagingSource
23 |
24 | fun getDeletedBooks(): Flow>
25 | suspend fun getAllBookUris(): List
26 | suspend fun getBookById(bookId: Long): Book?
27 | suspend fun insertBook(book: Book)
28 | suspend fun updateBook(book: Book)
29 | suspend fun deleteBook(book: Book)
30 | suspend fun deleteBookByUri(bookUri: String)
31 |
32 | suspend fun getReadingProgress(bookId: Long): String
33 | suspend fun setReadingProgress(bookId: Long, locator: String, progression: Float)
34 | suspend fun setReadingStatus(bookId: Long, status: ReadingStatus)
35 |
36 |
37 |
38 |
39 |
40 |
41 | // annotation (Highlights / Underlines)
42 | suspend fun getAllAnnotations(): Flow>
43 | suspend fun getAnnotations(bookId: Long): Flow>
44 | suspend fun addAnnotation(annotation: BookAnnotation): Long
45 | suspend fun updateAnnotation(annotation: BookAnnotation)
46 | suspend fun deleteAnnotation(annotation: BookAnnotation)
47 |
48 |
49 |
50 | // Notes
51 | suspend fun getAllNotes(): Flow>
52 | suspend fun getNotesForBook(bookId: Long): Flow>
53 | suspend fun addNote(note: Note)
54 | suspend fun updateNote(note: Note)
55 | suspend fun deleteNote(note: Note)
56 |
57 |
58 | // Bookmarks
59 | suspend fun getAllBookmarks(): Flow>
60 | suspend fun getBookmarksForBook(bookId: Long): Flow>
61 | suspend fun addBookmark(bookmark: Bookmark)
62 | suspend fun updateBookmark(bookmark: Bookmark)
63 | suspend fun deleteBookmark(bookmark: Bookmark)
64 |
65 |
66 |
67 |
68 |
69 | // Reading Activity
70 | suspend fun insertOrUpdateReadingActivity(readingActivity: ReadingActivity)
71 | suspend fun getReadingActivityByDate(date: Long): ReadingActivity?
72 | suspend fun getTotalReadingTime(): Long?
73 | suspend fun getAllReadingActivities(): Flow>
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/repository/ShelfRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.repository
2 | import com.ricdev.uread.data.model.Book
3 | import com.ricdev.uread.data.model.Shelf
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface ShelfRepository {
7 | fun getShelves(): Flow>
8 | suspend fun getShelfById(shelfId: Long): Shelf?
9 | suspend fun addShelf(shelf: Shelf): Long
10 | suspend fun updateShelf(shelf: Shelf)
11 | suspend fun deleteShelf(shelf: Shelf)
12 |
13 | suspend fun addBookToShelf(bookId: Long, shelfId: Long)
14 | suspend fun removeBookFromShelf(bookId: Long, shelfId: Long)
15 | fun getBooksForShelf(shelfId: Long): Flow>
16 | fun getShelvesForBook(bookId: Long): Flow>
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/annotations/AddAnnotationUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.annotations
2 |
3 | import com.ricdev.uread.data.model.BookAnnotation
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 |
9 | class AddAnnotationUseCase @Inject constructor(private val repository: BooksRepository) {
10 | suspend operator fun invoke(annotation: BookAnnotation): Long = withContext(Dispatchers.IO) {
11 | repository.addAnnotation(annotation)
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/annotations/DeleteAnnotationUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.annotations
2 |
3 | import com.ricdev.uread.data.model.BookAnnotation
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteAnnotationUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(annotation: BookAnnotation) {
9 | repository.deleteAnnotation(annotation)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/annotations/GetAllAnnotationsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.annotations
2 |
3 | import com.ricdev.uread.data.model.BookAnnotation
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetAllAnnotationsUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(): Flow> {
10 | return repository.getAllAnnotations()
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/annotations/GetAnnotationsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.annotations
2 |
3 | import com.ricdev.uread.data.model.BookAnnotation
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetAnnotationsUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(bookId: Long): Flow> {
10 | return repository.getAnnotations(bookId)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/annotations/UpdateAnnotationUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.annotations
2 |
3 | import com.ricdev.uread.data.model.BookAnnotation
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class UpdateAnnotationUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(annotation: BookAnnotation) {
9 | repository.updateAnnotation(annotation)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/bookmarks/AddBookmarkUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.bookmarks
2 |
3 | import com.ricdev.uread.data.model.Bookmark
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class AddBookmarkUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(bookmark: Bookmark) {
9 | repository.addBookmark(bookmark)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/bookmarks/DeleteBookmarkUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.bookmarks
2 |
3 | import com.ricdev.uread.data.model.Bookmark
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteBookmarkUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(bookmark: Bookmark) {
9 | repository.deleteBookmark(bookmark)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/bookmarks/GetAllBookmarksUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.bookmarks
2 |
3 | import com.ricdev.uread.data.model.Bookmark
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetAllBookmarksUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(): Flow> {
10 | return repository.getAllBookmarks()
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/bookmarks/GetBookmarksForBookUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.bookmarks
2 |
3 | import com.ricdev.uread.data.model.Bookmark
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetBookmarksForBookUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(bookId: Long): Flow> {
10 | return repository.getBookmarksForBook(bookId)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/bookmarks/UpdateBookmarkUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.bookmarks
2 |
3 | import com.ricdev.uread.data.model.Bookmark
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class UpdateBookmarkUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(bookmark: Bookmark) {
9 | repository.updateBookmark(bookmark)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/DeleteBookByUriUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.domain.repository.BooksRepository
4 | import javax.inject.Inject
5 |
6 | class DeleteBookByUriUseCase @Inject constructor(private val repository: BooksRepository) {
7 | suspend operator fun invoke(bookUri: String) {
8 | repository.deleteBookByUri(bookUri)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/DeleteBookUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteBookUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(book: Book) {
9 | repository.deleteBook(book)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/GetAllBooksUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetAllBooksUseCase @Inject constructor(
9 | private val repository: BooksRepository
10 | ) {
11 | operator fun invoke(): Flow> {
12 | return repository.getAllBooks()
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/GetBookByIdUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 |
9 | class GetBookByIdUseCase @Inject constructor(private val repository: BooksRepository) {
10 | suspend operator fun invoke(bookId: Long): Book? = withContext(Dispatchers.IO) {
11 | repository.getBookById(bookId)
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/GetBookUrisUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.domain.repository.BooksRepository
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import javax.inject.Inject
7 |
8 | class GetBookUrisUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(): List = withContext(Dispatchers.IO) {
10 | repository.getAllBookUris()
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/GetBooksUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import androidx.paging.PagingData
6 | import com.ricdev.uread.data.model.Book
7 | import com.ricdev.uread.data.model.FileType
8 | import com.ricdev.uread.data.model.ReadingStatus
9 | import com.ricdev.uread.data.model.SortOption
10 | import com.ricdev.uread.domain.repository.BooksRepository
11 | import kotlinx.coroutines.flow.Flow
12 | import javax.inject.Inject
13 |
14 | class GetBooksUseCase @Inject constructor(
15 | private val repository: BooksRepository
16 | ) {
17 | operator fun invoke(
18 | sortOption: SortOption,
19 | isAscending: Boolean,
20 | readingStatuses: Set,
21 | fileTypes: Set
22 | ): Flow> = Pager(
23 | config = PagingConfig(
24 | pageSize = 9,
25 | enablePlaceholders = true,
26 | )
27 | ) {
28 | repository.getAllBooks(sortOption, isAscending, readingStatuses, fileTypes)
29 | }.flow
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/GetDeletedBooksUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetDeletedBooksUseCase @Inject constructor(private val repository: BooksRepository) {
9 | operator fun invoke(): Flow> = repository.getDeletedBooks()
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/InsertBookUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 |
9 | class InsertBookUseCase @Inject constructor(private val repository: BooksRepository) {
10 | suspend operator fun invoke(book: Book) = withContext(Dispatchers.IO) {
11 | repository.insertBook(book)
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/books/UpdateBookUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.books
2 |
3 | import com.ricdev.uread.data.model.Book
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class UpdateBookUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(book: Book) {
9 | repository.updateBook(book)
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/notes/AddNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.notes
2 |
3 | import com.ricdev.uread.data.model.Note
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class AddNoteUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(note: Note) {
9 | repository.addNote(note)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/notes/DeleteNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.notes
2 |
3 | import com.ricdev.uread.data.model.Note
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteNoteUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(note: Note) {
9 | repository.deleteNote(note)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/notes/GetAllNotesUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.notes
2 |
3 | import com.ricdev.uread.data.model.Note
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetAllNotesUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(): Flow> {
10 | return repository.getAllNotes()
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/notes/GetNotesForBookUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.notes
2 |
3 | import com.ricdev.uread.data.model.Note
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetNotesForBookUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(bookId: Long): Flow> {
10 | return repository.getNotesForBook(bookId)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/notes/UpdateNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.notes
2 |
3 | import com.ricdev.uread.data.model.Note
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class UpdateNoteUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(note: Note) {
9 | repository.updateNote(note)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/reading_activity/AddReadingActivityUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.reading_activity
2 |
3 | import com.ricdev.uread.data.model.ReadingActivity
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import javax.inject.Inject
6 |
7 | class AddReadingActivityUseCase @Inject constructor(private val repository: BooksRepository) {
8 | suspend operator fun invoke(readingActivity: ReadingActivity) {
9 | repository.insertOrUpdateReadingActivity(readingActivity)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/reading_activity/GetAllReadingActivitiesUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.reading_activity
2 |
3 | import com.ricdev.uread.data.model.ReadingActivity
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class GetAllReadingActivitiesUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(): Flow> {
10 | return repository.getAllReadingActivities()
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/reading_activity/GetReadingActivityByDateUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.reading_activity
2 |
3 | import com.ricdev.uread.domain.repository.BooksRepository
4 | import javax.inject.Inject
5 |
6 | class GetReadingActivityByDateUseCase @Inject constructor(private val repository: BooksRepository) {
7 | suspend operator fun invoke(date: Long) = repository.getReadingActivityByDate(date)
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/reading_progress/GetReadingProgressUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.reading_progress
2 |
3 | import com.ricdev.uread.domain.repository.BooksRepository
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import javax.inject.Inject
7 |
8 | class GetReadingProgressUseCase @Inject constructor(private val repository: BooksRepository) {
9 | suspend operator fun invoke(bookId: Long): String = withContext(Dispatchers.IO) {
10 | repository.getReadingProgress(bookId)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/reading_progress/SetReadingProgressUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.reading_progress
2 |
3 | import com.ricdev.uread.data.model.ReadingStatus
4 | import com.ricdev.uread.domain.repository.BooksRepository
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import org.json.JSONObject
8 | import javax.inject.Inject
9 |
10 | class SetReadingProgressUseCase @Inject constructor(private val repository: BooksRepository) {
11 | suspend operator fun invoke(bookId: Long, locator: String) = withContext(Dispatchers.IO) {
12 | val progression = getProgressionFromLocator(locator)
13 |
14 |
15 | updateReadingStatus(bookId, progression)
16 |
17 | repository.setReadingProgress(bookId, locator, progression)
18 |
19 | }
20 |
21 | private suspend fun updateReadingStatus(bookId: Long, progression: Float) {
22 | when {
23 | progression >= 99f -> {
24 | repository.setReadingStatus(bookId, ReadingStatus.FINISHED)
25 | }
26 | progression > 2f -> repository.setReadingStatus(bookId, ReadingStatus.IN_PROGRESS)
27 | }
28 | }
29 |
30 | private fun getProgressionFromLocator(locatorJson: String): Float {
31 | return try {
32 | val locator = JSONObject(locatorJson)
33 | val locations = locator.optJSONObject("locations")
34 | (locations?.optDouble("totalProgression", 0.0)?.toFloat() ?: 0f) * 100f
35 | } catch (e: Exception) {
36 | 0f
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/AddBookToShelfUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.domain.repository.ShelfRepository
4 | import javax.inject.Inject
5 |
6 | class AddBookToShelfUseCase @Inject constructor(private val shelfRepository: ShelfRepository) {
7 | suspend operator fun invoke(bookId: Long, shelfId: Long) {
8 | shelfRepository.addBookToShelf(bookId, shelfId)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/AddShelfUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.data.model.Shelf
4 | import com.ricdev.uread.domain.repository.ShelfRepository
5 | import javax.inject.Inject
6 |
7 | // AddShelfUseCase.kt
8 | class AddShelfUseCase @Inject constructor(private val shelfRepository: ShelfRepository) {
9 | suspend operator fun invoke(shelfName: String, order: Int): Long {
10 | return shelfRepository.addShelf(Shelf(name = shelfName, order = order))
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/GetBooksForShelfUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.domain.repository.ShelfRepository
4 | import javax.inject.Inject
5 |
6 | class GetBooksForShelfUseCase @Inject constructor(private val shelfRepository: ShelfRepository) {
7 | operator fun invoke(shelfId: Long) = shelfRepository.getBooksForShelf(shelfId)
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/GetShelvesUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.data.model.Shelf
4 | import com.ricdev.uread.domain.repository.ShelfRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | // GetShelvesUseCase.kt
9 | class GetShelvesUseCase @Inject constructor(private val shelfRepository: ShelfRepository) {
10 | operator fun invoke(): Flow> = shelfRepository.getShelves()
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/RemoveBooksFromShelfUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.domain.repository.ShelfRepository
4 | import javax.inject.Inject
5 |
6 | class RemoveBooksFromShelfUseCase @Inject constructor(private val shelfRepository: ShelfRepository) {
7 | suspend operator fun invoke(bookId: Long, shelfId: Long) {
8 | shelfRepository.removeBookFromShelf(bookId, shelfId)
9 | }
10 |
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/RemoveShelfUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.data.model.Shelf
4 | import com.ricdev.uread.domain.repository.ShelfRepository
5 | import javax.inject.Inject
6 |
7 | // RemoveShelfUseCase.kt
8 | class RemoveShelfUseCase @Inject constructor(private val shelfRepository: ShelfRepository) {
9 | suspend operator fun invoke(shelf: Shelf) {
10 | shelfRepository.deleteShelf(shelf)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/domain/use_case/shelves/UpdateShelfUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.domain.use_case.shelves
2 |
3 | import com.ricdev.uread.data.model.Shelf
4 | import com.ricdev.uread.domain.repository.ShelfRepository
5 | import javax.inject.Inject
6 |
7 | class UpdateShelfUseCase @Inject constructor(
8 | private val shelfRepository: ShelfRepository
9 | ) {
10 | suspend operator fun invoke(shelf: Shelf) {
11 | shelfRepository.updateShelf(shelf)
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/navigation/Screens.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.navigation
2 |
3 |
4 | sealed class Screens(val route: String) {
5 |
6 | data object GettingStartedScreen : Screens("getting_started_screen")
7 | data object HomeScreen : Screens("home_screen")
8 | data object BookReaderScreen: Screens("book_reader_screen")
9 | data object PdfReaderScreen: Screens("pdf_reader_screen")
10 | data object AudiobookReaderScreen: Screens("audiobook_reader_screen")
11 | data object BookDetailsScreen: Screens("book_details_screen")
12 | data object SettingsScreen: Screens("settings_screen")
13 | data object GeneralSettingsScreen: Screens("general_settings")
14 | data object ThemeScreen: Screens("theme_screen")
15 | data object DeletedBooksScreen: Screens("deleted_books_screen")
16 | data object ShelvesScreen: Screens("shelves_screen")
17 | data object AboutAppScreen: Screens("about_app_screen")
18 |
19 |
20 |
21 | data object NotesScreen: Screens("notes_screen")
22 | data object AnnotationsScreen: Screens("annotations_screen")
23 | data object StatisticsScreen: Screens("statistics_screen")
24 | // data object OnlineBooksScreen: Screens("online_books_screen")
25 |
26 |
27 | data object PremiumScreen: Screens("premium_screen")
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/annotations/AnnotationsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.annotations
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ricdev.uread.data.model.AppPreferences
7 | import com.ricdev.uread.data.model.BookAnnotation
8 | import com.ricdev.uread.data.model.Note
9 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
10 | import com.ricdev.uread.domain.use_case.annotations.DeleteAnnotationUseCase
11 | import com.ricdev.uread.domain.use_case.annotations.GetAnnotationsUseCase
12 | import com.ricdev.uread.domain.use_case.annotations.UpdateAnnotationUseCase
13 | import com.ricdev.uread.domain.use_case.books.GetAllBooksUseCase
14 | import com.ricdev.uread.util.PurchaseHelper
15 | import dagger.hilt.android.lifecycle.HiltViewModel
16 | import kotlinx.coroutines.ExperimentalCoroutinesApi
17 | import kotlinx.coroutines.flow.Flow
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.flow.StateFlow
20 | import kotlinx.coroutines.flow.asStateFlow
21 | import kotlinx.coroutines.flow.combine
22 | import kotlinx.coroutines.flow.first
23 | import kotlinx.coroutines.flow.flatMapLatest
24 | import kotlinx.coroutines.flow.map
25 | import kotlinx.coroutines.launch
26 | import javax.inject.Inject
27 |
28 | @HiltViewModel
29 | class AnnotationsViewModel @Inject constructor(
30 | private val appPreferencesUtil: AppPreferencesUtil,
31 | getAllBooksUseCase: GetAllBooksUseCase,
32 | private val getAnnotationsUseCase: GetAnnotationsUseCase,
33 | private val removeAnnotationUseCase: DeleteAnnotationUseCase,
34 | private val updateAnnotationUseCase: UpdateAnnotationUseCase,
35 | application: Application,
36 | ) : AndroidViewModel(application){
37 |
38 |
39 | private val _annotations = MutableStateFlow>(emptyList())
40 | val annotations: StateFlow> = _annotations.asStateFlow()
41 |
42 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
43 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
44 |
45 |
46 | @OptIn(ExperimentalCoroutinesApi::class)
47 | val booksWithAnnotations: Flow> = getAllBooksUseCase()
48 | .flatMapLatest { books ->
49 | combine(books.map { book ->
50 | getAnnotationsUseCase(book.id).map { annotations ->
51 | BookWithAnnotations(book, annotations)
52 | }
53 | }) { it.toList() }
54 | }
55 | .map { bookWithAnnotations ->
56 | bookWithAnnotations.filter { it.annotation.isNotEmpty() }
57 | }
58 |
59 |
60 | init {
61 | loadAppPreferences()
62 | }
63 |
64 |
65 | private fun loadAppPreferences(){
66 | viewModelScope.launch {
67 | appPreferencesUtil.appPreferencesFlow.first().let { initialPreferences ->
68 | _appPreferences.value = initialPreferences
69 | }
70 |
71 | // Continue collecting preferences updates
72 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
73 | _appPreferences.value = preferences
74 | }
75 | }
76 | }
77 |
78 |
79 | fun removeAnnotation(annotation: BookAnnotation){
80 | viewModelScope.launch {
81 | removeAnnotationUseCase(annotation)
82 | }
83 | }
84 |
85 |
86 | fun updateAnnotation(updatedAnnotation: BookAnnotation){
87 | viewModelScope.launch {
88 | updateAnnotationUseCase(updatedAnnotation)
89 | }
90 | }
91 |
92 |
93 |
94 | fun purchasePremium(purchaseHelper: PurchaseHelper) {
95 | purchaseHelper.makePurchase()
96 | viewModelScope.launch {
97 | purchaseHelper.isPremium.collect { isPremium ->
98 | updatePremiumStatus(isPremium)
99 | }
100 | }
101 | }
102 |
103 | fun updatePremiumStatus(isPremium: Boolean) {
104 | viewModelScope.launch {
105 | val currentPreferences = appPreferences.value
106 | if (currentPreferences.isPremium != isPremium) {
107 | val updatedPreferences = currentPreferences.copy(isPremium = isPremium)
108 | appPreferencesUtil.updateAppPreferences(updatedPreferences)
109 | _appPreferences.value = updatedPreferences
110 | }
111 | }
112 | }
113 |
114 |
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/audioBookReader/AudiobookReaderState.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.audioBookReader
2 |
3 | sealed class LoadingState {
4 | data object Loading : LoadingState()
5 | data object BookLoaded : LoadingState()
6 | data object InitializingPlayer : LoadingState()
7 | data object Ready : LoadingState()
8 | data class Error(val message: String) : LoadingState()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/bookDetails/BookDetailsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.bookDetails
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.graphics.BitmapFactory
6 | import android.net.Uri
7 | import androidx.lifecycle.AndroidViewModel
8 | import androidx.lifecycle.SavedStateHandle
9 | import androidx.lifecycle.viewModelScope
10 | import com.ricdev.uread.data.model.Book
11 | import com.ricdev.uread.data.model.ReadingStatus
12 | import com.ricdev.uread.domain.use_case.books.GetBookByIdUseCase
13 | import com.ricdev.uread.domain.use_case.books.UpdateBookUseCase
14 | import com.ricdev.uread.util.ImageUtils
15 | import dagger.hilt.android.lifecycle.HiltViewModel
16 | import kotlinx.coroutines.flow.MutableStateFlow
17 | import kotlinx.coroutines.flow.StateFlow
18 | import kotlinx.coroutines.flow.asStateFlow
19 | import kotlinx.coroutines.launch
20 | import javax.inject.Inject
21 |
22 | @HiltViewModel
23 | class BookDetailsViewModel @Inject constructor(
24 | application: Application,
25 | private val getBookByIdUseCase: GetBookByIdUseCase,
26 | private val updateBookUseCase: UpdateBookUseCase,
27 | savedStateHandle: SavedStateHandle
28 | ) : AndroidViewModel(application) {
29 |
30 | private val _book = MutableStateFlow(null)
31 | val book: StateFlow = _book.asStateFlow()
32 |
33 | private val _updateError = MutableStateFlow(null)
34 | val updateError: StateFlow = _updateError.asStateFlow()
35 |
36 |
37 |
38 | init {
39 | val bookId = savedStateHandle.get("bookId")?.toLongOrNull()
40 | if (bookId != null) {
41 | viewModelScope.launch {
42 | _book.value = getBookByIdUseCase(bookId)
43 | }
44 | }
45 | }
46 |
47 |
48 | fun updateBook(updatedBook: Book, updatedReadingStatus: Boolean = false) {
49 | viewModelScope.launch {
50 | var updateBook: Book = updatedBook
51 | if (updatedReadingStatus) {
52 | updateBook = when (updatedBook.readingStatus) {
53 | ReadingStatus.NOT_STARTED -> updatedBook.copy(
54 | startReadingDate = null,
55 | endReadingDate = null,
56 | readingTime = 0,
57 | progression = 0f
58 | )
59 | ReadingStatus.IN_PROGRESS -> updatedBook.copy(
60 | startReadingDate = System.currentTimeMillis(),
61 | endReadingDate = null,
62 | readingTime = 0,
63 | progression = 0f
64 | )
65 | ReadingStatus.FINISHED -> updatedBook.copy(
66 | endReadingDate = System.currentTimeMillis(),
67 | progression = 100f,
68 | )
69 | else -> updatedBook
70 | }
71 | }
72 |
73 | updateBookUseCase(updateBook)
74 | _book.value = getBookByIdUseCase(updatedBook.id)
75 | }
76 | }
77 |
78 |
79 | fun updateCoverImage(context: Context, uri: Uri): String? {
80 | return try {
81 | val inputStream = context.contentResolver.openInputStream(uri)
82 | val bitmap = BitmapFactory.decodeStream(inputStream)
83 | inputStream?.close()
84 | bitmap?.let { ImageUtils.saveCoverImage(bitmap, uri.toString(), context) }
85 | } catch (e: Exception) {
86 | e.printStackTrace()
87 | null
88 | }
89 | }
90 |
91 |
92 |
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/bookReader/BookReaderUiState.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.bookReader
2 |
3 | import org.readium.r2.shared.publication.Publication
4 |
5 | sealed class BookReaderUiState {
6 | data object Loading : BookReaderUiState()
7 | data class Error(val message: String) : BookReaderUiState()
8 | data class Success(val publication: Publication) : BookReaderUiState()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/bookReader/components/drawers/ChaptersDrawer.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.bookReader.components.drawers
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Check
10 | import androidx.compose.material.icons.filled.Close
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.text.style.TextOverflow
17 | import androidx.compose.ui.unit.dp
18 | import com.ricdev.uread.R
19 | import org.readium.r2.shared.publication.Link
20 |
21 | @Composable
22 | fun ChaptersDrawer(
23 | isOpen: Boolean,
24 | currentChapter: String,
25 | tableOfContents: List,
26 | onChapterSelect: (Link) -> Unit,
27 | onClose: () -> Unit,
28 | ) {
29 | AnimatedVisibility(
30 | visible = isOpen,
31 | enter = slideInHorizontally(initialOffsetX = { -it }),
32 | exit = slideOutHorizontally(targetOffsetX = { -it })
33 | ) {
34 | ModalDrawerSheet {
35 | Column(
36 | modifier = Modifier
37 | .fillMaxSize()
38 | .padding(16.dp)
39 | ) {
40 | Row(
41 | modifier = Modifier.fillMaxWidth(),
42 | horizontalArrangement = Arrangement.SpaceBetween,
43 | verticalAlignment = Alignment.CenterVertically
44 | ) {
45 | Text(stringResource(R.string.chapters), style = MaterialTheme.typography.titleLarge)
46 | IconButton(onClick = onClose) {
47 | Icon(Icons.Default.Close, contentDescription = "Close Chapters")
48 | }
49 | }
50 | Spacer(modifier = Modifier.height(16.dp))
51 | LazyColumn {
52 | items(tableOfContents) { chapter ->
53 | ChapterItem(
54 | chapter = chapter,
55 | isCurrentChapter = chapter.title == currentChapter,
56 | onClick = { onChapterSelect(chapter) }
57 | )
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 | @Composable
66 | fun ChapterItem(
67 | chapter: Link,
68 | isCurrentChapter: Boolean,
69 | onClick: () -> Unit
70 | ) {
71 | ListItem(
72 | colors = ListItemDefaults.colors(
73 | containerColor = MaterialTheme.colorScheme.surfaceContainerLow
74 | ) ,
75 | headlineContent = {
76 | Text(
77 | text = chapter.title ?: stringResource(R.string.untitled_chapter),
78 | maxLines = 1,
79 | overflow = TextOverflow.Ellipsis,
80 | style = if (isCurrentChapter) MaterialTheme.typography.bodyLarge else MaterialTheme.typography.bodyMedium,
81 | color = if (isCurrentChapter) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
82 | )
83 | },
84 | modifier = Modifier.clickable(onClick = onClick),
85 | leadingContent = if (isCurrentChapter) {
86 | {
87 | Icon(
88 | Icons.Default.Check,
89 | contentDescription = "Current Chapter",
90 | tint = MaterialTheme.colorScheme.primary
91 | )
92 | }
93 | } else null
94 | )
95 | HorizontalDivider()
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/bookReader/util/SelectionActionMode.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.bookReader.util
2 |
3 | import android.graphics.Rect
4 | import android.graphics.RectF
5 | import android.view.ActionMode
6 | import android.view.Menu
7 | import android.view.MenuItem
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 |
12 |
13 |
14 | class SelectionActionModeCallback(
15 | private val showCustomMenu: (Rect, String) -> Unit,
16 | private val hideCustomMenu: () -> Unit,
17 | private val getSelectedText: suspend () -> String?,
18 | private val getSelectionPosition: suspend () -> RectF?
19 | ) : ActionMode.Callback {
20 | override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
21 | CoroutineScope(Dispatchers.Main).launch {
22 | val selectionRectF = getSelectionPosition()
23 | val selectedText = getSelectedText()
24 | val selectionRect = selectionRectF?.let {
25 | Rect(it.left.toInt(), it.top.toInt(), it.right.toInt(), it.bottom.toInt())
26 | }
27 | if (selectedText != null && selectionRect != null) {
28 | showCustomMenu(selectionRect, selectedText)
29 | }
30 | }
31 | return true
32 | }
33 |
34 |
35 | override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false
36 |
37 | override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = false
38 |
39 | override fun onDestroyActionMode(mode: ActionMode) {
40 | hideCustomMenu()
41 | }
42 | }
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/bookShelf/BookShelfScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.bookShelf
2 |
3 |
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.outlined.ImportContacts
11 | import androidx.compose.material3.Icon
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.res.stringResource
17 | import androidx.compose.ui.unit.dp
18 | import androidx.navigation.NavHostController
19 | import androidx.paging.compose.LazyPagingItems
20 | import com.ricdev.uread.R
21 | import com.ricdev.uread.data.model.AppPreferences
22 | import com.ricdev.uread.data.model.Book
23 | import com.ricdev.uread.data.model.Layout
24 | import com.ricdev.uread.data.model.Shelf
25 | import com.ricdev.uread.presentation.home.HomeViewModel
26 | import com.ricdev.uread.presentation.home.components.GridLayout
27 | import com.ricdev.uread.presentation.home.components.ListLayout
28 |
29 | @Composable
30 | fun BookShelfScreen(
31 | clearSearch: () -> Unit,
32 | shelf: Shelf,
33 | books: LazyPagingItems,
34 | homeViewModel: HomeViewModel,
35 | navController: NavHostController,
36 | selectedBooks: List,
37 | selectionMode: Boolean,
38 | toggleSelection: (Book) -> Unit,
39 | isLoading: Boolean,
40 | appPreferences: AppPreferences,
41 | ) {
42 |
43 |
44 | when {
45 | books.itemCount == 0 -> {
46 | EmptyShelfContent(shelf.name)
47 | }
48 |
49 | appPreferences.homeLayout == Layout.Grid || appPreferences.homeLayout == Layout.CoverOnly -> {
50 | GridLayout(
51 | clearSearch = { clearSearch() },
52 | books = books,
53 | navController = navController,
54 | selectedBooks = selectedBooks,
55 | selectionMode = selectionMode,
56 | toggleSelection = toggleSelection,
57 | viewModel = homeViewModel,
58 | isLoading = isLoading,
59 | appPreferences = appPreferences,
60 |
61 | )
62 | }
63 |
64 | else -> {
65 | ListLayout(
66 | clearSearch = { clearSearch() },
67 | books = books,
68 | navController = navController,
69 | selectedBooks = selectedBooks,
70 | selectionMode = selectionMode,
71 | toggleSelection = toggleSelection,
72 | viewModel = homeViewModel,
73 | isLoading = isLoading,
74 | appPreferences = appPreferences,
75 | )
76 | }
77 | }
78 | }
79 |
80 |
81 |
82 |
83 | @Composable
84 | fun EmptyShelfContent(shelf: String) {
85 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
86 | Column(
87 | horizontalAlignment = Alignment.CenterHorizontally,
88 | verticalArrangement = Arrangement.spacedBy(12.dp)
89 | ) {
90 | Icon(
91 | imageVector = Icons.Outlined.ImportContacts,
92 | contentDescription = "No books in this shelf",
93 | modifier = Modifier.size(48.dp)
94 | )
95 | Text(stringResource(R.string.no_books_in, shelf))
96 |
97 | }
98 | }
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/gettingStarted/GettingStartedViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.gettingStarted
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ricdev.uread.data.model.AppPreferences
7 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class GettingStartedViewModel @Inject constructor(
17 | private val appPreferencesUtil: AppPreferencesUtil,
18 | application: Application,
19 | ) : AndroidViewModel(application) {
20 |
21 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
22 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
23 |
24 | private val _isButtonsEnabled = MutableStateFlow(true)
25 | val isButtonsEnabled: StateFlow = _isButtonsEnabled.asStateFlow()
26 |
27 |
28 | init {
29 | observeAppPreferences()
30 | }
31 |
32 | private fun observeAppPreferences() {
33 | viewModelScope.launch {
34 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
35 | _appPreferences.value = preferences
36 | }
37 | }
38 | }
39 |
40 |
41 | fun updateAppPreferences(newPreferences: AppPreferences) {
42 | viewModelScope.launch {
43 | _isButtonsEnabled.value = false
44 | appPreferencesUtil.updateAppPreferences(newPreferences)
45 | _appPreferences.value = newPreferences
46 | }
47 | }
48 |
49 |
50 |
51 |
52 |
53 | fun skipGettingStarted() {
54 | viewModelScope.launch {
55 | val updatedPreferences = appPreferences.value.copy(isFirstLaunch = false)
56 | updateAppPreferences(updatedPreferences)
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/gettingStarted/components/ActionButtons.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.gettingStarted.components
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.vector.ImageVector
14 | import androidx.compose.ui.semantics.contentDescription
15 | import androidx.compose.ui.semantics.semantics
16 | import androidx.compose.ui.unit.dp
17 |
18 | @Composable
19 | fun ActionButton(
20 | modifier: Modifier,
21 | text: String,
22 | icon: ImageVector,
23 | enabled: Boolean,
24 | onClick: () -> Unit,
25 | description: String
26 | ) {
27 | Button(
28 | modifier = modifier
29 | .semantics { contentDescription = description },
30 | enabled = enabled,
31 | onClick = onClick,
32 | contentPadding = PaddingValues(8.dp),
33 | ) {
34 | Icon(
35 | imageVector = icon,
36 | contentDescription = text,
37 | modifier = Modifier.size(24.dp)
38 | )
39 | Spacer(modifier = Modifier.width(8.dp))
40 | Text(text, style = MaterialTheme.typography.bodySmall)
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/gettingStarted/components/StorageAccessDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.gettingStarted.components
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Button
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.material3.TextButton
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.stringResource
10 | import com.ricdev.uread.R
11 |
12 | @Composable
13 | fun StorageAccessDialog(
14 | title: String,
15 | message: String,
16 | confirmButtonText: String,
17 | onConfirm: () -> Unit,
18 | onDismiss: () -> Unit,
19 | ) {
20 | AlertDialog(
21 | onDismissRequest = {
22 | onDismiss()
23 | },
24 | title = {
25 | Text(title)
26 | },
27 | text = {
28 | Text(message)
29 | },
30 | confirmButton = {
31 | Button(
32 | onClick = onConfirm
33 | ) {
34 | Text(confirmButtonText)
35 | }
36 | },
37 | dismissButton = {
38 | TextButton(
39 | onClick = onDismiss
40 | ) {
41 | Text(stringResource(R.string.cancel), color = MaterialTheme.colorScheme.error)
42 | }
43 | }
44 | )
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/home/components/CustomSnackbar.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.home.components
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
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.material3.LinearProgressIndicator
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Snackbar
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 | import com.ricdev.uread.presentation.home.states.ImportProgressState
17 | import com.ricdev.uread.presentation.home.states.SnackbarState
18 |
19 |
20 |
21 | @Composable
22 | fun CustomSnackbar(
23 | snackbarState: SnackbarState,
24 | importProgressState: ImportProgressState,
25 | ) {
26 | when (snackbarState) {
27 | is SnackbarState.Visible -> {
28 | Snackbar(
29 | modifier = Modifier
30 | .padding(16.dp)
31 | .fillMaxWidth(),
32 | // action = {
33 | // // Optional dismiss button
34 | // TextButton(onClick = onDismiss) {
35 | // Text("Dismiss")
36 | // }
37 | // }
38 | // dismissAction = {
39 | // TextButton(onClick = onDismiss) {
40 | // Text("Dismiss")
41 | // }
42 | // },
43 | containerColor = MaterialTheme.colorScheme.surfaceVariant,
44 | contentColor = MaterialTheme.colorScheme.onSurfaceVariant
45 | ) {
46 | // Show different content based on import progress
47 | when (importProgressState) {
48 | is ImportProgressState.InProgress -> {
49 | val animatedProgress = animateFloatAsState(
50 | targetValue = importProgressState.current.toFloat() / importProgressState.total,
51 | label = ""
52 | ).value
53 | Column(
54 | verticalArrangement = Arrangement.Center,
55 | horizontalAlignment = Alignment.Start,
56 | modifier = Modifier.fillMaxWidth()
57 | ) {
58 | LinearProgressIndicator(
59 | progress = { animatedProgress },
60 | modifier = Modifier
61 | .fillMaxWidth()
62 | .padding(bottom = 8.dp),
63 | color = MaterialTheme.colorScheme.primary,
64 | )
65 | Text(
66 | text = snackbarState.message
67 | )
68 | }
69 | }
70 | is ImportProgressState.Error -> {
71 | Text(
72 | text = snackbarState.message,
73 | color = MaterialTheme.colorScheme.error
74 | )
75 | }
76 | is ImportProgressState.Complete -> {
77 | Text(text = snackbarState.message)
78 | }
79 | ImportProgressState.Idle -> {
80 | Text(text = snackbarState.message)
81 | }
82 | }
83 | }
84 | }
85 | SnackbarState.Hidden -> {
86 | // Do nothing when hidden
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/home/components/HomeFloatingActionButton.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.home.components
2 |
3 | //import androidx.compose.animation.AnimatedVisibility
4 | //import androidx.compose.animation.expandVertically
5 | //import androidx.compose.animation.fadeIn
6 | //import androidx.compose.animation.fadeOut
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.Spacer
11 | //import androidx.compose.foundation.layout.height
12 | //import androidx.compose.foundation.layout.padding
13 | //import androidx.compose.foundation.layout.width
14 | //import androidx.compose.foundation.layout.wrapContentSize
15 | //import androidx.compose.foundation.shape.RoundedCornerShape
16 | //import androidx.compose.material.icons.Icons
17 | //import androidx.compose.material.icons.filled.Add
18 | //import androidx.compose.material.icons.filled.Close
19 | //import androidx.compose.material.icons.filled.Search
20 | //import androidx.compose.material3.FilledTonalButton
21 | //import androidx.compose.material3.FloatingActionButton
22 | //import androidx.compose.material3.Icon
23 | //import androidx.compose.material3.SmallFloatingActionButton
24 | //import androidx.compose.material3.Text
25 | //import androidx.compose.runtime.Composable
26 | //import androidx.compose.runtime.getValue
27 | //import androidx.compose.runtime.mutableStateOf
28 | //import androidx.compose.runtime.remember
29 | //import androidx.compose.runtime.setValue
30 | //import androidx.compose.ui.Alignment
31 | //import androidx.compose.ui.Modifier
32 | //import androidx.compose.ui.unit.dp
33 |
34 | //@Composable
35 | //fun HomeFloatingActionButton() {
36 | //
37 | // var isExpanded by remember { mutableStateOf(false) }
38 | //
39 | //
40 | //
41 | //
42 | // Column(
43 | // modifier = Modifier.wrapContentSize(),
44 | // horizontalAlignment = Alignment.End,
45 | // ) {
46 | // AnimatedVisibility(
47 | // visible = isExpanded,
48 | // enter = fadeIn() + expandVertically(),
49 | // exit = fadeOut(),
50 | // ) {
51 | // Column(
52 | // modifier = Modifier.padding(4.dp),
53 | // horizontalAlignment = Alignment.End,
54 | // ) {
55 | // Row {
56 | // FilledTonalButton(
57 | // contentPadding = PaddingValues(8.dp),
58 | // shape = RoundedCornerShape(12.dp),
59 | // onClick = { /*TODO*/ }
60 | // ) {
61 | // Text("Search in Open Library")
62 | // }
63 | // Spacer(modifier = Modifier.width(16.dp))
64 | // SmallFloatingActionButton(
65 | // onClick = { /* Handle click */ },
66 | // ) {
67 | // Icon(Icons.Filled.Search, contentDescription = "Search")
68 | // }
69 | // }
70 | // Spacer(modifier = Modifier.height(5.dp))
71 | // Row {
72 | // FilledTonalButton(
73 | // contentPadding = PaddingValues(8.dp),
74 | // shape = RoundedCornerShape(12.dp),
75 | // onClick = { /*TODO*/ }
76 | // ) {
77 | // Text("Add Manually")
78 | // }
79 | // Spacer(modifier = Modifier.width(16.dp))
80 | // SmallFloatingActionButton(
81 | // onClick = { /* Handle click */ },
82 | // ) {
83 | // Icon(Icons.Filled.Add, contentDescription = "Add")
84 | // }
85 | // }
86 | // }
87 | // }
88 | // Spacer(modifier = Modifier.height(12.dp))
89 | // FloatingActionButton(
90 | // onClick = { isExpanded = !isExpanded }
91 | // ) {
92 | // Icon(
93 | // if (isExpanded) Icons.Filled.Close else Icons.Filled.Add,
94 | // contentDescription = if (isExpanded) "Close" else "Add"
95 | // )
96 | // }
97 | // }
98 | //
99 | //}
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/home/states/ImportProgressState.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.home.states
2 |
3 |
4 |
5 | // Define sealed class for import states
6 | sealed class ImportProgressState {
7 | data object Idle : ImportProgressState()
8 | data class InProgress(val current: Int, val total: Int) : ImportProgressState()
9 | data class Error(val message: String) : ImportProgressState()
10 | data object Complete : ImportProgressState()
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/home/states/SnackbarState.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.home.states
2 |
3 |
4 |
5 |
6 | sealed class SnackbarState {
7 | data object Hidden : SnackbarState()
8 | data class Visible(
9 | val message: String,
10 | val unlimited: Boolean = false,
11 | ) : SnackbarState()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/pdfReader/components/PdfReaderTopBar.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.pdfReader.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.IconButton
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.shadow
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.text.style.TextOverflow
20 | import androidx.compose.ui.unit.dp
21 | import com.ricdev.uread.data.model.Book
22 |
23 | @Composable
24 | fun PdfReaderTopBar(
25 | book: Book?,
26 | onBackClick: () -> Unit
27 | ) {
28 |
29 |
30 | Column(
31 | modifier = Modifier
32 | .shadow(8.dp)
33 | .fillMaxWidth()
34 | .background(Color.White)
35 | .padding(top = 32.dp, bottom = 8.dp)
36 | ) {
37 | // Back arrow row
38 | Row(
39 | modifier = Modifier
40 | .fillMaxWidth()
41 | .padding(start = 4.dp),
42 | verticalAlignment = Alignment.CenterVertically
43 | ) {
44 | IconButton(onClick = onBackClick) {
45 | Icon(
46 | Icons.AutoMirrored.Filled.ArrowBack,
47 | contentDescription = "Back",
48 | tint = Color.Black
49 | )
50 | }
51 | book?.title?.let {
52 | Text(
53 | maxLines = 1,
54 | text = it,
55 | style = MaterialTheme.typography.titleMedium,
56 | overflow = TextOverflow.Ellipsis,
57 | color = Color.Black
58 | )
59 | }
60 | }
61 |
62 | // Title and page count
63 | // Column(
64 | // modifier = Modifier
65 | // .fillMaxWidth()
66 | // .padding(horizontal = 24.dp),
67 | // horizontalAlignment = Alignment.CenterHorizontally
68 | // ) {
69 | // book?.title?.let {
70 | // Text(
71 | // maxLines = 1,
72 | // text = it,
73 | // style = MaterialTheme.typography.titleMedium,
74 | // overflow = TextOverflow.Ellipsis,
75 | // color = Color.Black
76 | // )
77 | // }
78 | // }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.settings
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.lifecycle.AndroidViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.ricdev.uread.data.model.AppLanguage
8 | import com.ricdev.uread.data.model.AppPreferences
9 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
10 | import com.ricdev.uread.util.LanguageHelper
11 | import com.ricdev.uread.util.PurchaseHelper
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.asStateFlow
16 | import kotlinx.coroutines.flow.first
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class SettingsViewModel @Inject constructor(
22 | private val appPreferencesUtil: AppPreferencesUtil,
23 | private val languageHelper: LanguageHelper,
24 | application: Application,
25 | ) : AndroidViewModel(application) {
26 |
27 |
28 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
29 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
30 |
31 |
32 | init {
33 | viewModelScope.launch {
34 | appPreferencesUtil.appPreferencesFlow.first().let { initialPreferences ->
35 | _appPreferences.value = initialPreferences
36 | }
37 |
38 | // Continue collecting preferences updates
39 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
40 | _appPreferences.value = preferences
41 | }
42 | }
43 | }
44 |
45 |
46 | fun updatePdfSupport(isPdfSupported: Boolean) {
47 | viewModelScope.launch {
48 | appPreferencesUtil.updateAppPreferences(appPreferences.value.copy(enablePdfSupport = isPdfSupported))
49 | }
50 | }
51 |
52 |
53 | fun addScanDirectory(directory: String) {
54 | viewModelScope.launch {
55 | val currentDirectories = appPreferences.value.scanDirectories
56 | if (!currentDirectories.contains(directory)) {
57 | val updatedDirectories = currentDirectories + directory
58 | Log.d("it's me", "the Settings viewModel")
59 | appPreferencesUtil.updateAppPreferences(appPreferences.value.copy(scanDirectories = updatedDirectories))
60 | }
61 | }
62 | }
63 |
64 | fun removeScanDirectory(directory: String) {
65 | viewModelScope.launch {
66 | val updatedDirectories = appPreferences.value.scanDirectories - directory
67 | Log.d("it's me", "the Settings viewModel")
68 | appPreferencesUtil.updateAppPreferences(appPreferences.value.copy(scanDirectories = updatedDirectories))
69 | }
70 | }
71 |
72 |
73 |
74 |
75 |
76 | fun updateLanguage(languageCode: String) {
77 | viewModelScope.launch {
78 | val language = AppLanguage.fromCode(languageCode)
79 | appPreferencesUtil.updateAppPreferences(appPreferences.value.copy(language = language.code))
80 | languageHelper.changeLanguage(getApplication(), language)
81 | }
82 | }
83 |
84 |
85 |
86 | fun purchasePremium(purchaseHelper: PurchaseHelper) {
87 | purchaseHelper.makePurchase()
88 | viewModelScope.launch {
89 | purchaseHelper.isPremium.collect { isPremium ->
90 | updatePremiumStatus(isPremium)
91 | }
92 | }
93 | }
94 |
95 | private fun updatePremiumStatus(isPremium: Boolean) {
96 | viewModelScope.launch {
97 | val currentPreferences = appPreferences.value
98 | if (currentPreferences.isPremium != isPremium) {
99 | val updatedPreferences = currentPreferences.copy(isPremium = isPremium)
100 | appPreferencesUtil.updateAppPreferences(updatedPreferences)
101 | _appPreferences.value = updatedPreferences
102 | }
103 | }
104 | }
105 |
106 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/settings/states/DeletedBooksState.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.settings.states
2 |
3 | import com.ricdev.uread.data.model.Book
4 |
5 | sealed class DeletedBooksState {
6 | data object Loading : DeletedBooksState()
7 | data class Error(val message: String) : DeletedBooksState()
8 | data class Success(val books: List) : DeletedBooksState()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/settings/viewmodels/AboutViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.settings.viewmodels
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.SavedStateHandle
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.StateFlow
9 | import kotlinx.coroutines.flow.asStateFlow
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class AboutViewModel @Inject constructor(
14 | savedStateHandle: SavedStateHandle,
15 | application: Application,
16 | ): AndroidViewModel(application) {
17 |
18 | private val _isDarkTheme = MutableStateFlow(null)
19 | val isDarkTheme: StateFlow = _isDarkTheme.asStateFlow()
20 |
21 |
22 |
23 |
24 | init {
25 | val isDarkThemeString = savedStateHandle.get("isDarkTheme")
26 | _isDarkTheme.value = isDarkThemeString?.toBoolean()
27 |
28 | }
29 |
30 |
31 |
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/settings/viewmodels/DeletedBooksViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.settings.viewmodels
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.provider.DocumentsContract
7 | import androidx.lifecycle.AndroidViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.ricdev.uread.data.model.Book
10 | import com.ricdev.uread.domain.use_case.books.DeleteBookUseCase
11 | import com.ricdev.uread.domain.use_case.books.GetDeletedBooksUseCase
12 | import com.ricdev.uread.domain.use_case.books.UpdateBookUseCase
13 | import com.ricdev.uread.presentation.settings.states.DeletedBooksState
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.StateFlow
17 | import kotlinx.coroutines.flow.asStateFlow
18 | import kotlinx.coroutines.launch
19 | import java.io.File
20 | import javax.inject.Inject
21 |
22 | @HiltViewModel
23 | class DeletedBooksViewModel @Inject constructor(
24 | private val getDeletedBooksUseCase: GetDeletedBooksUseCase,
25 | private val updateBookUseCase: UpdateBookUseCase,
26 | private val deleteBookUseCase: DeleteBookUseCase,
27 | application: Application,
28 | ) : AndroidViewModel(application) {
29 |
30 | private val _deletedBooksState = MutableStateFlow(DeletedBooksState.Loading)
31 | val deletedBooksState: StateFlow = _deletedBooksState.asStateFlow()
32 |
33 | private val appContext: Context = application.applicationContext
34 |
35 |
36 | init {
37 | getDeletedBooks()
38 | }
39 |
40 |
41 | private fun getDeletedBooks() {
42 | viewModelScope.launch {
43 | try {
44 | getDeletedBooksUseCase().collect { books ->
45 | _deletedBooksState.value = DeletedBooksState.Success(books)
46 | }
47 | } catch (e: Exception) {
48 | _deletedBooksState.value =
49 | DeletedBooksState.Error(e.message ?: "Unknown error occurred")
50 | }
51 | }
52 | }
53 |
54 |
55 | fun restoreBooks(selectedBooks: Set) {
56 | viewModelScope.launch {
57 | selectedBooks.forEach { book ->
58 | val restoredBook = book.copy(deleted = false)
59 | updateBookUseCase(restoredBook)
60 | }
61 | }
62 | }
63 |
64 |
65 | fun permanentlyDeleteBooks(selectedBooks: Set) {
66 | viewModelScope.launch {
67 | selectedBooks.forEach { book ->
68 | val uri = Uri.parse(book.uri)
69 | if (uri.scheme == "content") {
70 | // Use ContentResolver to delete the file
71 | val contentResolver = appContext.contentResolver
72 | val documentUri = DocumentsContract.buildDocumentUriUsingTree(
73 | uri,
74 | DocumentsContract.getDocumentId(uri)
75 | )
76 | if (DocumentsContract.deleteDocument(
77 | contentResolver,
78 | documentUri
79 | )
80 | ) {
81 | deleteBookUseCase(book)
82 | }
83 | } else {
84 | // Handle cases where the URI is not a content URI (e.g., file://)
85 | val bookFile = uri.path?.let { File(it) }
86 | if (bookFile != null) {
87 | if (bookFile.exists() && bookFile.delete()) {
88 | deleteBookUseCase(book)
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/settings/viewmodels/ThemeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.settings.viewmodels
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ricdev.uread.data.model.AppPreferences
7 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
8 | import com.ricdev.uread.util.PurchaseHelper
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 |
17 | @HiltViewModel
18 | class ThemeViewModel @Inject constructor(
19 | private val appPreferencesUtil: AppPreferencesUtil,
20 | application: Application,
21 | ) : AndroidViewModel(application) {
22 |
23 |
24 |
25 |
26 |
27 |
28 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
29 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
30 |
31 |
32 | init {
33 | observeAppPreferences()
34 | }
35 |
36 |
37 | private fun observeAppPreferences() {
38 | viewModelScope.launch {
39 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
40 | _appPreferences.value = preferences
41 | }
42 | }
43 | }
44 |
45 |
46 |
47 |
48 |
49 | fun updateAppPreferences(newPreferences: AppPreferences) {
50 | viewModelScope.launch {
51 | appPreferencesUtil.updateAppPreferences(newPreferences)
52 | _appPreferences.value = newPreferences
53 | }
54 | }
55 |
56 |
57 |
58 |
59 |
60 | fun purchasePremium(purchaseHelper: PurchaseHelper) {
61 | purchaseHelper.makePurchase()
62 | viewModelScope.launch {
63 | purchaseHelper.isPremium.collect { isPremium ->
64 | updatePremiumStatus(isPremium)
65 | }
66 | }
67 | }
68 |
69 | fun updatePremiumStatus(isPremium: Boolean) {
70 | viewModelScope.launch {
71 | val currentPreferences = appPreferences.value
72 | if (currentPreferences.isPremium != isPremium) {
73 | val updatedPreferences = currentPreferences.copy(isPremium = isPremium)
74 | appPreferencesUtil.updateAppPreferences(updatedPreferences)
75 | _appPreferences.value = updatedPreferences
76 | }
77 | }
78 | }
79 |
80 |
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/sharedComponents/CustomNavigationViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.sharedComponents
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.Context
6 | import androidx.lifecycle.AndroidViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.ricdev.uread.data.model.AppPreferences
9 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
10 | import com.ricdev.uread.util.PurchaseHelper
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.asStateFlow
15 | import kotlinx.coroutines.flow.first
16 | import kotlinx.coroutines.launch
17 | import javax.inject.Inject
18 |
19 |
20 |
21 |
22 | @HiltViewModel
23 | class CustomNavigationViewModel @Inject constructor(
24 | private val appPreferencesUtil: AppPreferencesUtil,
25 | application: Application,
26 |
27 | ) : AndroidViewModel(application) {
28 |
29 | private val context: Context
30 | get() = getApplication().applicationContext
31 |
32 |
33 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
34 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
35 |
36 | private val _isDriveConnected = MutableStateFlow(false)
37 | val isDriveConnected: StateFlow = _isDriveConnected.asStateFlow()
38 |
39 |
40 | init {
41 | viewModelScope.launch {
42 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
43 | _appPreferences.value = preferences
44 | }
45 | }
46 | }
47 |
48 |
49 |
50 |
51 | fun updatePremiumStatus(isPremium: Boolean) {
52 | viewModelScope.launch {
53 | val currentPreferences = appPreferencesUtil.appPreferencesFlow.first()
54 | if (currentPreferences.isPremium != isPremium) {
55 | val updatedPreferences = currentPreferences.copy(isPremium = isPremium)
56 | appPreferencesUtil.updateAppPreferences(updatedPreferences)
57 | _appPreferences.value = updatedPreferences
58 | }
59 | }
60 | }
61 |
62 |
63 | fun purchasePremium(purchaseHelper: PurchaseHelper) {
64 | purchaseHelper.makePurchase()
65 | viewModelScope.launch {
66 | purchaseHelper.isPremium.collect { isPremium ->
67 | updatePremiumStatus(isPremium)
68 | }
69 | }
70 | }
71 |
72 |
73 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/sharedComponents/PremiumViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.sharedComponents
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ricdev.uread.data.model.AppPreferences
7 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
8 | import com.ricdev.uread.util.PurchaseHelper
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.flow.first
14 | import kotlinx.coroutines.launch
15 | import javax.inject.Inject
16 |
17 |
18 |
19 |
20 | @HiltViewModel
21 | class PremiumViewModel @Inject constructor(
22 | private val appPreferencesUtil: AppPreferencesUtil,
23 | application: Application,
24 | ) : AndroidViewModel(application) {
25 |
26 |
27 |
28 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
29 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
30 |
31 |
32 | init {
33 | viewModelScope.launch {
34 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
35 | _appPreferences.value = preferences
36 | }
37 | }
38 | }
39 |
40 |
41 |
42 |
43 | fun updatePremiumStatus(isPremium: Boolean) {
44 | viewModelScope.launch {
45 | val currentPreferences = appPreferencesUtil.appPreferencesFlow.first()
46 | if (currentPreferences.isPremium != isPremium) {
47 | val updatedPreferences = currentPreferences.copy(isPremium = isPremium)
48 | appPreferencesUtil.updateAppPreferences(updatedPreferences)
49 | _appPreferences.value = updatedPreferences
50 | }
51 | }
52 | }
53 |
54 |
55 | fun purchasePremium(purchaseHelper: PurchaseHelper, onPurchaseComplete: () -> Unit) {
56 | purchaseHelper.makePurchase()
57 | viewModelScope.launch {
58 | purchaseHelper.isPremium.collect { isPremium ->
59 | updatePremiumStatus(isPremium)
60 | if (isPremium) {
61 | onPurchaseComplete()
62 | }
63 | }
64 | }
65 | }
66 |
67 |
68 | // fun purchasePremium(purchaseHelper: PurchaseHelper) {
69 | // purchaseHelper.makePurchase()
70 | // viewModelScope.launch {
71 | // purchaseHelper.isPremium.collect { isPremium ->
72 | // updatePremiumStatus(isPremium)
73 | // }
74 | // }
75 | // }
76 |
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/sharedComponents/Shelves.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.sharedComponents
2 |
3 |
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Add
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.unit.dp
11 | import androidx.navigation.NavHostController
12 | import com.ricdev.uread.R
13 | import com.ricdev.uread.data.model.AppPreferences
14 | import com.ricdev.uread.data.model.Shelf
15 | import com.ricdev.uread.navigation.Screens
16 | import com.ricdev.uread.presentation.home.HomeViewModel
17 | import com.ricdev.uread.presentation.sharedComponents.dialogs.AddShelfDialog
18 | import com.ricdev.uread.util.PurchaseHelper
19 |
20 | @Composable
21 | fun Shelves(
22 | navController: NavHostController,
23 | viewModel: HomeViewModel,
24 | appPreferences: AppPreferences,
25 | shelves: List,
26 | selectedTab: Int,
27 | onTabSelected: (Int) -> Unit,
28 | onAddShelf: (String) -> Unit,
29 | purchaseHelper: PurchaseHelper,
30 | ) {
31 |
32 | // var showPremiumModal by remember { mutableStateOf(false) }
33 |
34 |
35 | val context = LocalContext.current
36 |
37 | var showAddShelfDialog by remember { mutableStateOf(false) }
38 | var newShelfName by remember { mutableStateOf("") }
39 |
40 | ScrollableTabRow(
41 | selectedTabIndex = selectedTab,
42 | edgePadding = 16.dp
43 | ) {
44 | Tab(
45 | text = { Text(stringResource(R.string.all_books)) },
46 | selected = selectedTab == 0,
47 | onClick = { onTabSelected(0) }
48 | )
49 | shelves.forEachIndexed { index, shelf ->
50 | Tab(
51 | text = { Text(shelf.name) },
52 | selected = selectedTab == index,
53 | onClick = { onTabSelected(index + 1) }
54 | )
55 | }
56 | Tab(
57 | icon = { Icon(imageVector = Icons.Filled.Add, contentDescription = "New Shelf") },
58 | selected = false,
59 | onClick = {
60 | if (shelves.isNotEmpty() && !appPreferences.isPremium) {
61 | navController.navigate(Screens.PremiumScreen.route);
62 | // viewModel.purchasePremium(purchaseHelper)
63 | // showPremiumModal = true
64 | } else {
65 | showAddShelfDialog = true
66 | }
67 | }
68 | )
69 | }
70 |
71 | if (showAddShelfDialog) {
72 | AddShelfDialog(
73 | newShelfName = newShelfName,
74 | onShelfNameChange = { newShelfName = it },
75 | shelves = listOf("All Books") + shelves.map { it.name },
76 | onAddShelf = onAddShelf,
77 | onDismiss = { showAddShelfDialog = false },
78 | context = context
79 | )
80 | }
81 |
82 |
83 | // if (showPremiumModal) {
84 | // PremiumModal(
85 | // purchaseHelper = purchaseHelper,
86 | // hidePremiumModal = { showPremiumModal = false }
87 | // )
88 | // }
89 | }
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/sharedComponents/dialogs/AddShelfDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.sharedComponents.dialogs
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.material3.AlertDialog
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.OutlinedTextField
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TextButton
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 | import com.ricdev.uread.R
16 |
17 | @Composable
18 | fun AddShelfDialog(
19 | newShelfName: String,
20 | onShelfNameChange: (String) -> Unit,
21 | shelves: List,
22 | onAddShelf: (String) -> Unit,
23 | onDismiss: () -> Unit,
24 | context: Context,
25 | ) {
26 | AlertDialog(
27 | onDismissRequest = { onDismiss() },
28 | title = { Text(stringResource(R.string.add_new_shelf)) },
29 | text = {
30 | Column(
31 | verticalArrangement = Arrangement.spacedBy(8.dp)
32 | ) {
33 | OutlinedTextField(
34 | value = newShelfName,
35 | onValueChange = onShelfNameChange,
36 | label = { Text(stringResource(R.string.shelf_name)) }
37 | )
38 |
39 | Text(text = stringResource(R.string.required), style = MaterialTheme.typography.bodySmall)
40 | }
41 | },
42 | confirmButton = {
43 | TextButton(
44 | onClick = {
45 | when {
46 | newShelfName.isBlank() -> {
47 | Toast.makeText(context, "Shelf name is required", Toast.LENGTH_SHORT)
48 | .show()
49 | }
50 |
51 | shelves.any { it.equals(newShelfName, ignoreCase = true) } -> {
52 | Toast.makeText(context, "Shelf name already exists", Toast.LENGTH_SHORT)
53 | .show()
54 | }
55 |
56 | else -> {
57 | onAddShelf(newShelfName)
58 | onShelfNameChange("")
59 | onDismiss()
60 | }
61 | }
62 | }
63 | ) {
64 | Text(stringResource(R.string.add))
65 | }
66 | },
67 | dismissButton = {
68 | TextButton(onClick = { onDismiss() }) {
69 | Text(stringResource(R.string.cancel))
70 | }
71 | }
72 | )
73 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/sharedComponents/dialogs/DeleteShelfDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.sharedComponents.dialogs
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.material3.TextButton
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import com.ricdev.uread.R
10 | import com.ricdev.uread.data.model.Shelf
11 |
12 | @Composable
13 | fun DeleteShelfDialog(
14 | selectedShelf: Shelf?,
15 | onDismiss: () -> Unit,
16 | onConfirmDelete: (Shelf) -> Unit
17 | ) {
18 | AlertDialog(
19 | onDismissRequest = {
20 | onDismiss()
21 | },
22 | confirmButton = {
23 | TextButton(onClick = {
24 | if (selectedShelf != null) {
25 | onConfirmDelete(selectedShelf)
26 | }
27 | onDismiss()
28 | }) {
29 | Text(text = stringResource(R.string.delete), color = MaterialTheme.colorScheme.error)
30 | }
31 | },
32 | dismissButton = {
33 | TextButton(onClick = {
34 | onDismiss()
35 | }) {
36 | Text(text = stringResource(R.string.cancel))
37 | }
38 | },
39 | title = {
40 | Text(text = stringResource(R.string.delete_shelf))
41 | },
42 | text = {
43 | if (selectedShelf != null) {
44 | Text(
45 | text = stringResource(
46 | R.string.are_you_sure_you_want_to_delete,
47 | selectedShelf.name
48 | )
49 | )
50 | }
51 | }
52 | )
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/sharedComponents/dialogs/ReadingDatesDialog.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.sharedComponents.dialogs
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.DatePicker
5 | import androidx.compose.material3.DatePickerDialog
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Text
8 | import androidx.compose.material3.TextButton
9 | import androidx.compose.material3.rememberDatePickerState
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.unit.dp
14 | import com.ricdev.uread.R
15 |
16 | @OptIn(ExperimentalMaterial3Api::class)
17 | @Composable
18 | fun ReadingDatesDialog(
19 | initialDate: Long?,
20 | onDateSelected: (Long) -> Unit,
21 | onDismiss: () -> Unit,
22 | isStartDate: Boolean
23 | ) {
24 | val datePickerState = rememberDatePickerState(
25 | initialSelectedDateMillis = initialDate ?: System.currentTimeMillis()
26 | )
27 |
28 | DatePickerDialog(
29 | onDismissRequest = onDismiss,
30 | confirmButton = {
31 | TextButton(
32 | onClick = {
33 | datePickerState.selectedDateMillis?.let(onDateSelected)
34 | onDismiss()
35 | }
36 | ) {
37 | Text("OK")
38 | }
39 | },
40 | dismissButton = {
41 | TextButton(onClick = onDismiss) {
42 | Text(stringResource(R.string.cancel))
43 | }
44 | }
45 | ) {
46 | DatePicker(
47 | state = datePickerState,
48 | title = {
49 | Text(
50 | if (isStartDate) stringResource(R.string.change_start_reading_date) else stringResource(
51 | R.string.change_end_reading_date
52 | ),
53 | modifier = Modifier.padding(16.dp)
54 | )
55 | }
56 | )
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/shelves/ShelvesState.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.shelves
2 |
3 | import com.ricdev.uread.data.model.Shelf
4 |
5 | sealed class ShelvesState {
6 | data object Loading : ShelvesState()
7 | data class Error(val message: String) : ShelvesState()
8 | data class Success(val shelves: List) : ShelvesState()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/statistics/components/ReadingHeatMap.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.statistics.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.horizontalScroll
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.rememberScrollState
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.unit.dp
19 | import com.ricdev.uread.data.model.ReadingActivity
20 | import java.time.Instant
21 | import java.time.ZoneId
22 | import java.time.temporal.ChronoUnit
23 | import java.util.Calendar
24 |
25 | @Composable
26 | fun ReadingHeatmap(
27 | readingActivities: List,
28 | modifier: Modifier = Modifier
29 | ) {
30 | val currentCalendar = Calendar.getInstance()
31 | val startOfYearCalendar = Calendar.getInstance().apply {
32 | set(Calendar.DAY_OF_YEAR, 1)
33 | set(Calendar.HOUR_OF_DAY, 0)
34 | set(Calendar.MINUTE, 0)
35 | set(Calendar.SECOND, 0)
36 | set(Calendar.MILLISECOND, 0)
37 | }
38 |
39 | val daysInWeek = 7
40 | val totalDays = ChronoUnit.DAYS.between(
41 | startOfYearCalendar.toInstant(),
42 | currentCalendar.toInstant()
43 | ).toInt() + 1
44 | val weeksToShow = totalDays / 7 + 1
45 |
46 | val sortedData = readingActivities.groupBy {
47 | Instant.ofEpochMilli(it.date).atZone(ZoneId.systemDefault()).toLocalDate()
48 | }
49 |
50 | val calendar = startOfYearCalendar.clone() as Calendar
51 |
52 | val scrollState = rememberScrollState()
53 |
54 | LaunchedEffect(Unit) {
55 | scrollState.animateScrollTo(scrollState.maxValue)
56 | }
57 |
58 | Column(modifier = modifier) {
59 | Row(
60 | modifier = Modifier
61 | .padding(top = 8.dp)
62 | .horizontalScroll(scrollState)
63 | ) {
64 | repeat(weeksToShow) {
65 | Column {
66 | repeat(daysInWeek) {
67 | val currentDate = calendar.timeInMillis
68 | if (currentDate <= currentCalendar.timeInMillis) {
69 | val currentLocalDate = Instant.ofEpochMilli(currentDate)
70 | .atZone(ZoneId.systemDefault())
71 | .toLocalDate()
72 | val readingData = sortedData[currentLocalDate] ?: emptyList()
73 | val readingTime = readingData.sumOf { it.readingTime / 60000 } // Convert to minutes
74 |
75 | Box(
76 | modifier = Modifier
77 | .size(20.dp)
78 | .padding(1.dp)
79 | .background(
80 | color = getColorForReadingTime(readingTime),
81 | shape = RoundedCornerShape(2.dp)
82 | )
83 | )
84 | } else {
85 | Spacer(modifier = Modifier.size(16.dp))
86 | }
87 | calendar.add(Calendar.DAY_OF_YEAR, 1)
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
95 | @Composable
96 | fun getColorForReadingTime(readingTimeMinutes: Long): Color {
97 | val baseColor = MaterialTheme.colorScheme.onSurface
98 | return when {
99 | readingTimeMinutes == 0L -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f)
100 | readingTimeMinutes < 15 -> baseColor.copy(alpha = 0.2f)
101 | readingTimeMinutes < 30 -> baseColor.copy(alpha = 0.4f)
102 | readingTimeMinutes < 60 -> baseColor.copy(alpha = 0.6f)
103 | readingTimeMinutes < 120 -> baseColor.copy(alpha = 0.8f)
104 | else -> baseColor
105 | }
106 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/presentation/statistics/components/StatColumn.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.presentation.statistics.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.width
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.text.TextStyle
12 | import androidx.compose.ui.text.style.TextAlign
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun StatColumn(
17 | title: String,
18 | titleStyle: TextStyle = MaterialTheme.typography.bodyLarge,
19 | value: String,
20 | ) {
21 | Column(
22 | horizontalAlignment = Alignment.CenterHorizontally,
23 | verticalArrangement = Arrangement.Center,
24 | modifier = Modifier.width(100.dp)
25 | ) {
26 | Text(
27 | text = title,
28 | style = titleStyle,
29 | textAlign = TextAlign.Center
30 | )
31 | Text(
32 | text = value,
33 | style = MaterialTheme.typography.titleLarge,
34 | color = MaterialTheme.colorScheme.primary
35 | )
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/ui/theme/AppThemeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.ui.theme
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ricdev.uread.data.model.AppPreferences
7 | import com.ricdev.uread.data.source.local.AppPreferencesUtil
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 |
16 |
17 | @HiltViewModel
18 | class AppThemeViewModel @Inject constructor(
19 | private val appPreferencesUtil: AppPreferencesUtil,
20 | application: Application,
21 | ) : AndroidViewModel(application) {
22 |
23 |
24 | private val _appPreferences = MutableStateFlow(AppPreferencesUtil.defaultPreferences)
25 | val appPreferences: StateFlow = _appPreferences.asStateFlow()
26 |
27 | init {
28 | viewModelScope.launch {
29 | appPreferencesUtil.appPreferencesFlow.collect { preferences ->
30 | _appPreferences.value = preferences
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/AppVersion.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import com.ricdev.uread.BuildConfig
4 |
5 | data class AppVersion(
6 | val versionName: String,
7 | val versionNumber: Long,
8 | val releaseDate: String
9 | )
10 |
11 | fun getAppVersion(): AppVersion? {
12 | return try {
13 | AppVersion(
14 | versionName = BuildConfig.VERSION_NAME,
15 | versionNumber = BuildConfig.VERSION_CODE.toLong(),
16 | releaseDate = BuildConfig.RELEASE_DATE
17 | )
18 | } catch (e: Exception) {
19 | null
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/EpubNavigator.kt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/java/com/ricdev/uread/util/EpubNavigator.kt
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/FullScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.DisposableEffect
7 | import androidx.core.view.WindowCompat
8 | import androidx.core.view.WindowInsetsCompat
9 | import androidx.core.view.WindowInsetsControllerCompat
10 |
11 | @Composable
12 | fun SetFullScreen(context: Context, showSystemBars: Boolean) {
13 | val window = (context as? Activity)?.window
14 | val windowInsetsController = WindowCompat.getInsetsController(window!!, window.decorView)
15 |
16 | DisposableEffect(showSystemBars) {
17 | windowInsetsController.systemBarsBehavior =
18 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
19 |
20 | if (showSystemBars) {
21 | windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
22 | windowInsetsController.isAppearanceLightStatusBars = true
23 | windowInsetsController.isAppearanceLightNavigationBars = true
24 | } else {
25 | windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
26 | }
27 |
28 | onDispose {
29 | windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/ImageUtils.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.net.Uri
6 | import java.io.ByteArrayOutputStream
7 | import java.io.File
8 | import java.security.MessageDigest
9 |
10 | private const val HOME_BACKGROUND_PREFIX = "home_bg_"
11 | private const val COVER_PREFIX = "cover_"
12 |
13 | object ImageUtils {
14 |
15 | fun saveHomeBackgroundImage(context: Context, uri: Uri): String? {
16 | return runCatching {
17 | context.contentResolver.openInputStream(uri)?.use { stream ->
18 | val imageBytes = stream.readBytes()
19 | val imageHash = imageBytes.md5Hash()
20 | val fileName = "$HOME_BACKGROUND_PREFIX$imageHash.jpg"
21 |
22 | context.filesDir.findFile(fileName)?.absolutePath
23 | ?: createImageFile(context, fileName, imageBytes)
24 | }
25 | }.getOrNull()
26 | }
27 |
28 | fun listSavedBookCovers(context: Context): List {
29 | return context.filesDir.listFiles { file ->
30 | file.isImageFile() && !file.name.startsWith(HOME_BACKGROUND_PREFIX)
31 | }.orEmpty().toList()
32 | }
33 |
34 | fun saveCoverImage(bitmap: Bitmap, uri: String, context: Context): String? {
35 | return runCatching {
36 | val uriHash = uri.md5Hash()
37 | val imageBytes = bitmap.toByteArray()
38 | val imageHash = imageBytes.md5Hash()
39 | val fileName = "$COVER_PREFIX${uriHash}_$imageHash.jpg"
40 | val file = File(context.filesDir, fileName)
41 |
42 | context.filesDir.listFiles { _, name ->
43 | name.startsWith("$COVER_PREFIX$uriHash") && name != fileName
44 | }?.forEach { it.delete() }
45 |
46 | file.writeBytes(imageBytes)
47 | file.absolutePath
48 | }.getOrNull()
49 | }
50 |
51 | private fun createImageFile(context: Context, fileName: String, bytes: ByteArray): String? {
52 | return File(context.filesDir, fileName).apply {
53 | writeBytes(bytes)
54 | }.takeIf { it.exists() }?.absolutePath
55 | }
56 |
57 | private fun ByteArray.md5Hash(): String = MessageDigest
58 | .getInstance("MD5")
59 | .digest(this)
60 | .joinToString("") { "%02x".format(it) }
61 |
62 | private fun String.md5Hash(): String = MessageDigest
63 | .getInstance("MD5")
64 | .digest(toByteArray())
65 | .joinToString("") { "%02x".format(it) }
66 |
67 | private fun Bitmap.toByteArray(): ByteArray {
68 | return ByteArrayOutputStream().use { stream ->
69 | compress(Bitmap.CompressFormat.JPEG, 90, stream)
70 | stream.toByteArray()
71 | }
72 | }
73 |
74 | private fun File.isImageFile(): Boolean = extension.equals("jpg", ignoreCase = true)
75 |
76 | private fun File.findFile(fileName: String): File? = listFiles { _, name ->
77 | name == fileName
78 | }?.firstOrNull()
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/KeepScreenOn.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.ui.platform.LocalView
6 |
7 | @Composable
8 | fun KeepScreenOn(keepScreenOn: Boolean) {
9 | val currentView = LocalView.current
10 | DisposableEffect(keepScreenOn) {
11 | currentView.keepScreenOn = keepScreenOn
12 | onDispose { currentView.keepScreenOn = false }
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/LanguageHelper.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import android.app.LocaleManager
4 | import android.content.Context
5 | import android.content.res.Resources
6 | import android.os.Build
7 | import android.os.LocaleList
8 | import android.util.Log
9 | import androidx.appcompat.app.AppCompatDelegate
10 | import androidx.core.os.LocaleListCompat
11 | import com.ricdev.uread.data.model.AppLanguage
12 | import java.util.Locale
13 |
14 | //class LanguageHelper {
15 | // fun changeLanguage(context: Context, languageCode: String) {
16 | // val locale = try{
17 | // when (languageCode) {
18 | // "system" -> Resources.getSystem().configuration.locales[0]
19 | // else -> Locale.forLanguageTag(languageCode)
20 | // }
21 | // } catch (e: Exception){
22 | // // Fallback to locale if invalid
23 | // Resources.getSystem().configuration.locales[0]
24 | // }
25 | //
26 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
27 | // context.getSystemService(LocaleManager::class.java).applicationLocales = LocaleList(locale)
28 | // } else {
29 | // AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(locale))
30 | // }
31 | // }
32 | //}
33 |
34 |
35 |
36 | //Experimental
37 | class LanguageHelper {
38 | fun changeLanguage(context: Context, language: AppLanguage) {
39 | val locale = when (language) {
40 | AppLanguage.SYSTEM -> {
41 | // Use LocaleManager to get the system default locale
42 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
43 | val localeManager = context.getSystemService(LocaleManager::class.java)
44 | localeManager.systemLocales.get(0) ?: Locale.getDefault()
45 | } else {
46 | // Fallback for older versions
47 | Locale.getDefault()
48 | }
49 | }
50 | else -> Locale.forLanguageTag(language.code)
51 | }
52 |
53 | try {
54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
55 | val localeManager = context.getSystemService(LocaleManager::class.java)
56 |
57 | // If selecting system language, use empty LocaleList to reset to system default
58 | val localeList = if (language == AppLanguage.SYSTEM) {
59 | LocaleList.getEmptyLocaleList()
60 | } else {
61 | LocaleList(locale)
62 | }
63 |
64 | localeManager.applicationLocales = localeList
65 | } else {
66 | AppCompatDelegate.setApplicationLocales(
67 | if (language == AppLanguage.SYSTEM) {
68 | LocaleListCompat.getEmptyLocaleList()
69 | } else {
70 | LocaleListCompat.create(locale)
71 | }
72 | )
73 | }
74 | } catch (e: Exception) {
75 | Log.e("LanguageHelper", "Failed to change language", e)
76 | }
77 | }
78 |
79 |
80 | // Context wrapper for more robust locale handling
81 | fun updateBaseContextLocale(context: Context,language: AppLanguage): Context {
82 | val locale = when (language) {
83 | AppLanguage.SYSTEM -> Resources.getSystem().configuration.locales[0]
84 | else -> Locale.forLanguageTag(language.code)
85 | }
86 | val configuration = context.resources.configuration
87 | configuration.setLocale(locale)
88 | return context.createConfigurationContext(configuration)
89 | }
90 |
91 |
92 |
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/PdfBitmapConverter.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.graphics.pdf.PdfRenderer
6 | import android.net.Uri
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import java.io.IOException
10 | import javax.inject.Inject
11 |
12 | class PdfBitmapConverter @Inject constructor(
13 | private val context: Context
14 | ) {
15 | suspend fun getPageCount(contentUri: Uri): Int {
16 | return withContext(Dispatchers.IO) {
17 | try {
18 | context.contentResolver.openFileDescriptor(contentUri, "r")?.use { descriptor ->
19 | PdfRenderer(descriptor).use { renderer ->
20 | renderer.pageCount
21 | }
22 | } ?: throw IOException("Unable to open PDF file")
23 | } catch (e: Exception) {
24 | throw IOException("Failed to get page count: ${e.message}", e)
25 | }
26 | }
27 | }
28 |
29 | suspend fun pdfToBitmap(contentUri: Uri, pageIndex: Int, scaleFactor: Float = 2f): Bitmap {
30 | return withContext(Dispatchers.IO) {
31 | try {
32 | context.contentResolver.openFileDescriptor(contentUri, "r")?.use { descriptor ->
33 | PdfRenderer(descriptor).use { renderer ->
34 | if (pageIndex < 0 || pageIndex >= renderer.pageCount) {
35 | throw IndexOutOfBoundsException("Invalid page index: $pageIndex")
36 | }
37 | renderer.openPage(pageIndex).use { page ->
38 | val width = (page.width * scaleFactor).toInt()
39 | val height = (page.height * scaleFactor).toInt()
40 |
41 | val bitmap = Bitmap.createBitmap(
42 | width,
43 | height,
44 | Bitmap.Config.ARGB_8888
45 | )
46 | page.render(
47 | bitmap,
48 | null,
49 | null,
50 | PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
51 | )
52 | bitmap
53 | }
54 | }
55 | } ?: throw IOException("Unable to open PDF file")
56 | } catch (e: Exception) {
57 | throw IOException("Failed to render page $pageIndex: ${e.message}", e)
58 | }
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/PermissionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import androidx.activity.result.ActivityResultLauncher
8 | import androidx.core.content.ContextCompat
9 |
10 | object PermissionHandler {
11 | fun hasPermissions(context: Context): Boolean {
12 | return when {
13 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
14 | ContextCompat.checkSelfPermission(
15 | context,
16 | Manifest.permission.READ_MEDIA_IMAGES
17 | ) == PackageManager.PERMISSION_GRANTED
18 | }
19 | else -> {
20 | ContextCompat.checkSelfPermission(
21 | context,
22 | Manifest.permission.READ_EXTERNAL_STORAGE
23 | ) == PackageManager.PERMISSION_GRANTED
24 | }
25 | }
26 | }
27 |
28 | fun requestPermissions(
29 | permissionLauncher: ActivityResultLauncher>
30 | ) {
31 | when {
32 | // Android 14+ (API 34)
33 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> {
34 | permissionLauncher.launch(arrayOf(
35 | Manifest.permission.READ_MEDIA_IMAGES,
36 | Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
37 | ))
38 | }
39 | // Android 13 (API 33)
40 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
41 | permissionLauncher.launch(arrayOf(
42 | Manifest.permission.READ_MEDIA_IMAGES
43 | ))
44 | }
45 | // Android 12L and below
46 | else -> {
47 | permissionLauncher.launch(arrayOf(
48 | Manifest.permission.READ_EXTERNAL_STORAGE
49 | ))
50 | }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ricdev/uread/util/customMarkdownTypography.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread.util
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.text.SpanStyle
6 | import androidx.compose.ui.text.TextStyle
7 | import androidx.compose.ui.text.font.FontFamily
8 | import androidx.compose.ui.text.font.FontStyle
9 | import com.mikepenz.markdown.model.DefaultMarkdownTypography
10 | import com.mikepenz.markdown.model.MarkdownTypography
11 |
12 | @Composable
13 | fun customMarkdownTypography(
14 | h1: TextStyle = MaterialTheme.typography.headlineLarge,
15 | h2: TextStyle = MaterialTheme.typography.headlineMedium,
16 | h3: TextStyle = MaterialTheme.typography.headlineSmall,
17 | h4: TextStyle = MaterialTheme.typography.titleLarge,
18 | h5: TextStyle = MaterialTheme.typography.titleMedium,
19 | h6: TextStyle = MaterialTheme.typography.titleSmall,
20 | text: TextStyle = MaterialTheme.typography.bodyLarge,
21 | code: TextStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
22 | quote: TextStyle = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)),
23 | paragraph: TextStyle = MaterialTheme.typography.bodyLarge,
24 | ordered: TextStyle = MaterialTheme.typography.bodyLarge,
25 | bullet: TextStyle = MaterialTheme.typography.bodyLarge,
26 | list: TextStyle = MaterialTheme.typography.bodyLarge
27 | ): MarkdownTypography = DefaultMarkdownTypography(
28 | h1 = h1, h2 = h2, h3 = h3, h4 = h4, h5 = h5, h6 = h6,
29 | text = text, quote = quote, code = code, paragraph = paragraph,
30 | ordered = ordered, bullet = bullet, list = list
31 | )
--------------------------------------------------------------------------------
/app/src/main/python/edit_metadata.py:
--------------------------------------------------------------------------------
1 | import ebooklib
2 | from ebooklib import epub
3 | from io import BytesIO
4 | import tempfile
5 | import os
6 | import json
7 |
8 | def edit_metadata(file_contents, title=None, authors=None, description=None):
9 | temp_file_path = None
10 | try:
11 | with tempfile.NamedTemporaryFile(delete=False, suffix='.epub') as temp_file:
12 | temp_file.write(file_contents)
13 | temp_file_path = temp_file.name
14 |
15 | book = epub.read_epub(temp_file_path)
16 |
17 | if title:
18 | book.set_title(title)
19 |
20 | if authors:
21 | if 'DC' not in book.metadata:
22 | book.metadata['DC'] = {}
23 | book.metadata['DC']['creator'] = []
24 | for author in authors.split(','):
25 | book.add_author(author.strip())
26 |
27 | if description:
28 | if 'DC' not in book.metadata:
29 | book.metadata['DC'] = {}
30 | book.add_metadata('DC', 'description', description)
31 |
32 | output = BytesIO()
33 | epub.write_epub(output, book)
34 | return output.getvalue()
35 | except Exception as e:
36 | print(f"Error editing metadata: {e}")
37 | raise
38 | finally:
39 | if temp_file_path and os.path.exists(temp_file_path):
40 | os.unlink(temp_file_path)
41 |
42 | def get_metadata(file_contents):
43 | temp_file_path = None
44 | try:
45 | with tempfile.NamedTemporaryFile(delete=False, suffix='.epub') as temp_file:
46 | temp_file.write(file_contents)
47 | temp_file_path = temp_file.name
48 |
49 | book = epub.read_epub(temp_file_path)
50 |
51 | title = book.get_metadata('DC', 'title')
52 | title = title[0][0] if title else ''
53 |
54 | authors = book.get_metadata('DC', 'creator')
55 | authors = ', '.join([author[0] for author in authors]) if authors else ''
56 |
57 | description = book.get_metadata('DC', 'description')
58 | description = description[0][0] if description else ''
59 |
60 | metadata = {
61 | 'title': title,
62 | 'authors': authors,
63 | 'description': description
64 | }
65 |
66 | return json.dumps(metadata)
67 | except Exception as e:
68 | print(f"Error getting metadata: {e}")
69 | raise
70 | finally:
71 | if temp_file_path and os.path.exists(temp_file_path):
72 | os.unlink(temp_file_path)
--------------------------------------------------------------------------------
/app/src/main/python/mobi_converter.py:
--------------------------------------------------------------------------------
1 | import mobi
2 | import os
3 | import shutil
4 |
5 | def convert_mobi_to_epub(input_path, output_path):
6 | try:
7 | # Extract the mobi file
8 | tempdir, filepath = mobi.extract(input_path)
9 |
10 | # Check if the extracted file is already an epub
11 | if filepath.endswith('.epub'):
12 | # Move the file to the output path
13 | shutil.move(filepath, output_path)
14 | else:
15 | # If it's not an epub, we can't convert it directly
16 | # You might want to implement additional conversion steps here
17 | # For now, we'll just return False to indicate failure
18 | shutil.rmtree(tempdir)
19 | return False
20 |
21 | # Clean up the temporary directory
22 | shutil.rmtree(tempdir)
23 | return True
24 | except Exception as e:
25 | print(f"Error converting file: {str(e)}")
26 | return False
--------------------------------------------------------------------------------
/app/src/main/res/drawable/broken_crown.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/crown.xml:
--------------------------------------------------------------------------------
1 |
6 |
14 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/globe.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | #171717
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/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/com/ricdev/uread/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.ricdev.uread
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 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.jetbrains.kotlin.android) apply false
5 | alias(libs.plugins.compose.compiler) apply false
6 | // id("com.google.devtools.ksp") version "2.0.20-1.0.25" apply false
7 | id("com.google.devtools.ksp") version "2.1.10-1.0.30" apply false
8 | id("com.google.dagger.hilt.android") version "2.51.1" apply false
9 | id("androidx.room") version "2.6.1" apply false
10 |
11 |
12 | id("com.mikepenz.aboutlibraries.plugin") version "11.2.3" apply false
13 | alias(libs.plugins.google.gms.google.services) apply false
14 | alias(libs.plugins.google.firebase.crashlytics) apply false
15 |
16 | // id("com.chaquo.python") version "15.0.1" apply false
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/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. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-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/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 09 15:05:02 CET 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.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 |
--------------------------------------------------------------------------------
/kls_database.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/kls_database.db
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 |
3 | repositories {
4 | google {
5 | content {
6 | includeGroupByRegex("com\\.android.*")
7 | includeGroupByRegex("com\\.google.*")
8 | includeGroupByRegex("androidx.*")
9 | }
10 | }
11 | mavenCentral()
12 | gradlePluginPortal()
13 | maven { url = uri("https://jitpack.io") }
14 | }
15 | }
16 | dependencyResolutionManagement {
17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18 | repositories {
19 | google()
20 | mavenCentral()
21 | maven { url = uri("https://jitpack.io") }
22 | gradlePluginPortal()
23 | }
24 | }
25 |
26 | rootProject.name = "uRead"
27 | include(":app")
28 |
--------------------------------------------------------------------------------