├── .github ├── Licence.svg ├── app-icon.png └── screenshots │ ├── screenshot1.png │ ├── screenshot2.png │ ├── screenshot3.png │ ├── screenshot4.png │ ├── screenshot5.png │ ├── screenshot6.png │ └── screenshot7.png ├── .gitignore ├── .idea ├── .gitignore ├── appInsightsSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── .kotlin └── errors │ ├── errors-1724804959538.log │ ├── errors-1724848835609.log │ ├── errors-1724848857106.log │ ├── errors-1724848972131.log │ ├── errors-1724864528206.log │ ├── errors-1725047117655.log │ ├── errors-1725047130352.log │ ├── errors-1725228490754.log │ ├── errors-1725648205153.log │ ├── errors-1725648225392.log │ ├── errors-1725648308578.log │ ├── errors-1726053915578.log │ ├── errors-1726062896147.log │ ├── errors-1726684643036.log │ ├── errors-1726927591762.log │ ├── errors-1727800762174.log │ ├── errors-1732750141503.log │ ├── errors-1732750161540.log │ ├── errors-1732750224974.log │ ├── errors-1732887228181.log │ ├── errors-1739659484266.log │ ├── errors-1739659505461.log │ └── errors-1740008010777.log ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── release │ └── app-release.aab ├── schemas │ └── com.ricdev.uread.data.source.local.AppDatabase │ │ └── 1.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ricdev │ │ └── uread │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── annotation-icon.svg │ │ ├── books │ │ │ ├── alice_in_wonderlands.epub │ │ │ └── romeo_and_juliet.epub │ │ ├── broken-crown.svg │ │ ├── crown.svg │ │ ├── documentation │ │ │ ├── CHANGELOG.md │ │ │ └── PRIVACY_POLICY.md │ │ └── github.svg │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── ricdev │ │ │ └── uread │ │ │ ├── BookApplication.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SplashViewModel.kt │ │ │ ├── data │ │ │ ├── model │ │ │ │ ├── AppLanguage.kt │ │ │ │ ├── AppPreferences.kt │ │ │ │ ├── AppTheme.kt │ │ │ │ ├── Book.kt │ │ │ │ ├── BookAnnotation.kt │ │ │ │ ├── BookShelf.kt │ │ │ │ ├── Bookmark.kt │ │ │ │ ├── Note.kt │ │ │ │ ├── ReaderPreferences.kt │ │ │ │ ├── ReadingActivity.kt │ │ │ │ └── Shelf.kt │ │ │ ├── repository │ │ │ │ ├── BooksRepositoryImpl.kt │ │ │ │ └── ShelfRepositoryImpl.kt │ │ │ └── source │ │ │ │ └── local │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── AppPreferencesUtil.kt │ │ │ │ ├── ReaderPreferencesUtil.kt │ │ │ │ └── dao │ │ │ │ ├── AnnotationDao.kt │ │ │ │ ├── BookDao.kt │ │ │ │ ├── BookShelfDao.kt │ │ │ │ ├── BookmarkDao.kt │ │ │ │ ├── NoteDao.kt │ │ │ │ ├── ReadingActivityDao.kt │ │ │ │ └── ShelfDao.kt │ │ │ ├── di │ │ │ ├── ActivityModule.kt │ │ │ └── AppModule.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ ├── Author.kt │ │ │ │ ├── DecorationStyleAnnotationMark.kt │ │ │ │ ├── Genre.kt │ │ │ │ └── Statistics.kt │ │ │ ├── repository │ │ │ │ ├── BooksRepository.kt │ │ │ │ └── ShelfRepository.kt │ │ │ └── use_case │ │ │ │ ├── annotations │ │ │ │ ├── AddAnnotationUseCase.kt │ │ │ │ ├── DeleteAnnotationUseCase.kt │ │ │ │ ├── GetAllAnnotationsUseCase.kt │ │ │ │ ├── GetAnnotationsUseCase.kt │ │ │ │ └── UpdateAnnotationUseCase.kt │ │ │ │ ├── bookmarks │ │ │ │ ├── AddBookmarkUseCase.kt │ │ │ │ ├── DeleteBookmarkUseCase.kt │ │ │ │ ├── GetAllBookmarksUseCase.kt │ │ │ │ ├── GetBookmarksForBookUseCase.kt │ │ │ │ └── UpdateBookmarkUseCase.kt │ │ │ │ ├── books │ │ │ │ ├── DeleteBookByUriUseCase.kt │ │ │ │ ├── DeleteBookUseCase.kt │ │ │ │ ├── GetAllBooksUseCase.kt │ │ │ │ ├── GetBookByIdUseCase.kt │ │ │ │ ├── GetBookUrisUseCase.kt │ │ │ │ ├── GetBooksUseCase.kt │ │ │ │ ├── GetDeletedBooksUseCase.kt │ │ │ │ ├── InsertBookUseCase.kt │ │ │ │ └── UpdateBookUseCase.kt │ │ │ │ ├── notes │ │ │ │ ├── AddNoteUseCase.kt │ │ │ │ ├── DeleteNoteUseCase.kt │ │ │ │ ├── GetAllNotesUseCase.kt │ │ │ │ ├── GetNotesForBookUseCase.kt │ │ │ │ └── UpdateNoteUseCase.kt │ │ │ │ ├── reading_activity │ │ │ │ ├── AddReadingActivityUseCase.kt │ │ │ │ ├── GetAllReadingActivitiesUseCase.kt │ │ │ │ └── GetReadingActivityByDateUseCase.kt │ │ │ │ ├── reading_progress │ │ │ │ ├── GetReadingProgressUseCase.kt │ │ │ │ └── SetReadingProgressUseCase.kt │ │ │ │ └── shelves │ │ │ │ ├── AddBookToShelfUseCase.kt │ │ │ │ ├── AddShelfUseCase.kt │ │ │ │ ├── GetBooksForShelfUseCase.kt │ │ │ │ ├── GetShelvesUseCase.kt │ │ │ │ ├── RemoveBooksFromShelfUseCase.kt │ │ │ │ ├── RemoveShelfUseCase.kt │ │ │ │ └── UpdateShelfUseCase.kt │ │ │ ├── navigation │ │ │ ├── Screens.kt │ │ │ └── SetupNavGraph.kt │ │ │ ├── presentation │ │ │ ├── annotations │ │ │ │ ├── AnnotationsScreen.kt │ │ │ │ └── AnnotationsViewModel.kt │ │ │ ├── audioBookReader │ │ │ │ ├── AudiobookReaderScreen.kt │ │ │ │ ├── AudiobookReaderState.kt │ │ │ │ └── AudiobookReaderViewModel.kt │ │ │ ├── bookDetails │ │ │ │ ├── BookDetailsViewModel.kt │ │ │ │ ├── bookDetailsScreen.kt │ │ │ │ └── components │ │ │ │ │ ├── BookDescription.kt │ │ │ │ │ ├── BookReview.kt │ │ │ │ │ ├── EditMetadataModal.kt │ │ │ │ │ ├── ReadingProgress.kt │ │ │ │ │ └── ReadingStats.kt │ │ │ ├── bookReader │ │ │ │ ├── BookReaderScreen.kt │ │ │ │ ├── BookReaderUiState.kt │ │ │ │ ├── BookReaderViewModel.kt │ │ │ │ ├── components │ │ │ │ │ ├── TextToolbar.kt │ │ │ │ │ ├── TtsPlayer.kt │ │ │ │ │ ├── dialogs │ │ │ │ │ │ ├── NoteContent.kt │ │ │ │ │ │ └── NoteDialog.kt │ │ │ │ │ ├── drawers │ │ │ │ │ │ ├── AnnotationsDrawer.kt │ │ │ │ │ │ ├── BookmarksDrawer.kt │ │ │ │ │ │ ├── ChaptersDrawer.kt │ │ │ │ │ │ └── NotesDrawer.kt │ │ │ │ │ ├── modals │ │ │ │ │ │ ├── FontSettings.kt │ │ │ │ │ │ ├── PageSettings.kt │ │ │ │ │ │ ├── ReaderSettings.kt │ │ │ │ │ │ └── UiSettings.kt │ │ │ │ │ └── toolbars │ │ │ │ │ │ ├── BottomToolbar.kt │ │ │ │ │ │ └── TopToolbar.kt │ │ │ │ └── util │ │ │ │ │ └── SelectionActionMode.kt │ │ │ ├── bookShelf │ │ │ │ └── BookShelfScreen.kt │ │ │ ├── gettingStarted │ │ │ │ ├── GettingStarted.kt │ │ │ │ ├── GettingStartedViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── ActionButtons.kt │ │ │ │ │ └── StorageAccessDialog.kt │ │ │ ├── home │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ ├── components │ │ │ │ │ ├── BookCard.kt │ │ │ │ │ ├── BookListCard.kt │ │ │ │ │ ├── CustomBottomAppBar.kt │ │ │ │ │ ├── CustomSnackbar.kt │ │ │ │ │ ├── CustomTopAppBar.kt │ │ │ │ │ ├── GridLayout.kt │ │ │ │ │ ├── HomeFloatingActionButton.kt │ │ │ │ │ ├── LayoutModal.kt │ │ │ │ │ ├── ListLayout.kt │ │ │ │ │ ├── SortFilterModal.kt │ │ │ │ │ └── StarRating.kt │ │ │ │ └── states │ │ │ │ │ ├── ImportProgressState.kt │ │ │ │ │ └── SnackbarState.kt │ │ │ ├── notes │ │ │ │ ├── NotesScreen.kt │ │ │ │ └── NotesViewModel.kt │ │ │ ├── onlineBooks │ │ │ │ ├── OnlineBooksScreen.kt │ │ │ │ ├── WebViewScreen.kt │ │ │ │ └── WebViewScreenViewModel.kt │ │ │ ├── pdfReader │ │ │ │ ├── PdfReaderScreen.kt │ │ │ │ ├── PdfReaderViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── PdfReaderBottomBar.kt │ │ │ │ │ └── PdfReaderTopBar.kt │ │ │ ├── settings │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ ├── components │ │ │ │ │ ├── AboutAppScreen.kt │ │ │ │ │ ├── DeletedBooksScreen.kt │ │ │ │ │ ├── GeneralSettings.kt │ │ │ │ │ ├── SegmentedButtons.kt │ │ │ │ │ └── ThemeScreen.kt │ │ │ │ ├── states │ │ │ │ │ └── DeletedBooksState.kt │ │ │ │ └── viewmodels │ │ │ │ │ ├── AboutViewModel.kt │ │ │ │ │ ├── DeletedBooksViewModel.kt │ │ │ │ │ └── ThemeViewModel.kt │ │ │ ├── sharedComponents │ │ │ │ ├── CustomNavigationDrawer.kt │ │ │ │ ├── CustomNavigationViewModel.kt │ │ │ │ ├── PremiumScreen.kt │ │ │ │ ├── PremiumViewModel.kt │ │ │ │ ├── Shelves.kt │ │ │ │ └── dialogs │ │ │ │ │ ├── AddShelfDialog.kt │ │ │ │ │ ├── DeleteShelfDialog.kt │ │ │ │ │ ├── RatingDialog.kt │ │ │ │ │ ├── ReadingDatesDialog.kt │ │ │ │ │ └── ReadingStatusDialog.kt │ │ │ ├── shelves │ │ │ │ ├── ShelvesScreen.kt │ │ │ │ ├── ShelvesState.kt │ │ │ │ └── ShelvesViewModel.kt │ │ │ └── statistics │ │ │ │ ├── StatisticsScreen.kt │ │ │ │ ├── StatisticsViewModel.kt │ │ │ │ └── components │ │ │ │ ├── ReadingGraph.kt │ │ │ │ ├── ReadingHeatMap.kt │ │ │ │ └── StatColumn.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── AppThemeViewModel.kt │ │ │ │ ├── ColorSchemes.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── util │ │ │ ├── AppVersion.kt │ │ │ ├── ColorPicker.kt │ │ │ ├── EpubNavigator.kt │ │ │ ├── FullScreen.kt │ │ │ ├── ImageUtils.kt │ │ │ ├── KeepScreenOn.kt │ │ │ ├── LanguageHelper.kt │ │ │ ├── PdfBitmapConverter.kt │ │ │ ├── PermissionHandler.kt │ │ │ ├── PurchaseHelper.kt │ │ │ └── customMarkdownTypography.kt │ ├── python │ │ ├── edit_metadata.py │ │ └── mobi_converter.py │ └── res │ │ ├── drawable │ │ ├── broken_crown.xml │ │ ├── crown.xml │ │ ├── github.xml │ │ ├── globe.xml │ │ └── splash_icon.xml │ │ ├── mipmap-anydpi │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-hi │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-ko │ │ └── strings.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-nl │ │ └── strings.xml │ │ ├── values-pt │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-sv │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-zh │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── ricdev │ └── uread │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kls_database.db └── settings.gradle.kts /.github/Licence.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/app-icon.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot1.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot2.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot3.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot4.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot5.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot6.png -------------------------------------------------------------------------------- /.github/screenshots/screenshot7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rics-Dev/uRead/ffd23e162484ff36d757f4b6775339ea08ddc0ee/.github/screenshots/screenshot7.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | apikey.properties 17 | app/src/main/res/values/appId.xml 18 | app/google-services.json 19 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 40 | 41 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1724804959538.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1724848972131.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1725228490754.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1725648308578.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1726062896147.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1726684643036.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1726927591762.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1732887228181.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1740008010777.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.1.20-RC 2 | error message: java.lang.NoSuchMethodError: 'org.jetbrains.kotlin.config.LanguageVersionSettings org.jetbrains.kotlin.codegen.state.KotlinTypeMapper$Companion.getLANGUAGE_VERSION_SETTINGS_DEFAULT()' 3 | at com.google.devtools.ksp.processing.impl.ResolverImpl.(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 | Get it on Google Play 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 | [![GNU GPLv3 License](https://raw.githubusercontent.com/Rics-Dev/uRead/main/.github/Licence.svg)](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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/assets/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------