diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9dc1af8..4b5e05db 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,10 +2,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.devtools.ksp") - id("com.google.dagger.hilt.android") - kotlin("kapt") - id("kotlin-parcelize") } @@ -18,7 +15,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "0.8.0" + versionName = "0.9.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -62,35 +59,35 @@ android { } dependencies { - val composeVersion = "1.5.7" // Default implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation("androidx.activity:activity-compose:1.8.2") + // Compose BOM libraries implementation(platform("androidx.compose:compose-bom:2024.01.00")) - implementation("androidx.compose.ui:ui:$composeVersion") - implementation("androidx.compose.ui:ui-graphics:$composeVersion") - implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") - implementation("androidx.compose.ui:ui-android:1.6.0") - implementation("androidx.compose.material3:material3:1.1.2") - implementation("androidx.compose.material:material:1.6.0") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.ui:ui-android") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material3:material3-window-size-class") + implementation("androidx.compose.material:material") // Unit tests testImplementation("androidx.test:core:latest.release") testImplementation("junit:junit:latest.release") testImplementation("androidx.arch.core:core-testing:latest.release") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release") - testImplementation("com.google.truth:truth:1.2.0") + testImplementation("com.google.truth:truth:1.4.2") testImplementation("androidx.test.ext:truth:1.5.0") testImplementation("com.squareup.okhttp3:mockwebserver:latest.release") testImplementation("io.mockk:mockk:latest.release") - debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.3") // Instrumentation tests androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release") - kaptAndroidTest("com.google.dagger:hilt-android-compiler:latest.release") androidTestImplementation("junit:junit:latest.release") androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release") androidTestImplementation("androidx.arch.core:core-testing:latest.release") @@ -102,17 +99,16 @@ dependencies { androidTestImplementation("androidx.test:runner:latest.release") // All dependencies - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") - implementation("androidx.compose.material:material-icons-extended:1.5.4") - implementation("androidx.activity:activity-compose:1.8.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.compose.material:material-icons-extended") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") // Dagger - Hilt - implementation("com.google.dagger:hilt-android:2.50") - kapt("com.google.dagger:hilt-android-compiler:2.50") + implementation("com.google.dagger:hilt-android:2.51") + ksp("com.google.dagger:hilt-android-compiler:2.51") implementation("com.google.dagger:hilt-compiler:latest.release") - kapt("androidx.hilt:hilt-compiler:latest.release") - implementation("androidx.hilt:hilt-navigation-compose:1.1.0") + ksp("androidx.hilt:hilt-compiler:latest.release") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // Room implementation("androidx.room:room-runtime:2.6.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b438d2f9..7d1dab8b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,21 @@ android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> + + + + + + + + + + + + + + + - - - - \ No newline at end of file diff --git a/app/src/main/app_icon-playstore.png b/app/src/main/app_icon-playstore.png new file mode 100644 index 00000000..62d5d399 Binary files /dev/null and b/app/src/main/app_icon-playstore.png differ diff --git a/app/src/main/app_icon_monochrome-playstore.png b/app/src/main/app_icon_monochrome-playstore.png new file mode 100644 index 00000000..26220dc8 Binary files /dev/null and b/app/src/main/app_icon_monochrome-playstore.png differ diff --git a/app/src/main/java/ua/acclorite/book_story/MainActivity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt similarity index 58% rename from app/src/main/java/ua/acclorite/book_story/MainActivity.kt rename to app/src/main/java/ua/acclorite/book_story/Activity.kt index e33f3564..b8ccfa15 100644 --- a/app/src/main/java/ua/acclorite/book_story/MainActivity.kt +++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt @@ -1,67 +1,104 @@ package ua.acclorite.book_story +import android.annotation.SuppressLint import android.content.res.Configuration +import android.database.CursorWindow import android.os.Bundle +import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.slideInHorizontally import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import dagger.hilt.android.AndroidEntryPoint import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.presentation.NavigationHost -import ua.acclorite.book_story.presentation.Screen -import ua.acclorite.book_story.presentation.components.BottomNavigationBar +import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.BottomNavigationBar +import ua.acclorite.book_story.presentation.components.custom_navigation_rail.CustomNavigationRail import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.presentation.data.NavigationHost +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen +import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.history.HistoryScreen +import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.library.LibraryScreen +import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.reader.ReaderScreen import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings +import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettings import ua.acclorite.book_story.ui.BooksHistoryResurrectionTheme +import ua.acclorite.book_story.ui.DarkTheme +import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.isDark +import java.lang.reflect.Field + +@SuppressLint("DiscouragedPrivateApi") +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint -class MainActivity : AppCompatActivity() { +class Activity : ComponentActivity() { private val mainViewModel: MainViewModel by viewModels() + private val libraryViewModel: LibraryViewModel by viewModels() + private val historyViewModel: HistoryViewModel by viewModels() + private val browseViewModel: BrowseViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - mainViewModel.init(this) + // New Cursor size for Room + try { + val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize") + field.isAccessible = true + field.set(null, 100 * 1024 * 1024) + } catch (e: Exception) { + e.printStackTrace() + } + + // Initializing all variables + mainViewModel.init( + activity = this, + libraryViewModel, + ) + + // Splash screen installSplashScreen().apply { setKeepOnScreenCondition { !mainViewModel.isReady.value } } + // Edge to edge enableEdgeToEdge() WindowCompat.setDecorFitsSystemWindows(window, false) setContent { + val windowClass = calculateWindowSizeClass(activity = this) val updating = mainViewModel.updating.collectAsState().value - val theme = mainViewModel.theme.collectAsState().value!! - val darkTheme = mainViewModel.darkTheme.collectAsState().value!! + + val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE + val darkTheme = + mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM + val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact BooksHistoryResurrectionTheme( theme = theme, @@ -70,28 +107,36 @@ class MainActivity : AppCompatActivity() { if (!updating) { NavigationHost(startScreen = Screen.LIBRARY) { val currentScreen by this.getCurrentScreen().collectAsState() + AnimatedVisibility( visible = currentScreen == Screen.LIBRARY || currentScreen == Screen.HISTORY || currentScreen == Screen.BROWSE, - enter = fadeIn(tween(200)) + - slideInHorizontally(tween(200)) { it / 15 }, + enter = Transitions.BackSlidingTransitionIn, exit = Transitions.SlidingTransitionOut ) { Scaffold( bottomBar = { - BottomNavigationBar(navigator = this@NavigationHost) + if (!tabletUI) { + BottomNavigationBar(navigator = this@NavigationHost) + } }, containerColor = MaterialTheme.colorScheme.surface ) { Box( modifier = Modifier .fillMaxSize() - .padding(bottom = it.calculateBottomPadding()) + .padding( + start = if (tabletUI) 80.dp else 0.dp, + bottom = it.calculateBottomPadding() + ) ) { composable(screen = Screen.LIBRARY) { @Suppress("UNCHECKED_CAST") LibraryScreen( + viewModel = libraryViewModel, + historyViewModel = historyViewModel, + browseViewModel = browseViewModel, navigator = this@NavigationHost, addedBooks = retrieveArgument("added_books") as? List ?: emptyList() @@ -99,13 +144,25 @@ class MainActivity : AppCompatActivity() { } composable(screen = Screen.HISTORY) { - HistoryScreen(navigator = this@NavigationHost) + HistoryScreen( + viewModel = historyViewModel, + libraryViewModel = libraryViewModel, + navigator = this@NavigationHost + ) } composable(screen = Screen.BROWSE) { - BrowseScreen(navigator = this@NavigationHost) + BrowseScreen( + viewModel = browseViewModel, + libraryViewModel = libraryViewModel, + navigator = this@NavigationHost + ) } } + + if (tabletUI) { + CustomNavigationRail(navigator = this@NavigationHost) + } } } @@ -116,6 +173,9 @@ class MainActivity : AppCompatActivity() { exitAnim = Transitions.SlidingTransitionOut ) { BookInfoScreen( + libraryViewModel = libraryViewModel, + browseViewModel = browseViewModel, + historyViewModel = historyViewModel, navigator = this@NavigationHost ) } @@ -125,6 +185,9 @@ class MainActivity : AppCompatActivity() { exitAnim = Transitions.SlidingTransitionOut ) { ReaderScreen( + mainViewModel = mainViewModel, + libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, navigator = this@NavigationHost ) } @@ -147,6 +210,7 @@ class MainActivity : AppCompatActivity() { exitAnim = Transitions.SlidingTransitionOut ) { GeneralSettings( + mainViewModel = mainViewModel, navigator = this@NavigationHost ) } @@ -156,9 +220,25 @@ class MainActivity : AppCompatActivity() { exitAnim = Transitions.SlidingTransitionOut ) { AppearanceSettings( + mainViewModel = mainViewModel, navigator = this@NavigationHost ) } + composable( + screen = Screen.READER_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + ReaderSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } + +// Start screen (later) +// composable(screen = Screen.START) { +// StartScreen() +// } } } else { Box( diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt index a129eccf..bf178666 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt @@ -6,10 +6,10 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.data_store.DataStoreImpl -import ua.acclorite.book_story.data.mapper.BookMapper -import ua.acclorite.book_story.data.mapper.BookMapperImpl -import ua.acclorite.book_story.data.mapper.HistoryMapper -import ua.acclorite.book_story.data.mapper.HistoryMapperImpl +import ua.acclorite.book_story.data.mapper.book.BookMapper +import ua.acclorite.book_story.data.mapper.book.BookMapperImpl +import ua.acclorite.book_story.data.mapper.history.HistoryMapper +import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl import ua.acclorite.book_story.data.repository.BookRepositoryImpl import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Singleton diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt index 08addf36..719c5dc2 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt @@ -8,9 +8,11 @@ import ua.acclorite.book_story.domain.model.Category data class BookEntity( @PrimaryKey(true) val id: Int? = null, val title: String, + val author: String?, + val description: String?, + val text: String, val filePath: String, val progress: Float, - var lastOpened: Long? = null, val image: ByteArray = byteArrayOf(), val category: Category ) { diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt index eb3f9fec..e110fb27 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt @@ -19,8 +19,7 @@ interface BookDao { @Query( """ - SELECT * - FROM bookentity + SELECT * FROM bookentity WHERE LOWER(title) LIKE '%' || LOWER(:query) || '%' """ ) @@ -31,6 +30,9 @@ interface BookDao { ) suspend fun findBookById(id: Int): BookEntity + @Query("SELECT * FROM bookentity WHERE id IN (:ids)") + suspend fun findBooksById(ids: List): List + @Delete suspend fun deleteBooks(books: List) diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapperImpl.kt deleted file mode 100644 index f72eb061..00000000 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapperImpl.kt +++ /dev/null @@ -1,81 +0,0 @@ -package ua.acclorite.book_story.data.mapper - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import ua.acclorite.book_story.R -import ua.acclorite.book_story.data.local.dto.BookEntity -import ua.acclorite.book_story.data.parser.EpubFileParser -import ua.acclorite.book_story.data.parser.PdfFileParser -import ua.acclorite.book_story.data.parser.TxtFileParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.util.UIText -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.File -import javax.inject.Inject - -class BookMapperImpl @Inject constructor( - private val txtFileParser: TxtFileParser, - private val pdfFileParser: PdfFileParser, - private val epubFileParser: EpubFileParser -) : BookMapper { - override suspend fun toBookEntity(book: Book): BookEntity { - val stream = ByteArrayOutputStream() - book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 30, stream) - - return BookEntity( - id = book.id, - title = book.title, - filePath = book.filePath, - progress = book.progress, - lastOpened = book.lastOpened, - image = stream.toByteArray(), - category = book.category - ) - } - - override suspend fun toBook(bookEntity: BookEntity): Book { - val file = File(bookEntity.filePath) - val inputStream = ByteArrayInputStream(bookEntity.image) - val image = BitmapFactory.decodeStream(inputStream) - - val parsedBook = if (file.name.endsWith(".txt")) { - txtFileParser.parse(file) - } else if (file.name.endsWith(".pdf")) { - pdfFileParser.parse(file) - } else if (file.name.endsWith(".epub")) { - epubFileParser.parse(file) - } else { - null - } - - return if (parsedBook != null) - Book( - id = bookEntity.id, - title = bookEntity.title, - author = parsedBook.author, - description = parsedBook.description, - text = emptyList(), - progress = bookEntity.progress, - file = file, - filePath = bookEntity.filePath, - lastOpened = bookEntity.lastOpened, - category = bookEntity.category, - coverImage = if (bookEntity.image.isNotEmpty()) image else null - ) - else - Book( - id = bookEntity.id, - title = bookEntity.title, - author = UIText.StringResource(R.string.unknown_author), - text = emptyList(), - description = null, - progress = bookEntity.progress, - file = null, - filePath = bookEntity.filePath, - lastOpened = bookEntity.lastOpened, - category = bookEntity.category, - coverImage = if (bookEntity.image.isNotEmpty()) image else null - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt similarity index 82% rename from app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapper.kt rename to app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt index e3fe7bce..358807a3 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapper.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.data.mapper +package ua.acclorite.book_story.data.mapper.book import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.domain.model.Book diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt new file mode 100644 index 00000000..f314e27d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt @@ -0,0 +1,74 @@ +package ua.acclorite.book_story.data.mapper.book + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Build +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.local.dto.BookEntity +import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.model.StringWithId +import ua.acclorite.book_story.util.UIText +import java.io.ByteArrayOutputStream +import java.io.File +import javax.inject.Inject + +class BookMapperImpl @Inject constructor() : BookMapper { + override suspend fun toBookEntity(book: Book): BookEntity { + val legacyAPI = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + + val stream = ByteArrayOutputStream() + book.coverImage?.asAndroidBitmap()?.compress( + if (legacyAPI) Bitmap.CompressFormat.WEBP + else Bitmap.CompressFormat.WEBP_LOSSLESS, + if (legacyAPI) 30 else 100, + stream + ) + + val text = book.text.joinToString("\n") { + it.line.trim() + } + + return BookEntity( + id = book.id, + title = book.title, + filePath = book.filePath, + progress = book.progress, + author = book.author.string, + text = text, + description = book.description, + image = stream.toByteArray(), + category = book.category + ) + } + + override suspend fun toBook(bookEntity: BookEntity): Book { + val file = File(bookEntity.filePath) + var image: ImageBitmap? = null + + if (bookEntity.image.isNotEmpty()) { + image = BitmapFactory + .decodeByteArray(bookEntity.image, 0, bookEntity.image.size) + .asImageBitmap() + image.prepareToDraw() + } + + return Book( + id = bookEntity.id, + title = bookEntity.title, + author = bookEntity.author?.let { UIText.StringValue(it) } ?: UIText.StringResource( + R.string.unknown_author + ), + description = bookEntity.description, + progress = bookEntity.progress, + file = if (file.exists()) file else null, + text = bookEntity.text.split("\n").map { StringWithId(it) }, + filePath = bookEntity.filePath, + lastOpened = null, + category = bookEntity.category, + coverImage = image + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt similarity index 54% rename from app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapper.kt rename to app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt index 0648b0d0..2d607770 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapper.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt @@ -1,11 +1,10 @@ -package ua.acclorite.book_story.data.mapper +package ua.acclorite.book_story.data.mapper.history import ua.acclorite.book_story.data.local.dto.HistoryEntity -import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History interface HistoryMapper { suspend fun toHistoryEntity(history: History): HistoryEntity - suspend fun toHistory(historyEntity: HistoryEntity, book: Book): History + suspend fun toHistory(historyEntity: HistoryEntity): History } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt similarity index 66% rename from app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapperImpl.kt rename to app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt index 81c8ef76..ff6292f8 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt @@ -1,7 +1,6 @@ -package ua.acclorite.book_story.data.mapper +package ua.acclorite.book_story.data.mapper.history import ua.acclorite.book_story.data.local.dto.HistoryEntity -import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History import javax.inject.Inject @@ -9,15 +8,15 @@ class HistoryMapperImpl @Inject constructor() : HistoryMapper { override suspend fun toHistoryEntity(history: History): HistoryEntity { return HistoryEntity( id = history.id, - bookId = history.book.id ?: 0, + bookId = history.bookId, time = history.time ) } - override suspend fun toHistory(historyEntity: HistoryEntity, book: Book): History { + override suspend fun toHistory(historyEntity: HistoryEntity): History { return History( historyEntity.id, - book = book, + bookId = historyEntity.bookId, time = historyEntity.time ) } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt index 50be3cec..fcee309e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt @@ -1,8 +1,9 @@ package ua.acclorite.book_story.data.parser import ua.acclorite.book_story.domain.model.StringWithId +import ua.acclorite.book_story.util.Resource import java.io.File interface TextParser { - suspend fun parse(file: File): List + suspend fun parse(file: File): Resource> } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TxtTextParser.kt deleted file mode 100644 index 1964463a..00000000 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtTextParser.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ua.acclorite.book_story.data.parser - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import ua.acclorite.book_story.domain.model.StringWithId -import java.io.BufferedReader -import java.io.File -import java.io.FileReader -import javax.inject.Inject - -class TxtTextParser @Inject constructor() : TextParser { - - override suspend fun parse(file: File): List { - if (!file.name.endsWith(".txt")) { - return emptyList() - } - - return try { - val formattedLines = mutableListOf() - - withContext(Dispatchers.IO) { - BufferedReader(FileReader(file)).forEachLine { line -> - formattedLines.add( - StringWithId(line) - ) - } - } - - formattedLines - } catch (e: Exception) { - emptyList() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt similarity index 89% rename from app/src/main/java/ua/acclorite/book_story/data/parser/EpubFileParser.kt rename to app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index 3547b05a..482745c7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -1,9 +1,11 @@ -package ua.acclorite.book_story.data.parser +package ua.acclorite.book_story.data.parser.epub import android.graphics.BitmapFactory +import androidx.compose.ui.graphics.asImageBitmap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import nl.siegmann.epublib.epub.EpubReader +import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.util.UIText @@ -51,7 +53,7 @@ class EpubFileParser @Inject constructor() : FileParser { filePath = file.path, lastOpened = null, category = Category.entries[0], - coverImage = coverImage + coverImage = coverImage?.asImageBitmap() ) } catch (e: Exception) { return null diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt similarity index 66% rename from app/src/main/java/ua/acclorite/book_story/data/parser/EpubTextParser.kt rename to app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt index c8ee1158..35688701 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt @@ -1,10 +1,14 @@ -package ua.acclorite.book_story.data.parser +package ua.acclorite.book_story.data.parser.epub import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import nl.siegmann.epublib.epub.EpubReader import org.jsoup.Jsoup +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.domain.model.StringWithId +import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.util.UIText import java.io.BufferedReader import java.io.File import java.io.FileInputStream @@ -14,9 +18,9 @@ import javax.inject.Inject class EpubTextParser @Inject constructor() : TextParser { - override suspend fun parse(file: File): List { + override suspend fun parse(file: File): Resource> { if (!file.name.endsWith(".epub")) { - return emptyList() + return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) } try { @@ -47,9 +51,18 @@ class EpubTextParser @Inject constructor() : TextParser { } } - return stringWithIds + if (stringWithIds.isEmpty()) { + return Resource.Error(UIText.StringResource(R.string.error_file_empty)) + } + + return Resource.Success(stringWithIds) } catch (e: Exception) { - return emptyList() + return Resource.Error( + UIText.StringResource( + R.string.error_query, + e.message?.take(40)?.trim() ?: "" + ) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt similarity index 94% rename from app/src/main/java/ua/acclorite/book_story/data/parser/PdfFileParser.kt rename to app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index 36badf5c..5b894e1e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -1,9 +1,10 @@ -package ua.acclorite.book_story.data.parser +package ua.acclorite.book_story.data.parser.pdf import android.app.Application import com.tom_roush.pdfbox.android.PDFBoxResourceLoader import com.tom_roush.pdfbox.pdmodel.PDDocument import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.StringWithId diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt similarity index 62% rename from app/src/main/java/ua/acclorite/book_story/data/parser/PdfTextParser.kt rename to app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt index 42391f12..19759293 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt @@ -1,18 +1,22 @@ -package ua.acclorite.book_story.data.parser +package ua.acclorite.book_story.data.parser.pdf import android.app.Application import com.tom_roush.pdfbox.android.PDFBoxResourceLoader import com.tom_roush.pdfbox.pdmodel.PDDocument import com.tom_roush.pdfbox.text.PDFTextStripper +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.domain.model.StringWithId +import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.util.UIText import java.io.File import javax.inject.Inject class PdfTextParser @Inject constructor(private val application: Application) : TextParser { - override suspend fun parse(file: File): List { + override suspend fun parse(file: File): Resource> { if (!file.name.endsWith(".pdf")) { - return emptyList() + return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) } try { @@ -24,6 +28,11 @@ class PdfTextParser @Inject constructor(private val application: Application) : val pdfStripper = PDFTextStripper() pdfStripper.paragraphStart = "_new_line_" + if (document.isEncrypted) { + document.close() + return Resource.Error(UIText.StringResource(R.string.error_file_encrypted)) + } + val oldText = pdfStripper.getText(document) .replace("\r", "") @@ -38,7 +47,7 @@ class PdfTextParser @Inject constructor(private val application: Application) : } val unformattedLines = text.split("${pdfStripper.paragraphStart}|\\n".toRegex()) - .filter { it.isNotEmpty() } + .filter { it.isNotBlank() } val lines = mutableListOf() unformattedLines.forEachIndexed { index, string -> @@ -52,10 +61,18 @@ class PdfTextParser @Inject constructor(private val application: Application) : stringWithIds.add(StringWithId(line.trim())) } - return stringWithIds + if (stringWithIds.isEmpty()) { + return Resource.Error(UIText.StringResource(R.string.error_file_empty)) + } + return Resource.Success(stringWithIds) } catch (e: Exception) { - return emptyList() + return Resource.Error( + UIText.StringResource( + R.string.error_query, + e.message?.take(40)?.trim() ?: "" + ) + ) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt similarity index 91% rename from app/src/main/java/ua/acclorite/book_story/data/parser/TxtFileParser.kt rename to app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index bb7f0fe1..f2b9485d 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -1,6 +1,7 @@ -package ua.acclorite.book_story.data.parser +package ua.acclorite.book_story.data.parser.txt import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.StringWithId diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt new file mode 100644 index 00000000..d1756dda --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.data.parser.txt + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.TextParser +import ua.acclorite.book_story.domain.model.StringWithId +import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.util.UIText +import java.io.BufferedReader +import java.io.File +import java.io.FileReader +import javax.inject.Inject + +class TxtTextParser @Inject constructor() : TextParser { + + override suspend fun parse(file: File): Resource> { + if (!file.name.endsWith(".txt")) { + return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) + } + + return try { + val formattedLines = mutableListOf() + + withContext(Dispatchers.IO) { + BufferedReader(FileReader(file)).forEachLine { line -> + if (line.isNotBlank()) { + formattedLines.add( + StringWithId(line) + ) + } + } + } + + if (formattedLines.isEmpty()) { + return Resource.Error(UIText.StringResource(R.string.error_file_empty)) + } + + Resource.Success(formattedLines) + } catch (e: Exception) { + Resource.Error( + UIText.StringResource( + R.string.error_query, + e.message?.take(40)?.trim() ?: "" + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index 30a15dd3..915605c7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -4,24 +4,27 @@ import android.os.Environment import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import ua.acclorite.book_story.R import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.room.BookDao -import ua.acclorite.book_story.data.mapper.BookMapper -import ua.acclorite.book_story.data.mapper.HistoryMapper -import ua.acclorite.book_story.data.parser.EpubFileParser -import ua.acclorite.book_story.data.parser.EpubTextParser -import ua.acclorite.book_story.data.parser.PdfFileParser -import ua.acclorite.book_story.data.parser.PdfTextParser -import ua.acclorite.book_story.data.parser.TxtFileParser -import ua.acclorite.book_story.data.parser.TxtTextParser +import ua.acclorite.book_story.data.mapper.book.BookMapper +import ua.acclorite.book_story.data.mapper.history.HistoryMapper +import ua.acclorite.book_story.data.parser.epub.EpubFileParser +import ua.acclorite.book_story.data.parser.epub.EpubTextParser +import ua.acclorite.book_story.data.parser.pdf.PdfFileParser +import ua.acclorite.book_story.data.parser.pdf.PdfTextParser +import ua.acclorite.book_story.data.parser.txt.TxtFileParser +import ua.acclorite.book_story.data.parser.txt.TxtTextParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.repository.BookRepository +import ua.acclorite.book_story.presentation.data.calculateFamiliarity import ua.acclorite.book_story.util.Constants import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.util.UIText import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -49,9 +52,20 @@ class BookRepositoryImpl @Inject constructor( emit(Resource.Loading(true)) val books = database.searchBooks(query) + val history = database.getHistory() + emit( Resource.Success( - data = books.map { bookMapper.toBook(it) } + data = books.map { entity -> + val book = bookMapper.toBook(entity) + val lastHistory = history + .filter { it.bookId == book.id } + .maxByOrNull { it.time } + + book.copy( + lastOpened = lastHistory?.time + ) + } ) ) } @@ -59,7 +73,21 @@ class BookRepositoryImpl @Inject constructor( override suspend fun fastGetBooks(query: String): List { val books = database.searchBooks(query) - return books.map { bookMapper.toBook(it) } + val history = database.getHistory() + + return books.map { entity -> + val book = bookMapper.toBook(entity) + val lastHistory = + history.filter { it.bookId == book.id }.maxByOrNull { it.time } + + book.copy( + lastOpened = lastHistory?.time + ) + } + } + + override suspend fun getBooksById(ids: List): List { + return database.findBooksById(ids).map { bookMapper.toBook(it) } } override suspend fun findBook( @@ -154,7 +182,10 @@ class BookRepositoryImpl @Inject constructor( filteredFiles = if (query.trim().isNotEmpty()) { filteredFiles.sortedByDescending { - it.name.lowercase().startsWith(query.trim().lowercase()) + calculateFamiliarity( + query, + it.name.lowercase().trim() + ) }.toMutableList() } else { filteredFiles.sortedByDescending { @@ -182,61 +213,113 @@ class BookRepositoryImpl @Inject constructor( } else if (file.name.endsWith(".epub")) { epubTextParser.parse(file) } else { + emit(Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))) return@flow } - emit(Resource.Success(text)) - } - } - - override suspend fun getBooksFromFiles(files: List): Flow>> { - return flow { - emit(Resource.Loading(true)) - - val books = mutableListOf() - for (file in files) { - - val parsedBook = if (file.name.endsWith(".txt")) { - txtFileParser.parse(file) - } else if (file.name.endsWith(".pdf")) { - pdfFileParser.parse(file) - } else if (file.name.endsWith(".epub")) { - epubFileParser.parse(file) - } else { - books.add(NullableBook.Null(file.name)) - continue - } - - val parsedText = if (file.name.endsWith(".txt")) { - txtTextParser.parse(file) - } else if (file.name.endsWith(".pdf")) { - pdfTextParser.parse(file) - } else if (file.name.endsWith(".epub")) { - epubTextParser.parse(file) - } else { - books.add(NullableBook.Null(file.name)) - continue - } - - if (parsedBook == null || parsedText.isEmpty()) { - books.add(NullableBook.Null(file.name)) - continue - } - - books.add( - NullableBook.NotNull( - Pair( - parsedBook.copy(text = parsedText), - true - ) + if (text is Resource.Success && text.data == null) { + emit( + Resource.Error( + text.message + ?: UIText.StringResource(R.string.error_file_empty) ) ) } - emit(Resource.Success(books)) + when (text) { + is Resource.Success -> emit(Resource.Success(text.data)) + is Resource.Error -> emit( + Resource.Error( + text.message + ?: UIText.StringResource(R.string.error_something_went_wrong) + ) + ) + + is Resource.Loading -> Unit + } + } } + override suspend fun getBooksFromFiles(files: List): List { + val books = mutableListOf() + + for (file in files) { + val parsedBook = if (file.name.endsWith(".txt")) { + txtFileParser.parse(file) + } else if (file.name.endsWith(".pdf")) { + pdfFileParser.parse(file) + } else if (file.name.endsWith(".epub")) { + epubFileParser.parse(file) + } else { + books.add( + NullableBook.Null( + file.name, + UIText.StringResource(R.string.error_wrong_file_format) + ) + ) + continue + } + + val parsedText = if (file.name.endsWith(".txt")) { + txtTextParser.parse(file) + } else if (file.name.endsWith(".pdf")) { + pdfTextParser.parse(file) + } else if (file.name.endsWith(".epub")) { + epubTextParser.parse(file) + } else { + books.add( + NullableBook.Null( + file.name, + UIText.StringResource(R.string.error_wrong_file_format) + ) + ) + continue + } + + if (parsedBook == null) { + books.add( + NullableBook.Null( + file.name, + UIText.StringResource(R.string.error_something_went_wrong) + ) + ) + continue + } + + if (parsedText is Resource.Error) { + books.add( + NullableBook.Null( + file.name, + parsedText.message + ) + ) + continue + } + + if (parsedText.data == null) { + books.add( + NullableBook.Null( + file.name, + UIText.StringResource(R.string.error_file_empty) + ) + ) + continue + } + + books.add( + NullableBook.NotNull( + Pair( + parsedBook.copy(text = parsedText.data), + true + ) + ) + ) + } + + return books + } + override suspend fun insertHistory(history: List) { database.insertHistory(history.map { historyMapper.toHistoryEntity(it) }) } @@ -246,11 +329,13 @@ class BookRepositoryImpl @Inject constructor( emit(Resource.Loading(true)) val history = database.getHistory() + emit( Resource.Success( data = history.map { - val book = database.findBookById(it.bookId) - historyMapper.toHistory(it, bookMapper.toBook(book)) + historyMapper.toHistory( + it + ) } ) ) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt index 985463a3..0d72a8b0 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.domain.model -import android.graphics.Bitmap import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.ImageBitmap import ua.acclorite.book_story.util.UIText import java.io.File @@ -17,5 +17,5 @@ data class Book( val filePath: String, val lastOpened: Long?, val category: Category, - val coverImage: Bitmap? + val coverImage: ImageBitmap? ) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt index dfbb5321..acab1534 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt @@ -1,5 +1,8 @@ package ua.acclorite.book_story.domain.model +import androidx.compose.runtime.Immutable + +@Immutable enum class Category { READING, ALREADY_READ, PLANNING, DROPPED } diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt index 1e9d8ada..4bf92477 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt @@ -5,6 +5,6 @@ import androidx.compose.runtime.Immutable @Immutable data class History( val id: Int?, - val book: Book, + val bookId: Int, val time: Long ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationBarItem.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt similarity index 88% rename from app/src/main/java/ua/acclorite/book_story/domain/model/NavigationBarItem.kt rename to app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt index ff897133..3e62e80c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationBarItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.painter.Painter @Immutable -data class NavigationBarItem( +data class NavigationItem( val title: String, val selectedIcon: Painter, val unselectedIcon: Painter diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt index 014b0940..7af9a63c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt @@ -1,6 +1,17 @@ package ua.acclorite.book_story.domain.model -sealed class NullableBook(val book: Pair?, val fileName: String?) { - class NotNull(book: Pair) : NullableBook(book, null) - class Null(fileName: String) : NullableBook(null, fileName) -} \ No newline at end of file +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.util.UIText + +@Immutable +sealed class NullableBook( + val book: Pair?, + val fileName: String?, + val message: UIText? +) { + class NotNull(book: Pair) : NullableBook(book, null, null) + class Null( + fileName: String, + message: UIText? + ) : NullableBook(null, fileName, message) +} diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index b2bf1844..edf27f7d 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -20,6 +20,10 @@ interface BookRepository { query: String ): List + suspend fun getBooksById( + ids: List + ): List + suspend fun findBook( id: Int ): BookEntity @@ -50,7 +54,7 @@ interface BookRepository { suspend fun getBookTextFromFile(file: File): Flow>> - suspend fun getBooksFromFiles(files: List): Flow>> + suspend fun getBooksFromFiles(files: List): List suspend fun insertHistory(history: List) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookFromFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookFromFile.kt new file mode 100644 index 00000000..b98c325e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookFromFile.kt @@ -0,0 +1,15 @@ +package ua.acclorite.book_story.domain.use_case + +import ua.acclorite.book_story.domain.model.NullableBook +import ua.acclorite.book_story.domain.repository.BookRepository +import java.io.File +import javax.inject.Inject + +class GetBookFromFile @Inject constructor( + private val repository: BookRepository +) { + + suspend fun execute(file: File): NullableBook { + return repository.getBooksFromFiles(listOf(file)).first() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksById.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksById.kt new file mode 100644 index 00000000..f854cf2d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksById.kt @@ -0,0 +1,12 @@ +package ua.acclorite.book_story.domain.use_case + +import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.repository.BookRepository +import javax.inject.Inject + +class GetBooksById @Inject constructor(private val repository: BookRepository) { + + suspend fun execute(ids: List): List { + return repository.getBooksById(ids) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt index b8d0374d..48bf6413 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt @@ -1,9 +1,7 @@ package ua.acclorite.book_story.domain.use_case -import kotlinx.coroutines.flow.Flow import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.repository.BookRepository -import ua.acclorite.book_story.util.Resource import java.io.File import javax.inject.Inject @@ -11,7 +9,7 @@ class GetBooksFromFiles @Inject constructor( private val repository: BookRepository ) { - suspend fun execute(files: List): Flow>> { + suspend fun execute(files: List): List { return repository.getBooksFromFiles(files) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt index 275ee0a6..f68d490c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt @@ -26,6 +26,14 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.ui.elevation +/** + * Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info. + * + * @param isTopBarScrolled Whether isScrolled state should be forced or not. + * @param content1Visibility Whether first top app bar should be shown. + * @param content2Visibility Whether second top app bar should be shown. + * @param content3Visibility Whether third top app bar should be shown. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun AnimatedTopAppBar( @@ -50,6 +58,7 @@ fun AnimatedTopAppBar( content3Title: @Composable () -> Unit = {}, content3Actions: @Composable RowScope.() -> Unit = {} ) { + //todo fix lags. Box(modifier = Modifier.fillMaxWidth()) { if ((scrollBehavior != null || isTopBarScrolled != null) && scrolledContainerColor != null) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt index c553bf4c..be7079a7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt @@ -14,6 +14,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +/** + * Category Title. Used to name a category. + */ @Composable fun CategoryTitle( modifier: Modifier = Modifier, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt index addfddca..b63df96b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt @@ -16,6 +16,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import ua.acclorite.book_story.ui.elevation +/** + * Custom Checkbox. Has a Circle shape. + */ @Composable fun CustomCheckbox(selected: Boolean) { Icon( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt index f4353a5b..b6dc35b1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt @@ -11,6 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +/** + * Custom Dropdown Menu Item. Used in Dropdown. + */ @Composable fun CustomDropDownMenuItem( leadingIcon: ImageVector, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt new file mode 100644 index 00000000..e67b4435 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt @@ -0,0 +1,236 @@ +package ua.acclorite.book_story.presentation.components + +import android.content.ClipboardManager +import android.content.Context +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.TextToolbar +import androidx.compose.ui.platform.TextToolbarStatus +import ua.acclorite.book_story.R + + +private const val MENU_ITEM_COPY = 0 +private const val MENU_ITEM_TRANSLATE = 1 +private const val MENU_ITEM_DICTIONARY = 2 + +/** + * Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info. + */ +private class CustomTextActionModeCallback( + private val context: Context, + var rect: Rect = Rect.Zero, + var onCopyRequested: (() -> Unit)? = null, + var onTranslateRequested: (() -> Unit)? = null, + var onDictionaryRequested: (() -> Unit)? = null +) : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + requireNotNull(menu) + requireNotNull(mode) + + onCopyRequested?.let { + menu.add(0, MENU_ITEM_COPY, 0, context.getString(R.string.copy)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } + + onTranslateRequested?.let { + menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } + + onDictionaryRequested?.let { + menu.add(0, MENU_ITEM_DICTIONARY, 2, context.getString(R.string.dictionary)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } + + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return false + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + when (item!!.itemId) { + MENU_ITEM_COPY -> onCopyRequested?.invoke() + MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke() + MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke() + else -> return false + } + mode?.finish() + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) {} +} + +/** + * Floating [CustomTextActionModeCallback]. + */ +private class FloatingTextActionModeCallback( + val callback: CustomTextActionModeCallback +) : ActionMode.Callback2() { + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + return callback.onActionItemClicked(mode, item) + } + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return callback.onCreateActionMode(mode, menu) + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return callback.onPrepareActionMode(mode, menu) + } + + override fun onDestroyActionMode(mode: ActionMode?) { + callback.onDestroyActionMode(mode) + } + + override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: android.graphics.Rect?) { + val rect = callback.rect + outRect?.set( + rect.left.toInt(), + rect.top.toInt(), + rect.right.toInt(), + rect.bottom.toInt() + ) + } +} + +/** + * Custom Selection Toolbar. Used in pair with [CustomSelectionContainer] to display custom toolbar. + */ +private class CustomSelectionToolbar( + private val view: View, + context: Context, + private val onCopyRequest: (() -> Unit)?, + private val onTranslateRequest: ((String) -> Unit)?, + private val onDictionaryRequest: ((String) -> Unit)? +) : TextToolbar { + private var actionMode: ActionMode? = null + private val callback = CustomTextActionModeCallback(context = context) + + val clipboardManager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + override var status: TextToolbarStatus by mutableStateOf(TextToolbarStatus.Hidden) + private set + + override fun showMenu( + rect: Rect, + onCopyRequested: (() -> Unit)?, + onPasteRequested: (() -> Unit)?, + onCutRequested: (() -> Unit)?, + onSelectAllRequested: (() -> Unit)? + ) { + callback.rect = rect + callback.onCopyRequested = { + onCopyRequested?.invoke() + onCopyRequest?.invoke() + } + callback.onTranslateRequested = { + val previousClipboard = clipboardManager.primaryClip + onCopyRequested?.invoke() + val currentClipboard = clipboardManager.text + + onTranslateRequest?.invoke(currentClipboard.toString()) + + if (previousClipboard != null) { + clipboardManager.setPrimaryClip( + previousClipboard + ) + } + } + callback.onDictionaryRequested = { + val previousClipboard = clipboardManager.primaryClip + onCopyRequested?.invoke() + val currentClipboard = clipboardManager.text + + onDictionaryRequest?.invoke(currentClipboard.toString()) + + if (previousClipboard != null) { + clipboardManager.setPrimaryClip( + previousClipboard + ) + } + } + + if (actionMode == null) { + status = TextToolbarStatus.Shown + actionMode = view.startActionMode( + FloatingTextActionModeCallback(callback), + ActionMode.TYPE_FLOATING + ) + } else { + actionMode?.invalidate() + } + } + + override fun hide() { + status = TextToolbarStatus.Hidden + actionMode?.finish() + actionMode = null + } +} + +/** + * Custom selection container. + * + * @param onCopyRequested Callback for when the copy option is clicked. + * @param onTranslateRequested Callback for when the translate option is clicked. + * @param onDictionaryRequested Callback for when the dictionary option is clicked. + * @param content Selection container content. + */ +@Composable +fun CustomSelectionContainer( + onCopyRequested: (() -> Unit), + onTranslateRequested: ((String) -> Unit), + onDictionaryRequested: ((String) -> Unit), + content: @Composable (Boolean) -> Unit +) { + val view = LocalView.current + val context = LocalContext.current + + val customSelectionToolbar = remember { + CustomSelectionToolbar( + view = view, + context = context, + + onCopyRequest = { + onCopyRequested() + }, + onTranslateRequest = { + onTranslateRequested(it) + }, + onDictionaryRequest = { + onDictionaryRequested(it) + } + ) + } + val isToolbarHidden = remember(customSelectionToolbar.status) { + derivedStateOf { + customSelectionToolbar.status == TextToolbarStatus.Hidden + } + } + + CompositionLocalProvider( + LocalTextToolbar provides customSelectionToolbar + ) { + SelectionContainer { + content(isToolbarHidden.value) + } + } +} diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackBar.kt deleted file mode 100644 index b23809c6..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackBar.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ua.acclorite.book_story.presentation.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.DismissValue -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.SwipeToDismiss -import androidx.compose.material.rememberDismissState -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun CustomSnackbar(snackbarState: SnackbarHostState) { - val dismissState = rememberDismissState { value -> - if (value != DismissValue.Default) { - snackbarState.currentSnackbarData?.dismiss() - true - } else false - } - - LaunchedEffect(dismissState.currentValue) { - if (dismissState.currentValue != DismissValue.Default) { - dismissState.reset() - } - } - - SwipeToDismiss( - modifier = Modifier - .padding(vertical = 4.dp) - .fillMaxWidth(), - state = dismissState, - background = {} - ) { - SnackbarHost( - hostState = snackbarState, - snackbar = { data -> - Snackbar( - data - ) - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt new file mode 100644 index 00000000..46e76aac --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt @@ -0,0 +1,100 @@ +package ua.acclorite.book_story.presentation.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +/** + * Custom Snackbar with vertical Swipe-To-Dismiss and animation. + * It is recommended to put it in Scaffold bottomBar(this way padding is the same at all sides) + * + * @param snackbarState Regular [SnackbarHostState], use ..remember { SnackbarHostState() } + */ +@Composable +fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostState) { + var showSnackbar by remember { mutableStateOf(false) } + var snackbarData by remember { mutableStateOf(snackbarState.currentSnackbarData) } + var offset by remember { mutableFloatStateOf(0f) } + + val density = LocalDensity.current + val mutableInteractionSource = remember { MutableInteractionSource() } + val isDragged by mutableInteractionSource.collectIsDraggedAsState() + + LaunchedEffect(offset, isDragged) { + val offsetDp = with(density) { offset.toDp() } + if (offsetDp > 35.dp && !isDragged) { + snackbarState.currentSnackbarData?.dismiss() + return@LaunchedEffect + } + + if (offsetDp <= 35.dp && !isDragged) { + offset = 0f + } + } + LaunchedEffect(snackbarState.currentSnackbarData) { + val data = snackbarState.currentSnackbarData + + if (data == null) { + showSnackbar = false + return@LaunchedEffect + } + + offset = 0f + snackbarData = data + showSnackbar = true + } + + AnimatedVisibility( + visible = showSnackbar, + enter = slideInVertically(tween(250)) { it } + fadeIn(tween(250)), + exit = slideOutVertically(tween(100)) { it } + fadeOut(tween(100)) + ) { + Box( + modifier = modifier + .offset { IntOffset(0, offset.roundToInt()) } + .padding(vertical = 4.dp) + .fillMaxWidth() + .draggable( + interactionSource = mutableInteractionSource, + orientation = Orientation.Vertical, + state = rememberDraggableState { + offset = (offset + it).coerceAtLeast(0f) + } + ) + ) { + Snackbar( + modifier = Modifier.align(Alignment.Center), + snackbarData = snackbarData!!, + shape = MaterialTheme.shapes.medium + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt new file mode 100644 index 00000000..70afc399 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt @@ -0,0 +1,38 @@ +package ua.acclorite.book_story.presentation.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator + +/** + * Go back arrow button. Prevents double or triple clicking on it while going back action is performed. + */ +@Composable +fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) { + var isClicked by remember { mutableStateOf(false) } + + IconButton( + enabled = !isClicked, + onClick = { + isClicked = true + navigator.navigateBack() + customOnClick() + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.go_back_content_desc), + tint = MaterialTheme.colorScheme.onSurface + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt index c5cd1e4b..f71eb974 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt @@ -21,9 +21,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +/** + * More drop down. Navigates to Settings, About and Help screens. + */ @Composable fun MoreDropDown(navigator: Navigator) { var showDropDown by remember { mutableStateOf(false) } @@ -58,7 +61,7 @@ fun MoreDropDown(navigator: Navigator) { ) }, onClick = { - navigator.navigate(Screen.SETTINGS) + navigator.navigate(Screen.SETTINGS, false) showDropDown = false }, contentPadding = PaddingValues(start = startPadding, end = endPadding) @@ -72,7 +75,7 @@ fun MoreDropDown(navigator: Navigator) { ) }, onClick = { - navigator.navigate(Screen.HELP) + navigator.navigate(Screen.HELP, false) showDropDown = false }, contentPadding = PaddingValues(start = startPadding, end = endPadding) @@ -86,7 +89,7 @@ fun MoreDropDown(navigator: Navigator) { ) }, onClick = { - navigator.navigate(Screen.ABOUT) + navigator.navigate(Screen.ABOUT, false) showDropDown = false }, contentPadding = PaddingValues(start = startPadding, end = endPadding) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt similarity index 80% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationBar.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt index b50bc246..e2cb079d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.bottom_navigation_bar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.NavigationBarItem -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen +import ua.acclorite.book_story.domain.model.NavigationItem +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.ui.elevation /** @@ -37,34 +37,34 @@ fun BottomNavigationBar( containerColor = MaterialTheme.elevation() ) { BottomNavigationBarItem( - item = NavigationBarItem( + item = NavigationItem( stringResource(id = R.string.library_screen), selectedIcon = painterResource(id = R.drawable.library_screen_filled), unselectedIcon = painterResource(id = R.drawable.library_screen_outlined) ), isSelected = currentScreen == Screen.LIBRARY ) { - navigator.navigate(Screen.LIBRARY) + navigator.navigate(Screen.LIBRARY, false) } BottomNavigationBarItem( - item = NavigationBarItem( + item = NavigationItem( stringResource(id = R.string.history_screen), selectedIcon = painterResource(id = R.drawable.history_screen_filled), unselectedIcon = painterResource(id = R.drawable.history_screen_outlined) ), isSelected = currentScreen == Screen.HISTORY ) { - navigator.navigate(Screen.HISTORY) + navigator.navigate(Screen.HISTORY, false) } BottomNavigationBarItem( - item = NavigationBarItem( + item = NavigationItem( stringResource(id = R.string.browse_screen), selectedIcon = painterResource(id = R.drawable.browse_screen_filled), unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined) ), isSelected = currentScreen == Screen.BROWSE ) { - navigator.navigate(Screen.BROWSE) + navigator.navigate(Screen.BROWSE, false) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt similarity index 91% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt index 94c8edac..eda6f49f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.bottom_navigation_bar import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.size @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.NavigationBarItem +import ua.acclorite.book_story.domain.model.NavigationItem /** * Bottom Navigation Bar Item, uses default [NavigationBarItem]. @@ -19,7 +19,7 @@ import ua.acclorite.book_story.domain.model.NavigationBarItem @Composable fun RowScope.BottomNavigationBarItem( modifier: Modifier = Modifier, - item: NavigationBarItem, + item: NavigationItem, isSelected: Boolean, onClick: () -> Unit ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithContent.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt index a7751c64..0fc92a81 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.custom_dialog import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -35,6 +35,23 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.ui.elevation +/** + * Custom Dialog with custom content. + * + * @param modifier Modifier to be applied. + * @param properties Dialog properties. + * @param drawableIcon Drawable icon to show(1/2). + * @param imageVectorIcon Image vector icon to show(2/2). + * @param backgroundTransparency The transparency of the background behind the dialog. + * @param title The title of the dialog. + * @param description The description of the dialog. + * @param actionText The action text of the dialog. + * @param isActionEnabled Whether action is enabled or not, + * @param onDismiss Code to execute when the dialog is dismissed. + * @param onAction Code to execute when action is clicked. + * @param withDivider Should divider be shown or not. + * @param customContent Custom content(not necessary, dialog also can be use just as alert dialog). + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomDialogWithContent( @@ -54,7 +71,7 @@ fun CustomDialogWithContent( ) { AlertDialog( onDismissRequest = { onDismiss() }, - properties = properties, + properties = properties ) { (LocalView.current.parent as DialogWindowProvider).window.setDimAmount( backgroundTransparency @@ -118,8 +135,8 @@ fun CustomDialogWithContent( if (withDivider) { Spacer(modifier = Modifier.height(12.dp)) Divider( - color = MaterialTheme.colorScheme.outlineVariant, - modifier = Modifier.padding(horizontal = 24.dp) + modifier = Modifier.padding(horizontal = 24.dp), + color = MaterialTheme.colorScheme.outlineVariant ) Spacer(modifier = Modifier.height(12.dp)) } else { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithLazyColumn.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt similarity index 88% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithLazyColumn.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt index 51cf8bb1..0e7ea033 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithLazyColumn.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.custom_dialog import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -36,6 +36,24 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.ui.elevation + +/** + * Custom Dialog with lazy column. + * + * @param modifier Modifier to be applied. + * @param properties Dialog properties. + * @param drawableIcon Drawable icon to show(1/2). + * @param imageVectorIcon Image vector icon to show(2/2). + * @param backgroundTransparency The transparency of the background behind the dialog. + * @param title The title of the dialog. + * @param description The description of the dialog. + * @param actionText The action text of the dialog. + * @param isActionEnabled Whether action is enabled or not, + * @param onDismiss Code to execute when the dialog is dismissed. + * @param onAction Code to execute when action is clicked. + * @param withDivider Should divider be shown or not. + * @param items The items of this dialog, use items(..) { .. } or item { .. }. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomDialogWithLazyColumn( @@ -55,7 +73,7 @@ fun CustomDialogWithLazyColumn( ) { AlertDialog( onDismissRequest = { onDismiss() }, - properties = properties, + properties = properties ) { (LocalView.current.parent as DialogWindowProvider).window.setDimAmount( backgroundTransparency @@ -119,8 +137,8 @@ fun CustomDialogWithLazyColumn( if (withDivider) { Spacer(modifier = Modifier.height(12.dp)) Divider( - color = MaterialTheme.colorScheme.outlineVariant, - modifier = Modifier.padding(horizontal = 24.dp) + modifier = Modifier.padding(horizontal = 24.dp), + color = MaterialTheme.colorScheme.outlineVariant ) Spacer(modifier = Modifier.height(12.dp)) } else { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/SelectableDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt similarity index 91% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/SelectableDialogItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt index 7c638013..7e476e36 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/SelectableDialogItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.custom_dialog import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row @@ -17,6 +17,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +/** + * Selectable dialog item. Perfectly combines with [CustomDialogWithLazyColumn]. + */ @Composable fun SelectableDialogItem(selected: Boolean, title: String, onClick: () -> Unit) { Row( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt new file mode 100644 index 00000000..939d68ea --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt @@ -0,0 +1,90 @@ +package ua.acclorite.book_story.presentation.components.custom_navigation_rail + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationRail +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.model.NavigationItem +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.ui.elevation + +/** + * Custom Navigation Rail. It is used to be shown on Tablets. + */ +@Composable +fun CustomNavigationRail( + navigator: Navigator +) { + var currentScreen: Screen? by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + navigator.getCurrentScreen().collect { + if (it == Screen.LIBRARY || it == Screen.HISTORY || it == Screen.BROWSE) { + currentScreen = it + } + } + } + + NavigationRail( + modifier = Modifier + .width(80.dp) + .background(MaterialTheme.elevation()) + .padding(horizontal = 12.dp), + containerColor = MaterialTheme.elevation() + ) { + Column( + Modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CustomNavigationRailItem( + item = NavigationItem( + stringResource(id = R.string.library_screen), + selectedIcon = painterResource(id = R.drawable.library_screen_filled), + unselectedIcon = painterResource(id = R.drawable.library_screen_outlined) + ), + isSelected = currentScreen == Screen.LIBRARY + ) { + navigator.navigate(Screen.LIBRARY, false) + } + CustomNavigationRailItem( + item = NavigationItem( + stringResource(id = R.string.history_screen), + selectedIcon = painterResource(id = R.drawable.history_screen_filled), + unselectedIcon = painterResource(id = R.drawable.history_screen_outlined) + ), + isSelected = currentScreen == Screen.HISTORY + ) { + navigator.navigate(Screen.HISTORY, false) + } + CustomNavigationRailItem( + item = NavigationItem( + stringResource(id = R.string.browse_screen), + selectedIcon = painterResource(id = R.drawable.browse_screen_filled), + unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined) + ), + isSelected = currentScreen == Screen.BROWSE + ) { + navigator.navigate(Screen.BROWSE, false) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt new file mode 100644 index 00000000..80ed8e28 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt @@ -0,0 +1,54 @@ +package ua.acclorite.book_story.presentation.components.custom_navigation_rail + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.domain.model.NavigationItem + +/** + * Custom Navigation Rail item. + */ +@Composable +fun CustomNavigationRailItem( + modifier: Modifier = Modifier, + item: NavigationItem, + isSelected: Boolean, + onClick: () -> Unit +) { + NavigationRailItem( + label = { + Text( + text = item.title, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + selected = isSelected, + enabled = !isSelected, + onClick = { onClick() }, + icon = { + Icon( + painter = + if (isSelected) item.selectedIcon + else item.unselectedIcon, + contentDescription = item.title, + modifier = Modifier.size(24.dp) + ) + }, + modifier = modifier, + colors = NavigationRailItemDefaults.colors( + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledIconColor = MaterialTheme.colorScheme.onSurface, + disabledTextColor = MaterialTheme.colorScheme.onSurface + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsEmpty.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsEmpty.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/IsEmpty.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsEmpty.kt index 0a4d67a5..311afe73 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsEmpty.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsEmpty.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.is_messages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,6 +20,16 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp + +/** + * Is Empty message. It usually placed when the list is empty. + * + * @param modifier Modifier to be applied. + * @param message The message. + * @param icon Icon to be displayed. + * @param actionTitle Optional action. + * @param action On click action. + */ @Composable fun IsEmpty( modifier: Modifier = Modifier, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsError.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsError.kt similarity index 85% rename from app/src/main/java/ua/acclorite/book_story/presentation/components/IsError.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsError.kt index 61bf90b1..25e5e20b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsError.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsError.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.components +package ua.acclorite.book_story.presentation.components.is_messages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,6 +20,15 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +/** + * Is Empty message. It usually placed when something went wrong and you need to display large error message. + * + * @param modifier Modifier to be applied. + * @param errorMessage The error message. + * @param icon Icon to be displayed. + * @param actionTitle Optional action. + * @param action On click action. + */ @Composable fun IsError( modifier: Modifier = Modifier, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt index 71b426bd..e80b1fc2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt @@ -1,5 +1,9 @@ package ua.acclorite.book_story.presentation.data +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path +import kotlin.math.abs + fun String.removeTrailingZero(): String { if (!this.contains('.')) return this @@ -8,4 +12,19 @@ fun String.removeTrailingZero(): String { .dropLastWhile { it == '.' } } -fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".") \ No newline at end of file +fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".") + +fun calculateFamiliarity(string: String, target: String): Int { + val targetCounts = target.lowercase().trim().groupingBy { it }.eachCount() + val familiarity = string.lowercase().trim().sumOf { targetCounts.getOrDefault(it, 0) } + return familiarity +} + +fun Path.standardQuadFromTo(from: Offset, to: Offset) { + quadraticTo( + from.x, + from.y, + abs(from.x + to.x) / 2f, + abs(from.y + to.y) / 2f + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt index 73b2fffe..08020779 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.launch import ua.acclorite.book_story.domain.use_case.ChangeLanguage import ua.acclorite.book_story.domain.use_case.GetDatastore import ua.acclorite.book_story.domain.use_case.SetDatastore +import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.ui.DarkTheme import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.ui.toDarkTheme @@ -264,19 +265,47 @@ class MainViewModel @Inject constructor( } } - fun init(activity: ComponentActivity) { + fun init( + activity: ComponentActivity, + libraryViewModel: LibraryViewModel, + ) { + val isViewModelsReady = combine( + libraryViewModel.isReady, + ) { values -> + values.all { it } + } + val isDataReady = combine( - language, theme, darkTheme, showStartScreen, backgroundColor, - fontColor, fontFamily, isItalic, fontSize, lineHeight, - paragraphHeight, paragraphIndentation + language, + theme, + darkTheme, + showStartScreen, + backgroundColor, + fontColor, + fontFamily, + isItalic, + fontSize, + lineHeight, + paragraphHeight, + paragraphIndentation ) { values -> values.all { it != null } } + val isReady = combine( + isViewModelsReady, + isDataReady + ) { values -> + values.all { it } + } + viewModelScope.launch { - isDataReady.collect { bool -> + isReady.collect { bool -> if (bool) { - _isReady.value = true + _isReady.update { + true + } + return@collect } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/Navigation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt similarity index 81% rename from app/src/main/java/ua/acclorite/book_story/presentation/Navigation.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt index ac384499..c9cf246e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/Navigation.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation +package ua.acclorite.book_story.presentation.data import android.annotation.SuppressLint import androidx.activity.ComponentActivity @@ -33,6 +33,7 @@ import ua.acclorite.book_story.ui.Transitions private const val CURRENT_SCREEN = "current_screen" private const val BACKSTACK = "back_stack" +private const val USE_BACK_ANIM = "use_back_animation" /** * All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function. @@ -48,9 +49,11 @@ enum class Screen { SETTINGS, GENERAL_SETTINGS, APPEARANCE_SETTINGS, + READER_SETTINGS, ABOUT, - HELP + HELP, + START } data class Argument( @@ -72,6 +75,7 @@ class Navigator @AssistedInject constructor( ) : ViewModel() { private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen) + private val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false) private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf()) private val arguments = mutableStateListOf() @@ -104,7 +108,7 @@ class Navigator @AssistedInject constructor( arguments.removeIf { it.key == key } } - fun navigate(screen: Screen, vararg args: Argument) = + fun navigate(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) = viewModelScope.launch(Dispatchers.Default) { backStack.value.add(currentScreen.value) @@ -112,10 +116,11 @@ class Navigator @AssistedInject constructor( putArgument(it) } + savedStateHandle[USE_BACK_ANIM] = useBackAnimation savedStateHandle[CURRENT_SCREEN] = screen } - fun navigateWithoutBackStack(screen: Screen, vararg args: Argument) = + fun navigateWithoutBackStack(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) = viewModelScope.launch(Dispatchers.Default) { if (backStack.value.last() == screen) { backStack.value.removeLast() @@ -125,15 +130,18 @@ class Navigator @AssistedInject constructor( putArgument(it) } + savedStateHandle[USE_BACK_ANIM] = useBackAnimation savedStateHandle[CURRENT_SCREEN] = screen } - fun navigateBack() = viewModelScope.launch(Dispatchers.Default) { - if (canGoBack()) { - savedStateHandle[CURRENT_SCREEN] = backStack.value.last() - backStack.value.removeLast() + fun navigateBack(useBackAnimation: Boolean = true) = + viewModelScope.launch(Dispatchers.Default) { + if (canGoBack()) { + savedStateHandle[USE_BACK_ANIM] = useBackAnimation + savedStateHandle[CURRENT_SCREEN] = backStack.value.last() + backStack.value.removeLast() + } } - } fun canGoBack(): Boolean { return backStack.value.isNotEmpty() @@ -148,7 +156,9 @@ class Navigator @AssistedInject constructor( * * @param screen The [Screen] that represents [content]. * @param enterAnim Enter Animation. + * @param backEnterAnim Enter Animation for navigating back. * @param exitAnim Exit Animation. + * @param backExitAnim Exit Animation for navigating back. * @param content The Screen content to show when [Navigator.currentScreen] equals [screen]. */ @SuppressLint("ComposableNaming") @@ -156,14 +166,16 @@ class Navigator @AssistedInject constructor( fun composable( screen: Screen, enterAnim: EnterTransition = Transitions.FadeTransitionIn, + backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn, exitAnim: ExitTransition = Transitions.FadeTransitionOut, + backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut, content: @Composable () -> Unit ) { AnimatedVisibility( visible = getCurrentScreen().collectAsState().value == screen, - enter = enterAnim, - exit = exitAnim, - label = screen.toString() + enter = if (!useBackAnimation.collectAsState().value) enterAnim else backEnterAnim, + exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim, + label = "" ) { content() } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt index 140a1ce6..9e417482 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt @@ -21,10 +21,15 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.pullrefresh.PullRefreshDefaults +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButtonDefaults @@ -32,6 +37,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -43,32 +49,33 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.lifecycle.withCreationCallback import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.presentation.Argument -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.CustomSnackbar +import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoChangeCoverBottomSheet -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDeleteDialog import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDetailsBottomSheet import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreDropDown -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoveDialog import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection +import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet +import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet +import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoDeleteDialog +import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel +import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel @@ -77,12 +84,12 @@ import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.util.Constants -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable fun BookInfoScreen( - libraryViewModel: LibraryViewModel = hiltViewModel(), - browseViewModel: BrowseViewModel = hiltViewModel(), - historyViewModel: HistoryViewModel = hiltViewModel(), + libraryViewModel: LibraryViewModel, + browseViewModel: BrowseViewModel, + historyViewModel: HistoryViewModel, navigator: Navigator ) { val context = LocalContext.current @@ -104,22 +111,29 @@ fun BookInfoScreen( val state by viewModel.state.collectAsState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val listState = rememberLazyListState() - + val snackbarState = remember { SnackbarHostState() } + val refreshState = rememberPullRefreshState( + refreshing = state.isRefreshing, + refreshThreshold = PullRefreshDefaults.RefreshThreshold + 32.dp, + refreshingOffset = PullRefreshDefaults.RefreshingOffset + 64.dp, + onRefresh = { + viewModel.onEvent( + BookInfoEvent.OnUpdateBook( + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + historyViewModel.onEvent(HistoryEvent.OnLoadList) + }, + snackbarState, + context + ) + ) + } + ) val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } LaunchedEffect(Unit) { viewModel.init( - navigator, - failure = { - Toast.makeText( - context, - context.getString( - R.string.file_not_found, - it.takeLast(40) - ), - Toast.LENGTH_LONG - ).show() - } + navigator ) } @@ -153,8 +167,9 @@ fun BookInfoScreen( Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), + .windowInsetsPadding(WindowInsets.navigationBars) + .pullRefresh(refreshState) + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface, topBar = { AnimatedTopAppBar( @@ -164,15 +179,7 @@ fun BookInfoScreen( isTopBarScrolled = null, content1NavigationIcon = { - IconButton(onClick = { - navigator.navigateBack() - }) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.ArrowBack, - contentDescription = "Go back", - tint = MaterialTheme.colorScheme.onSurface - ) - } + GoBackButton(navigator = navigator) }, content1Title = { DefaultTransition(visible = firstVisibleItemIndex > 0) { @@ -186,11 +193,37 @@ fun BookInfoScreen( } }, content1Actions = { + IconButton( + enabled = !state.isRefreshing, + onClick = { + viewModel.onEvent( + BookInfoEvent.OnUpdateBook( + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + historyViewModel.onEvent(HistoryEvent.OnLoadList) + }, + snackbarState, + context + ) + ) + } + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(id = R.string.refresh_book_content_desc), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Box { DefaultTransition( visible = !state.editTitle ) { - BookInfoMoreDropDown(viewModel = viewModel) + BookInfoMoreDropDown( + viewModel = viewModel, + snackbarState = snackbarState + ) } androidx.compose.animation.AnimatedVisibility( visible = state.editTitle, @@ -230,6 +263,12 @@ fun BookInfoScreen( } } ) + }, + bottomBar = { + CustomSnackbar( + modifier = Modifier.padding(bottom = 70.dp), + snackbarState = snackbarState + ) } ) { paddingValues -> Box( @@ -246,7 +285,7 @@ fun BookInfoScreen( if (state.book.coverImage != null) { BookInfoBackground( height = paddingValues.calculateTopPadding() + 12.dp + 195.dp, - image = state.book.coverImage!!.asImageBitmap() + image = state.book.coverImage!! ) } @@ -271,48 +310,54 @@ fun BookInfoScreen( } } - if (state.book.file != null) { - FloatingActionButton( - onClick = { - navigator.navigate( - Screen.READER, Argument( - "book", state.book - ) + FloatingActionButton( + onClick = { + navigator.navigate( + Screen.READER, false, Argument( + "book", state.book ) - }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - shape = MaterialTheme.shapes.large, - elevation = FloatingActionButtonDefaults.elevation(), - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - content = { - Row(verticalAlignment = Alignment.CenterVertically) { - Spacer(modifier = Modifier.width(13.dp)) - Icon( - imageVector = Icons.Filled.PlayArrow, - contentDescription = "Continue reading", - Modifier.size(24.dp) + ) + }, + shape = MaterialTheme.shapes.large, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + elevation = FloatingActionButtonDefaults.elevation(), + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + content = { + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.width(13.dp)) + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.continue_reading_content_desc), + Modifier.size(24.dp) + ) + AnimatedVisibility( + visible = !listState.canScrollBackward + ) { + Text( + text = stringResource( + if (state.book.progress == 0f) R.string.start + else R.string.continue_read + ), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + modifier = Modifier.padding(start = 8.dp) ) - AnimatedVisibility( - visible = !listState.canScrollBackward - ) { - Text( - text = stringResource( - if (state.book.progress == 0f) R.string.start - else R.string.continue_read - ), - style = MaterialTheme.typography.labelLarge, - maxLines = 1, - modifier = Modifier.padding(start = 8.dp) - ) - } - Spacer(modifier = Modifier.width(16.dp)) } + Spacer(modifier = Modifier.width(16.dp)) } - ) - } + } + ) + + PullRefreshIndicator( + state.isRefreshing, + refreshState, + Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface + ) } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt index 31d3dc3f..722e60ed 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt @@ -15,6 +15,9 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +/** + * Background of the BookInfoScreen. + */ @Composable fun BookInfoBackground(height: Dp, image: ImageBitmap) { val background = MaterialTheme.colorScheme.background @@ -34,7 +37,7 @@ fun BookInfoBackground(height: Dp, image: ImageBitmap) { ) ) } - .blur(2.dp), + .blur(3.dp), alpha = 0.3f, contentScale = ContentScale.Crop ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt index f4f4ab4b..8c06c645 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt @@ -14,12 +14,15 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book +/** + * Description section. + */ @Composable fun BookInfoDescriptionSection(book: Book) { SelectionContainer { Text( - book.description - ?: stringResource(id = R.string.error_no_description), + if (book.description?.isNotBlank() == true) book.description + else stringResource(id = R.string.error_no_description), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Start, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt index 61c385a9..06af60bf 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.TextStyle @@ -46,6 +45,9 @@ import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.ui.elevation +/** + * Info section. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { @@ -84,7 +86,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { if (book.coverImage != null) { Image( - bitmap = book.coverImage.asImageBitmap(), + bitmap = book.coverImage, contentDescription = "Cover", modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt index 756cac9e..61f1ef0a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -23,8 +24,11 @@ import ua.acclorite.book_story.presentation.components.CustomDropDownMenuItem import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel +/** + * Book info more dropdown. Has three options: Delete, Move and Details. + */ @Composable -fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) { +fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) { val state by viewModel.state.collectAsState() Box { @@ -64,6 +68,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) { leadingIcon = Icons.Outlined.Info, text = stringResource(id = R.string.details_book) ) { + snackbarState.currentSnackbarData?.dismiss() viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet) viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt index a08305bb..4d0410ba 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt @@ -19,6 +19,9 @@ import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero +/** + * Statistic section. + */ @Composable fun BookInfoStatisticSection(book: Book) { Column( @@ -41,16 +44,16 @@ fun BookInfoStatisticSection(book: Book) { LinearProgressIndicator( book.progress.coerceAtLeast(0.01f), - strokeCap = StrokeCap.Round, modifier = Modifier .fillMaxWidth(), color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.05f) + trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.05f), + strokeCap = StrokeCap.Round, ) Spacer(modifier = Modifier.height(6.dp)) Text( - text = stringResource( + text = if (book.progress < 1f) stringResource( id = R.string.you_read_query, (book.progress * 100) .toDouble() @@ -59,7 +62,7 @@ fun BookInfoStatisticSection(book: Book) { ) + " " + stringResource( if (book.progress > 0.2f) R.string.read_keep else R.string.read_more - ), + ) else stringResource(id = R.string.read_done), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, maxLines = 1, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt similarity index 97% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt index 5e6e4eca..072a1d45 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult @@ -32,6 +32,9 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.ui.elevation +/** + * Change cover bottom sheet. Lets user select photo from the gallery and replaces old one. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun BookInfoChangeCoverBottomSheet( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt similarity index 96% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt index 7778d6a9..bbd0080e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -21,6 +21,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +/** + * Change cover bottom sheet item. + */ @Composable fun BookInfoChangeCoverBottomSheetItem( icon: ImageVector, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt similarity index 97% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt index 6a460b8a..7797eab8 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet import android.widget.Toast import androidx.compose.foundation.layout.Spacer @@ -25,6 +25,9 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +/** + * Details bottom sheet. Displays name, path, last opened and size of the file. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun BookInfoDetailsBottomSheet( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt similarity index 96% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt index 662ff4d3..22cf689f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -18,6 +18,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +/** + * Details bottom sheet item. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun BookInfoDetailsBottomSheetItem( @@ -51,7 +54,6 @@ fun BookInfoDetailsBottomSheetItem( Spacer(modifier = Modifier.height(2.dp)) Text( description, - maxLines = 3, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt similarity index 91% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDeleteDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt index 84921428..e395d483 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDeleteDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.screens.book_info.components.dialog import android.widget.Toast import androidx.compose.material.icons.Icons @@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.components.CustomDialogWithContent +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent @@ -18,6 +18,9 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +/** + * Delete dialog. Deletes current book. + */ @Composable fun BookInfoDeleteDialog( libraryViewModel: LibraryViewModel, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt similarity index 90% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoveDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt index 92411f5d..12355b58 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.screens.book_info.components.dialog import android.widget.Toast import androidx.compose.foundation.lazy.items @@ -13,14 +13,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn -import ua.acclorite.book_story.presentation.components.SelectableDialogItem +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn +import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +/** + * Move dialog. Moves current book to the selected category. + */ @Composable fun BookInfoMoveDialog( libraryViewModel: LibraryViewModel, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt index dce99736..f30828d2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt @@ -2,9 +2,10 @@ package ua.acclorite.book_story.presentation.screens.book_info.data import android.content.Context import android.net.Uri +import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.focus.FocusRequester import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.Navigator +import ua.acclorite.book_story.presentation.data.Navigator sealed class BookInfoEvent { data object OnShowHideChangeCoverBottomSheet : BookInfoEvent() @@ -33,4 +34,18 @@ sealed class BookInfoEvent { val text: String, val success: () -> Unit ) : BookInfoEvent() + + data class OnShowSnackbar( + val text: String, + val action: String?, + val onAction: () -> Unit = {}, + val durationMillis: Long, + val snackbarState: SnackbarHostState + ) : BookInfoEvent() + + data class OnUpdateBook( + val refreshList: () -> Unit, + val snackbarState: SnackbarHostState, + val context: Context + ) : BookInfoEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt index 7c580f2b..9e6f8a28 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt @@ -7,6 +7,7 @@ import ua.acclorite.book_story.domain.model.Category @Immutable data class BookInfoState( val book: Book, + val isRefreshing: Boolean = false, val editTitle: Boolean = false, val hasFocused: Boolean = false, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt index 559cd40b..48511fe1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt @@ -5,6 +5,8 @@ import android.content.ClipboardManager import android.content.Context.CLIPBOARD_SERVICE import android.graphics.BitmapFactory import android.os.Build +import androidx.compose.material3.SnackbarResult +import androidx.compose.ui.graphics.asImageBitmap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.assisted.Assisted @@ -12,28 +14,36 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category +import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.use_case.DeleteBooks +import ua.acclorite.book_story.domain.use_case.GetBookFromFile import ua.acclorite.book_story.domain.use_case.UpdateBooks -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen @HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class) class BookInfoViewModel @AssistedInject constructor( @Assisted book: Book, private val updateBooks: UpdateBooks, - private val deleteBooks: DeleteBooks + private val deleteBooks: DeleteBooks, + private val getBookFromFile: GetBookFromFile ) : ViewModel() { private val _state = MutableStateFlow(BookInfoState(book)) val state = _state.asStateFlow() + var job: Job? = null + fun onEvent(event: BookInfoEvent) { when (event) { is BookInfoEvent.OnShowHideChangeCoverBottomSheet -> { @@ -47,9 +57,11 @@ class BookInfoViewModel @AssistedInject constructor( is BookInfoEvent.OnChangeCover -> { viewModelScope.launch { val image = event.context.contentResolver?.openInputStream(event.uri)?.use { - BitmapFactory.decodeStream(it) + BitmapFactory.decodeStream(it).asImageBitmap() } ?: return@launch + image.prepareToDraw() + updateBooks.execute( listOf( _state.value.book.copy(coverImage = image) @@ -221,7 +233,7 @@ class BookInfoViewModel @AssistedInject constructor( it != _state.value.selectedCategory }.size - 1 ) - event.navigator.navigate(Screen.LIBRARY) + event.navigator.navigate(Screen.LIBRARY, true) } } @@ -243,10 +255,139 @@ class BookInfoViewModel @AssistedInject constructor( event.success() } } + + is BookInfoEvent.OnShowSnackbar -> { + viewModelScope.launch { + job?.cancel() + event.snackbarState.currentSnackbarData?.dismiss() + + if (event.durationMillis > 0) { + job = viewModelScope.launch(Dispatchers.IO) { + delay(event.durationMillis) + event.snackbarState.currentSnackbarData?.dismiss() + } + } + + val snackbar = event.snackbarState.showSnackbar( + event.text, + actionLabel = event.action + ) + + when (snackbar) { + SnackbarResult.Dismissed -> Unit + SnackbarResult.ActionPerformed -> { + event.onAction() + } + } + } + } + + is BookInfoEvent.OnUpdateBook -> { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isRefreshing = true + ) + } + + if (_state.value.book.file == null) { + onEvent( + BookInfoEvent.OnShowSnackbar( + text = event.context.getString( + R.string.file_not_found, + _state.value.book.filePath + .substringAfterLast("/") + .takeLast(25) + ), + action = event.context.getString(R.string.retry), + onAction = { + onEvent( + BookInfoEvent.OnUpdateBook( + refreshList = event.refreshList, + snackbarState = event.snackbarState, + context = event.context + ) + ) + }, + durationMillis = 4000L, + snackbarState = event.snackbarState + ) + ) + delay(500) + _state.update { + it.copy( + isRefreshing = false + ) + } + return@launch + } + + val nullableBook = getBookFromFile.execute(_state.value.book.file!!) + + if (nullableBook is NullableBook.Null) { + onEvent( + BookInfoEvent.OnShowSnackbar( + text = nullableBook.message?.asString(event.context) + ?: event.context.getString(R.string.error_something_went_wrong), + action = event.context.getString(R.string.retry), + onAction = { + onEvent( + BookInfoEvent.OnUpdateBook( + refreshList = event.refreshList, + snackbarState = event.snackbarState, + context = event.context + ) + ) + }, + durationMillis = 4000L, + snackbarState = event.snackbarState + ) + ) + + delay(500) + _state.update { + it.copy( + isRefreshing = false + ) + } + return@launch + } + + val updatedBook = nullableBook.book?.first ?: return@launch + + _state.update { + it.copy( + book = it.book.copy( + author = updatedBook.author, + description = updatedBook.description, + text = updatedBook.text + ) + ) + } + updateBooks.execute(listOf(_state.value.book)) + event.refreshList() + + onEvent( + BookInfoEvent.OnShowSnackbar( + event.context.getString(R.string.book_updated), + action = null, + durationMillis = 4000L, + snackbarState = event.snackbarState + ) + ) + + delay(500) + _state.update { + it.copy( + isRefreshing = false + ) + } + } + } } } - fun init(navigator: Navigator, failure: (String) -> Unit) { + fun init(navigator: Navigator) { viewModelScope.launch { val book = navigator.retrieveArgument("book") as? Book @@ -255,10 +396,6 @@ class BookInfoViewModel @AssistedInject constructor( return@launch } - if (book.file == null) { - failure(book.filePath) - } - _state.update { BookInfoState(book = book) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt index ca5d96ab..36f6a1fa 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt @@ -53,24 +53,24 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar -import ua.acclorite.book_story.presentation.components.IsEmpty -import ua.acclorite.book_story.presentation.components.IsError import ua.acclorite.book_story.presentation.components.MoreDropDown -import ua.acclorite.book_story.presentation.screens.browse.components.BrowseAddingDialog +import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty +import ua.acclorite.book_story.presentation.components.is_messages.IsError +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.browse.components.BrowseFileItem import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog +import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel +import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.elevation @@ -83,7 +83,8 @@ import ua.acclorite.book_story.ui.elevation ) @Composable fun BrowseScreen( - viewModel: BrowseViewModel = hiltViewModel(), + viewModel: BrowseViewModel, + libraryViewModel: LibraryViewModel, navigator: Navigator ) { val permissionState = rememberPermissionState( @@ -127,7 +128,11 @@ fun BrowseScreen( BrowseStoragePermissionDialog(viewModel, permissionState) } if (state.showAddingDialog) { - BrowseAddingDialog(viewModel = viewModel, navigator = navigator) + BrowseAddingDialog( + viewModel = viewModel, + navigator = navigator, + libraryViewModel = libraryViewModel + ) } Scaffold( @@ -270,13 +275,14 @@ fun BrowseScreen( columns = GridCells.Adaptive(170.dp), contentPadding = PaddingValues(12.dp) ) { - items(state.selectableFiles, key = { it.first.path }) { selectableFile -> + items( + state.selectableFiles, + key = { it.first.path } + ) { selectableFile -> BrowseFileItem( file = selectableFile, modifier = Modifier - .animateItemPlacement( - animationSpec = tween(300) - ), + .animateItemPlacement(), onClick = { viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) } @@ -317,7 +323,7 @@ fun BrowseScreen( message = stringResource(id = R.string.browse_empty), icon = painterResource(id = R.drawable.empty_browse), actionTitle = stringResource(id = R.string.get_help), - action = { navigator.navigate(Screen.HELP) } + action = { navigator.navigate(Screen.HELP, false) } ) } @@ -325,9 +331,8 @@ fun BrowseScreen( state.isRefreshing, refreshState, Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.elevation(), - contentColor = MaterialTheme.colorScheme.primary, - scale = true + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface ) } } @@ -343,7 +348,7 @@ fun BrowseScreen( return@BackHandler } - navigator.navigate(Screen.LIBRARY) + navigator.navigate(Screen.LIBRARY, false) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt index fd538230..af035842 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt @@ -37,6 +37,9 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +/** + * Browse list element item. Can be selected. + */ @Composable fun BrowseFileItem(file: Pair, modifier: Modifier, onClick: () -> Unit) { val fileExtension: String = file.first.name.substringAfterLast(".", "") diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt index 2dddfea9..887ac8cb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt @@ -10,10 +10,13 @@ import androidx.compose.ui.res.stringResource import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.components.CustomDialogWithContent +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel +/** + * Storage permission dialog. + */ @OptIn(ExperimentalPermissionsApi::class) @Composable fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt similarity index 77% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt index 92cef92b..6e50c3c1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components +package ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog import android.widget.Toast import androidx.compose.foundation.layout.Box @@ -19,18 +19,21 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn +import ua.acclorite.book_story.domain.model.NullableBook +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +/** + * Adding dialog. Adds all selected books to the Library. + */ @Composable fun BrowseAddingDialog( - libraryViewModel: LibraryViewModel = hiltViewModel(), + libraryViewModel: LibraryViewModel, viewModel: BrowseViewModel, navigator: Navigator ) { @@ -42,7 +45,10 @@ fun BrowseAddingDialog( imageVectorIcon = Icons.Default.AddChart, description = stringResource(id = R.string.add_books_description), actionText = stringResource(id = R.string.add), - isActionEnabled = !state.isBooksLoading, + isActionEnabled = !state.isBooksLoading && + state.selectedBooks + .filterIsInstance() + .isNotEmpty(), onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) }, onAction = { viewModel.onEvent( @@ -80,7 +86,15 @@ fun BrowseAddingDialog( } else { items(state.selectedBooks) { book -> BrowseAddingDialogItem(result = book) { - viewModel.onEvent(BrowseEvent.OnSelectBook(book)) + if (it) { + viewModel.onEvent(BrowseEvent.OnSelectBook(book)) + } else { + Toast.makeText( + context, + book.message?.asString(context), + Toast.LENGTH_LONG + ).show() + } } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt similarity index 81% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialogItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt index 55776d10..9bfd271c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialogItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components +package ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -10,27 +10,32 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.presentation.components.CustomCheckbox +/** + * Adding dialog item. + */ @Composable -fun BrowseAddingDialogItem(result: NullableBook, onClick: () -> Unit) { - +fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) { if (result is NullableBook.NotNull) { Row( Modifier .fillMaxWidth() .clickable { - onClick() + onClick(true) } .padding(vertical = 12.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically @@ -58,14 +63,24 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: () -> Unit) { } } } else { + val icon = + if (result.message?.asString() == stringResource(R.string.error_file_encrypted)) { + Icons.Default.Lock + } else { + Icons.Default.Error + } + Row( Modifier .fillMaxWidth() + .clickable { + onClick(false) + } .padding(vertical = 12.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Default.Warning, + imageVector = icon, contentDescription = "Error", modifier = Modifier.size(26.dp), tint = MaterialTheme.colorScheme.error diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt index c5ca39a8..9992bd75 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.focus.FocusRequester import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.presentation.Navigator +import ua.acclorite.book_story.presentation.data.Navigator import java.io.File sealed class BrowseEvent { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt index 62db5c49..bd4e0df8 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt @@ -22,8 +22,8 @@ import ua.acclorite.book_story.domain.use_case.FastGetBooks import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles import ua.acclorite.book_story.domain.use_case.GetFilesFromDownloads import ua.acclorite.book_story.domain.use_case.InsertBooks -import ua.acclorite.book_story.presentation.Argument -import ua.acclorite.book_story.presentation.Screen +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.util.Resource import javax.inject.Inject @@ -42,7 +42,14 @@ class BrowseViewModel @Inject constructor( private var job: Job? = null init { - onEvent(BrowseEvent.OnLoadList) + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isLoading = true + ) + } + getFilesFromDownloads() + } } fun onEvent(event: BrowseEvent) { @@ -268,34 +275,23 @@ class BrowseViewModel @Inject constructor( is BrowseEvent.OnGetBooksFromFiles -> { viewModelScope.launch(Dispatchers.IO) { - getBooksFromFiles.execute( + _state.update { + it.copy( + isBooksLoading = true + ) + } + + val books = getBooksFromFiles.execute( _state.value.selectableFiles .filter { it.second } .map { it.first } - ).collect { result -> - when (result) { - is Resource.Success -> { - if (result.data?.isEmpty() ?: return@collect) { - return@collect - } - _state.update { - it.copy( - selectedBooks = result.data, - isBooksLoading = false - ) - } - } + ) - is Resource.Loading -> { - _state.update { - it.copy( - isBooksLoading = result.isLoading - ) - } - } - - is Resource.Error -> Unit - } + _state.update { + it.copy( + selectedBooks = books, + isBooksLoading = false + ) } } } @@ -323,7 +319,11 @@ class BrowseViewModel @Inject constructor( onEvent(BrowseEvent.OnClearSelectedFiles) onEvent(BrowseEvent.OnLoadList) - event.navigator.navigate(Screen.LIBRARY, Argument("added_books", books)) + event.navigator.navigate( + Screen.LIBRARY, + false, + Argument("added_books", books) + ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt index 7d6740bb..c8973853 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt @@ -54,23 +54,24 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Argument -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CustomSnackbar -import ua.acclorite.book_story.presentation.components.IsEmpty import ua.acclorite.book_story.presentation.components.MoreDropDown +import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.history.components.HistoryDeleteWholeHistoryDialog import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel +import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent +import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.elevation import java.util.UUID @@ -81,11 +82,14 @@ import java.util.UUID ) @Composable fun HistoryScreen( - viewModel: HistoryViewModel = hiltViewModel(), + viewModel: HistoryViewModel, + libraryViewModel: LibraryViewModel, navigator: Navigator ) { val context = LocalContext.current val state by viewModel.state.collectAsState() + val books = libraryViewModel.state.collectAsState().value.books.map { it.first } + val refreshState = rememberPullRefreshState( refreshing = state.isRefreshing, onRefresh = { @@ -118,7 +122,7 @@ fun HistoryScreen( } if (state.showDeleteWholeHistoryDialog) { - HistoryDeleteWholeHistoryDialog(viewModel = viewModel) + HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel) } Scaffold( @@ -144,7 +148,10 @@ fun HistoryScreen( ) }, content1Actions = { - IconButton(onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }) { + IconButton( + enabled = !state.isRefreshing, + onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) } + ) { Icon( imageVector = Icons.Default.Search, contentDescription = "Search history", @@ -153,7 +160,9 @@ fun HistoryScreen( ) } IconButton( - enabled = !state.isLoading && !state.isRefreshing && state.history.isNotEmpty(), + enabled = !state.isLoading + && !state.isRefreshing + && state.history.isNotEmpty(), onClick = { viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) } @@ -248,7 +257,7 @@ fun HistoryScreen( } CategoryTitle( - modifier = Modifier.animateItemPlacement(tween(300)), + modifier = Modifier.animateItemPlacement(), title = when (groupedHistory.title) { "today" -> stringResource(id = R.string.today) "yesterday" -> stringResource(id = R.string.yesterday) @@ -262,27 +271,36 @@ fun HistoryScreen( items( groupedHistory.history, key = { it.id ?: UUID.randomUUID() } ) { + val book = books.find { book -> book.id == it.bookId } ?: return@items + HistoryItem( - modifier = Modifier.animateItemPlacement(tween(300)), + modifier = Modifier.animateItemPlacement(), history = it, + book = book, onBodyClick = { navigator.navigate( Screen.BOOK_INFO, - Argument("book", it.book) + false, + Argument("book", book) ) }, onTitleClick = { navigator.navigate( Screen.READER, - Argument("book", it.book) + false, + Argument("book", book) ) }, + isDeleteEnabled = !state.isRefreshing, onDeleteClick = { viewModel.onEvent( HistoryEvent.OnDeleteHistoryElement( - it, - snackbarState, - context + historyToDelete = it, + snackbarState = snackbarState, + context = context, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + } ) ) } @@ -318,9 +336,8 @@ fun HistoryScreen( state.isRefreshing, refreshState, Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.elevation(), - contentColor = MaterialTheme.colorScheme.primary, - scale = true + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface ) } } @@ -331,7 +348,7 @@ fun HistoryScreen( return@BackHandler } - navigator.navigate(Screen.LIBRARY) + navigator.navigate(Screen.LIBRARY, false) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt index 0474860f..a71b7afa 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt @@ -7,13 +7,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.components.CustomDialogWithContent +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel +import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent +import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +/** + * Delete whole history dialog. + */ @Composable fun HistoryDeleteWholeHistoryDialog( - viewModel: HistoryViewModel + viewModel: HistoryViewModel, + libraryViewModel: LibraryViewModel ) { val context = LocalContext.current @@ -29,7 +35,9 @@ fun HistoryDeleteWholeHistoryDialog( isActionEnabled = true, onAction = { viewModel.onEvent( - HistoryEvent.OnDeleteWholeHistory + HistoryEvent.OnDeleteWholeHistory { + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + } ) Toast.makeText( context, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt index f1cb83f0..b890e7a7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt @@ -26,22 +26,29 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.ui.elevation import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +/** + * History list element item. + */ @Composable fun HistoryItem( modifier: Modifier = Modifier, history: History, + book: Book, onBodyClick: () -> Unit, onTitleClick: () -> Unit, + isDeleteEnabled: Boolean, onDeleteClick: () -> Unit ) { val date = Date(history.time) @@ -77,10 +84,10 @@ fun HistoryItem( tint = MaterialTheme.elevation(12.dp) ) - if (history.book.coverImage != null) { + if (book.coverImage != null) { Image( - bitmap = history.book.coverImage.asImageBitmap(), - contentDescription = "Cover", + bitmap = book.coverImage, + contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.medium), @@ -96,7 +103,7 @@ fun HistoryItem( modifier = Modifier.fillMaxHeight() ) { Text( - history.book.title, + book.title, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, modifier = Modifier @@ -109,7 +116,7 @@ fun HistoryItem( maxLines = 2, overflow = TextOverflow.Ellipsis ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(2.dp)) Text( pattern.format(date), color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -123,6 +130,7 @@ fun HistoryItem( } Box(modifier = Modifier.weight(0.1f), contentAlignment = Alignment.CenterEnd) { IconButton( + enabled = isDeleteEnabled, onClick = { onDeleteClick() } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt index ff091295..668978bd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt @@ -9,10 +9,11 @@ sealed class HistoryEvent { data object OnRefreshList : HistoryEvent() data object OnLoadList : HistoryEvent() data object OnShowHideDeleteWholeHistoryDialog : HistoryEvent() - data object OnDeleteWholeHistory : HistoryEvent() + data class OnDeleteWholeHistory(val refreshList: () -> Unit) : HistoryEvent() data class OnDeleteHistoryElement( val historyToDelete: History, val snackbarState: SnackbarHostState, + val refreshList: () -> Unit, val context: Context ) : HistoryEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt index 8217099f..055a05d4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt @@ -1,6 +1,5 @@ package ua.acclorite.book_story.presentation.screens.history.data -import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarResult import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -17,6 +16,7 @@ import ua.acclorite.book_story.domain.model.GroupedHistory import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.use_case.DeleteHistory import ua.acclorite.book_story.domain.use_case.DeleteWholeHistory +import ua.acclorite.book_story.domain.use_case.GetBooksById import ua.acclorite.book_story.domain.use_case.GetHistory import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.util.Resource @@ -30,16 +30,25 @@ class HistoryViewModel @Inject constructor( private val insertHistory: InsertHistory, private val getHistory: GetHistory, private val deleteWholeHistory: DeleteWholeHistory, - private val deleteHistory: DeleteHistory + private val deleteHistory: DeleteHistory, + private val getBooksById: GetBooksById ) : ViewModel() { private val _state = MutableStateFlow(HistoryState()) val state = _state.asStateFlow() private var job: Job? = null + private var job2: Job? = null init { - onEvent(HistoryEvent.OnLoadList) + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isLoading = true + ) + } + getHistoryFromDatabase() + } } fun onEvent(event: HistoryEvent) { @@ -85,6 +94,7 @@ class HistoryViewModel @Inject constructor( } deleteWholeHistory.execute() + event.refreshList() getHistoryFromDatabase("") } @@ -104,20 +114,27 @@ class HistoryViewModel @Inject constructor( listOf(event.historyToDelete) ) onEvent(HistoryEvent.OnRefreshList) + event.refreshList() + job2?.cancel() event.snackbarState.currentSnackbarData?.dismiss() + + job2 = viewModelScope.launch(Dispatchers.IO) { + delay(10000) + event.snackbarState.currentSnackbarData?.dismiss() + } val snackbar = event.snackbarState.showSnackbar( event.context.getString(R.string.history_element_deleted), - event.context.getString(R.string.undo), - duration = SnackbarDuration.Long + event.context.getString(R.string.undo) ) when (snackbar) { - SnackbarResult.Dismissed -> return@launch + SnackbarResult.Dismissed -> Unit SnackbarResult.ActionPerformed -> { insertHistory.execute( listOf(event.historyToDelete) ) + event.refreshList() onEvent(HistoryEvent.OnRefreshList) } } @@ -197,7 +214,7 @@ class HistoryViewModel @Inject constructor( } fun filterMaxElementsById(elements: List): List { - val groupedById = elements.groupBy { it.book.id } + val groupedById = elements.groupBy { it.bookId } val maxElementsById = groupedById.map { (_, values) -> values.maxByOrNull { it.time } } @@ -223,11 +240,15 @@ class HistoryViewModel @Inject constructor( return@collect } + val books = getBooksById.execute( + history.map { it.bookId }.distinct() + ) val groupedHistory = mutableListOf() history .filter { - it.book.title.lowercase().trim().contains(query.lowercase().trim()) + val book = books.find { book -> book.id == it.bookId }!! + book.title.lowercase().trim().contains(query.lowercase().trim()) }.groupBy { item -> val calendar = Calendar.getInstance().apply { timeInMillis = item.time diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt index 3f786611..fa843c82 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.CircularProgressIndicator @@ -69,24 +68,23 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.Argument -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar -import ua.acclorite.book_story.presentation.components.IsEmpty import ua.acclorite.book_story.presentation.components.MoreDropDown +import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem -import ua.acclorite.book_story.presentation.screens.library.components.LibraryDeleteDialog -import ua.acclorite.book_story.presentation.screens.library.components.LibraryMoveDialog import ua.acclorite.book_story.presentation.screens.library.components.LibraryTabRow +import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryDeleteDialog +import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.ui.Transitions @@ -99,15 +97,17 @@ import java.util.UUID ) @Composable fun LibraryScreen( - viewModel: LibraryViewModel = hiltViewModel(), - browseViewModel: BrowseViewModel = hiltViewModel(), - historyViewModel: HistoryViewModel = hiltViewModel(), + viewModel: LibraryViewModel, + browseViewModel: BrowseViewModel, + historyViewModel: HistoryViewModel, navigator: Navigator, addedBooks: List ) { val state by viewModel.state.collectAsState() - val pagerState = - rememberPagerState(initialPage = state.currentPage, pageCount = { Category.entries.size }) + val pagerState = rememberPagerState( + initialPage = state.currentPage, + pageCount = { Category.entries.size } + ) val refreshState = rememberPullRefreshState( refreshing = state.isRefreshing, onRefresh = { @@ -165,16 +165,15 @@ fun LibraryScreen( overflow = TextOverflow.Ellipsis, maxLines = 1 ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(6.dp)) Text( text = state.books.size.toString(), modifier = Modifier - .clip(RoundedCornerShape(12.dp)) + .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation(elevation = 6.dp)) - .padding(horizontal = 10.dp, vertical = 4.dp), + .padding(horizontal = 8.dp, vertical = 3.dp), color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 16.sp, - style = MaterialTheme.typography.titleLarge + fontSize = 16.sp ) } }, @@ -313,9 +312,11 @@ fun LibraryScreen( LaunchedEffect(state.books) { categorizedBooks.clear() categorizedBooks.addAll(state.books.filter { it.first.category == category } - .sortedByDescending { - it.first.lastOpened - }) + .sortedWith( + compareByDescending> { it.first.lastOpened } + .thenBy { it.first.title } + ) + ) categoryIsLoading = false } @@ -333,15 +334,14 @@ fun LibraryScreen( ) { LibraryBookItem( book = it, - modifier = Modifier.animateItemPlacement( - animationSpec = tween(300) - ), + modifier = Modifier.animateItemPlacement(), onCoverImageClick = { if (state.hasSelectedItems) { viewModel.onEvent(LibraryEvent.OnSelectBook(it)) } else { navigator.navigate( Screen.BOOK_INFO, + false, Argument("book", it.first) ) } @@ -354,6 +354,7 @@ fun LibraryScreen( onButtonClick = { navigator.navigate( Screen.READER, + false, Argument("book", it.first) ) } @@ -385,7 +386,7 @@ fun LibraryScreen( modifier = Modifier.align(Alignment.Center), actionTitle = stringResource(id = R.string.add_book) ) { - navigator.navigate(Screen.BROWSE) + navigator.navigate(Screen.BROWSE, false) } } @@ -393,9 +394,8 @@ fun LibraryScreen( state.isRefreshing, refreshState, Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.elevation(), - contentColor = MaterialTheme.colorScheme.primary, - scale = true + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt index f0d168c5..a097aafc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults @@ -30,7 +29,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -40,6 +38,9 @@ import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.ui.elevation +/** + * Library list element item. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun LibraryBookItem( @@ -102,7 +103,7 @@ fun LibraryBookItem( if (book.first.coverImage != null) { Image( - bitmap = book.first.coverImage!!.asImageBitmap(), + bitmap = book.first.coverImage!!, contentDescription = "Cover", modifier = Modifier .fillMaxSize() @@ -111,40 +112,25 @@ fun LibraryBookItem( ) } - if (book.first.file != null) { - Text( - "${ - (book.first.progress * 100) - .toDouble() - .removeDigits(1) - .removeTrailingZero() - }%", - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier - .padding(6.dp) - .clip(MaterialTheme.shapes.small) - .background(MaterialTheme.colorScheme.primary) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.bodySmall - ) - } else { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = "File is not found", - modifier = Modifier - .padding(6.dp) - .clip(MaterialTheme.shapes.small) - .background(MaterialTheme.colorScheme.error) - .padding(4.dp) - .size(20.dp), - tint = MaterialTheme.colorScheme.onError - ) - } + Text( + "${ + (book.first.progress * 100) + .toDouble() + .removeDigits(1) + .removeTrailingZero() + }%", + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .padding(6.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.primary) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodySmall + ) FilledIconButton( onClick = { onButtonClick() }, - enabled = book.first.file != null, modifier = Modifier .align(Alignment.BottomEnd) .padding(6.dp) @@ -154,11 +140,7 @@ fun LibraryBookItem( containerColor = MaterialTheme.colorScheme.primaryContainer .copy(0.9f), contentColor = MaterialTheme.colorScheme.onPrimaryContainer - .copy(0.9f), - disabledContainerColor = MaterialTheme.colorScheme.primaryContainer - .copy(0.6f), - disabledContentColor = MaterialTheme.colorScheme.onPrimaryContainer - .copy(0.6f) + .copy(0.9f) ) ) { Icon( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt index 664ce0ee..32cda42e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt @@ -31,6 +31,9 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.ui.elevation +/** + * Tab row, either scrollable or static, depends on screen width. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: PagerState) { @@ -97,9 +100,9 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: Pa Text( text = tabItem.second.toString(), modifier = Modifier - .clip(RoundedCornerShape(12.dp)) + .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation(6.dp)) - .padding(horizontal = 6.dp, vertical = 4.dp), + .padding(horizontal = 8.dp, vertical = 4.dp), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium ) @@ -153,9 +156,9 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: Pa Text( text = tabItem.second.toString(), modifier = Modifier - .clip(RoundedCornerShape(12.dp)) + .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation(6.dp)) - .padding(horizontal = 6.dp, vertical = 4.dp), + .padding(horizontal = 8.dp, vertical = 4.dp), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt similarity index 92% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryDeleteDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt index 1ea9df85..5b1117df 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryDeleteDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.library.components +package ua.acclorite.book_story.presentation.screens.library.components.dialog import android.widget.Toast import androidx.compose.material.icons.Icons @@ -9,7 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.components.CustomDialogWithContent +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent @@ -17,6 +17,9 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +/** + * Delete dialog. Deletes all selected books. + */ @Composable fun LibraryDeleteDialog( viewModel: LibraryViewModel, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt similarity index 90% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryMoveDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt index 86e17724..a904a4cb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.library.components +package ua.acclorite.book_story.presentation.screens.library.components.dialog import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi @@ -13,11 +13,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn -import ua.acclorite.book_story.presentation.components.SelectableDialogItem +import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn +import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +/** + * Move dialog. Moves all selected books to the selected category. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt index b25724b2..4f0c27a4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt @@ -29,10 +29,23 @@ class LibraryViewModel @Inject constructor( private val _state = MutableStateFlow(LibraryState()) val state = _state.asStateFlow() + private val _isReady = MutableStateFlow(false) + val isReady = _isReady.asStateFlow() + private var job: Job? = null init { - onEvent(LibraryEvent.OnLoadList) + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isLoading = true + ) + } + getBooksFromDatabase() + _isReady.update { + true + } + } } fun onEvent(event: LibraryEvent) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt index 2c8d9d18..5459a8f9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt @@ -1,6 +1,8 @@ package ua.acclorite.book_story.presentation.screens.reader import android.annotation.SuppressLint +import android.os.Build +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.viewModels @@ -20,7 +22,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.selection.DisableSelection -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -50,25 +51,25 @@ import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.components.IsError +import ua.acclorite.book_story.presentation.components.CustomSelectionContainer +import ua.acclorite.book_story.presentation.components.is_messages.IsError import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderBottomBar import ua.acclorite.book_story.presentation.screens.reader.components.ReaderEndItem -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderSettingsBottomSheet import ua.acclorite.book_story.presentation.screens.reader.components.ReaderStartItem -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTopBar +import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar +import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar +import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.ui.elevation @@ -78,9 +79,9 @@ import ua.acclorite.book_story.util.Constants @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun ReaderScreen( - mainViewModel: MainViewModel = hiltViewModel(), - libraryViewModel: LibraryViewModel = hiltViewModel(), - historyViewModel: HistoryViewModel = hiltViewModel(), + mainViewModel: MainViewModel, + libraryViewModel: LibraryViewModel, + historyViewModel: HistoryViewModel, navigator: Navigator ) { val context = LocalContext.current as ComponentActivity @@ -189,7 +190,7 @@ fun ReaderScreen( Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), - containerColor = Color(backgroundColor.toULong()), + containerColor = MaterialTheme.colorScheme.surface, topBar = { AnimatedVisibility( visible = state.showMenu, @@ -219,19 +220,66 @@ fun ReaderScreen( } } ) { - SelectionContainer { + CustomSelectionContainer( + onCopyRequested = { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText( + context, + context.getString(R.string.copied), + Toast.LENGTH_SHORT + ).show() + } + }, + onTranslateRequested = { textToTranslate -> + viewModel.onEvent( + ReaderEvent.OnTranslateText( + textToTranslate, + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_translator), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + }, + onDictionaryRequested = { textToDefine -> + viewModel.onEvent( + ReaderEvent.OnOpenDictionary( + textToDefine, + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + ) { toolbarShowed -> LazyColumn( state = listState, modifier = Modifier - .fillMaxSize() - .clickable( - interactionSource = null, - indication = null, - enabled = !loading, - onClick = { - viewModel.onEvent( - ReaderEvent.OnShowHideMenu(context = context) - ) + .then( + if (!loading && toolbarShowed) { + Modifier + .fillMaxSize() + .clickable( + interactionSource = null, + indication = null, + onClick = { + viewModel.onEvent( + ReaderEvent.OnShowHideMenu(context = context) + ) + } + ) + } else { + Modifier + .fillMaxSize() } ) ) { @@ -246,28 +294,31 @@ fun ReaderScreen( itemsIndexed( state.book.text, key = { _, key -> key.id } ) { index, text -> - if (index == 0) - Spacer(modifier = Modifier.height(36.dp)) + Column( + Modifier + .background(Color(backgroundColor.toULong())) + .fillMaxWidth() + .padding( + top = if (index == 0) 36.dp else 0.dp, + start = 18.dp, + end = 18.dp, + bottom = if (index == state.book.text.lastIndex) 36.dp + else (paragraphHeight * 3).dp + ) + ) { + Text( + text = "${if (paragraphIndentation) " " else ""}${text.line}", + color = Color(fontColor.toULong()), - Text( - text = "${if (paragraphIndentation) " " else ""}${text.line}", - color = Color(fontColor.toULong()), - - style = TextStyle( - lineBreak = LineBreak.Paragraph - ), - fontFamily = fontFamily.font, - fontStyle = fontStyle, - fontSize = fontSize.sp, - lineHeight = (fontSize + lineHeight).sp, - - modifier = Modifier.padding(horizontal = 18.dp) - ) - - if (index == state.book.text.lastIndex) - Spacer(modifier = Modifier.height(36.dp)) - else - Spacer(modifier = Modifier.height((paragraphHeight * 3).dp)) + style = TextStyle( + lineBreak = LineBreak.Paragraph + ), + fontFamily = fontFamily.font, + fontStyle = fontStyle, + fontSize = fontSize.sp, + lineHeight = (fontSize + lineHeight).sp + ) + } } if (state.book.text.isNotEmpty()) { @@ -300,6 +351,9 @@ fun ReaderScreen( actionTitle = stringResource(id = R.string.go_back), action = { navigator.navigateBack() + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + historyViewModel.onEvent(HistoryEvent.OnLoadList) + viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) } ) } else { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt index 47d27f7d..cc1b5c15 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt @@ -4,11 +4,15 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -26,13 +30,17 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel +/** + * Reader end item. Displays at the end of the book. + */ +@OptIn(ExperimentalLayoutApi::class) @Composable fun ReaderEndItem( libraryViewModel: LibraryViewModel, @@ -46,7 +54,13 @@ fun ReaderEndItem( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) - .padding(24.dp), + .windowInsetsPadding(WindowInsets.navigationBarsIgnoringVisibility) + .padding( + top = 24.dp, + start = 24.dp, + end = 24.dp, + bottom = 8.dp + ), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -56,7 +70,14 @@ fun ReaderEndItem( textAlign = TextAlign.Center, overflow = TextOverflow.Ellipsis ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(4.dp)) + Text( + stringResource(id = R.string.letters_and_words, state.letters, state.words), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(12.dp)) TextButton( onClick = { if (state.book.category != Category.ALREADY_READ) { @@ -86,7 +107,7 @@ fun ReaderEndItem( context ) ) - navigator.navigate(Screen.LIBRARY) + navigator.navigate(Screen.LIBRARY, true) } }, contentPadding = PaddingValues(horizontal = 12.dp), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt index b397dafa..d209e611 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt @@ -5,14 +5,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsIgnoringVisibility import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image import androidx.compose.material3.Icon @@ -24,7 +28,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -33,6 +36,10 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.ui.elevation +/** + * Reader start item. Displays at the beginning of the book. + */ +@OptIn(ExperimentalLayoutApi::class) @Composable fun ReaderStartItem(viewModel: ReaderViewModel) { val state by viewModel.state.collectAsState() @@ -41,6 +48,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) + .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) .padding(24.dp) ) { Row( @@ -66,7 +74,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { if (state.book.coverImage != null) { Image( - bitmap = state.book.coverImage!!.asImageBitmap(), + bitmap = state.book.coverImage!!, contentDescription = "Cover", modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt similarity index 96% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderBottomBar.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt index 6ae3075a..ea21c461 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderBottomBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.screens.reader.components.app_bar import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.Navigator +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent @@ -27,6 +27,9 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel +/** + * Reader bottom bar. Has a slider to change progress. + */ @Composable fun ReaderBottomBar( viewModel: ReaderViewModel, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt similarity index 82% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTopBar.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt index 37a88904..678b341a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.screens.reader.components.app_bar import androidx.activity.ComponentActivity import androidx.compose.foundation.basicMarquee @@ -6,10 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -28,17 +25,20 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Argument -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen +import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel +/** + * Reader top bar. Displays title of the book. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) { @@ -48,15 +48,8 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo TopAppBar( navigationIcon = { - IconButton(onClick = { - navigator.navigateBack() + GoBackButton(navigator = navigator) { viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Go back", - tint = MaterialTheme.colorScheme.onSurface - ) } }, title = { @@ -75,6 +68,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo onClick = { navigator.navigateWithoutBackStack( Screen.BOOK_INFO, + true, Argument( "book", book @@ -86,7 +80,6 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo delayMillis = 4000 ) ) - Spacer(modifier = Modifier.height(4.dp)) Text( stringResource( id = R.string.read_query, @@ -107,8 +100,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo }) { Icon( imageVector = Icons.Default.Settings, - contentDescription = "Open Settings", - tint = MaterialTheme.colorScheme.onSurface + contentDescription = stringResource(id = R.string.open_reader_settings_content_desc), ) } }, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt similarity index 98% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt index 0e1c507b..fe09675c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet import androidx.activity.ComponentActivity import androidx.compose.animation.animateColorAsState @@ -48,6 +48,9 @@ import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.util.Constants +/** + * Settings bottom sheet. Has General and Colors categories. + */ @OptIn( ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class ) @@ -118,7 +121,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { CategoryTitle( - title = stringResource(id = R.string.font_settings), + title = stringResource(id = R.string.font_reader_settings), color = MaterialTheme.colorScheme.onSurface ) } @@ -197,7 +200,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { CategoryTitle( - title = stringResource(id = R.string.text_settings), + title = stringResource(id = R.string.text_reader_settings), color = MaterialTheme.colorScheme.onSurface ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt similarity index 96% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheetTabRow.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt index 28d787c4..71818fc6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheetTabRow.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxWidth @@ -21,6 +21,9 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel +/** + * Settings bottom sheet tab row. It is used to switch between categories. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt index f9294234..92bcc2e9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt @@ -7,10 +7,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.pager.PagerState import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.presentation.Navigator +import ua.acclorite.book_story.presentation.data.Navigator sealed class ReaderEvent { - data class OnFileNotFound(val onLoaded: () -> Unit) : ReaderEvent() + data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent() data class OnLoadText( val scrollState: LazyListState, val navigator: Navigator, @@ -37,4 +37,16 @@ sealed class ReaderEvent { val updatePage: (Int) -> Unit, val navigator: Navigator ) : ReaderEvent() + + data class OnTranslateText( + val textToTranslate: String, + val context: ComponentActivity, + val noAppsFound: () -> Unit + ) : ReaderEvent() + + data class OnOpenDictionary( + val textToDefine: String, + val context: ComponentActivity, + val noAppsFound: () -> Unit + ) : ReaderEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt index 056f6758..5bca3077 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt @@ -7,6 +7,9 @@ import ua.acclorite.book_story.util.UIText @Immutable data class ReaderState( val book: Book, + val letters: Int = 0, + val words: Int = 0, + val errorMessage: UIText? = null, val showMenu: Boolean = false, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt index 1184f683..c6d59ae0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt @@ -1,5 +1,7 @@ package ua.acclorite.book_story.presentation.screens.reader.data +import android.app.SearchManager +import android.content.Intent import androidx.activity.ComponentActivity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState @@ -24,13 +26,11 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.History -import ua.acclorite.book_story.domain.use_case.GetText import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.UpdateBooks -import ua.acclorite.book_story.presentation.Argument -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.util.UIText import java.util.Date import kotlin.math.roundToInt @@ -39,7 +39,6 @@ import kotlin.math.roundToInt @HiltViewModel(assistedFactory = ReaderViewModel.Factory::class) class ReaderViewModel @AssistedInject constructor( @Assisted book: Book, - private val getText: GetText, private val updateBooks: UpdateBooks, private val insertHistory: InsertHistory ) : ViewModel() { @@ -49,61 +48,29 @@ class ReaderViewModel @AssistedInject constructor( fun onEvent(event: ReaderEvent) { when (event) { - is ReaderEvent.OnFileNotFound -> { + is ReaderEvent.OnTextIsEmpty -> { _state.update { event.onLoaded() it.copy( - errorMessage = UIText.StringResource(R.string.error_file_not_found), + errorMessage = UIText.StringResource(R.string.error_no_text), ) } } is ReaderEvent.OnLoadText -> { viewModelScope.launch(Dispatchers.IO) { - if (_state.value.book.file == null) { - return@launch - } - - if (_state.value.book.text.isEmpty()) { - getText.execute(_state.value.book.file!!).collect { result -> - when (result) { - is Resource.Loading -> Unit - is Resource.Success -> { - if (result.data?.isEmpty() != false) { - _state.update { - it.copy( - errorMessage = UIText.StringResource( - R.string.error_something_went_wrong - ), - ) - } - event.onLoaded() - return@collect - } - - _state.update { - it.copy( - book = it.book.copy( - text = result.data, - ) - ) - } - - - } - - is Resource.Error -> Unit - } - } - } - val time = Date().time + val text = _state.value.book.text.joinToString(separator = " ") { it.line } + val letters = text.replace(" ", "").length + val words = text.split("\\s+".toRegex()).size _state.update { it.copy( book = it.book.copy( lastOpened = time - ) + ), + letters = letters, + words = words ) } @@ -114,7 +81,7 @@ class ReaderViewModel @AssistedInject constructor( listOf( History( null, - _state.value.book, + _state.value.book.id!!, time ) ) @@ -141,7 +108,7 @@ class ReaderViewModel @AssistedInject constructor( } } - delay(500) + delay(100) event.onLoaded() return@collectLatest } @@ -234,7 +201,8 @@ class ReaderViewModel @AssistedInject constructor( onEvent(ReaderEvent.OnShowSystemBars(event.context)) val book = _state.value.book.copy( - category = Category.ALREADY_READ + category = Category.ALREADY_READ, + progress = 1f ) updateBooks.execute(listOf(book)) @@ -245,7 +213,59 @@ class ReaderViewModel @AssistedInject constructor( it != Category.ALREADY_READ }.size - 1 ) - event.navigator.navigate(Screen.LIBRARY) + event.navigator.navigate(Screen.LIBRARY, true) + } + } + + is ReaderEvent.OnTranslateText -> { + viewModelScope.launch { + val translatorIntent = Intent() + val browserIntent = Intent() + + translatorIntent.type = "text/plain" + translatorIntent.action = Intent.ACTION_PROCESS_TEXT + + browserIntent.action = Intent.ACTION_WEB_SEARCH + + translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, event.textToTranslate) + translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true) + + browserIntent.putExtra( + SearchManager.QUERY, + "translate: ${event.textToTranslate.trim()}" + ) + + if (translatorIntent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(translatorIntent) + return@launch + } + + if (browserIntent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(browserIntent) + return@launch + } + + event.noAppsFound() + } + } + + is ReaderEvent.OnOpenDictionary -> { + viewModelScope.launch { + val browserIntent = Intent() + + browserIntent.action = Intent.ACTION_WEB_SEARCH + browserIntent.putExtra( + SearchManager.QUERY, + "dictionary" + + ": ${event.textToDefine.trim()}" + ) + + if (browserIntent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(browserIntent) + return@launch + } + + event.noAppsFound() } } } @@ -270,8 +290,8 @@ class ReaderViewModel @AssistedInject constructor( ReaderState(book = book) } - if (book.file == null) { - onEvent(ReaderEvent.OnFileNotFound(onLoaded = { onLoaded() })) + if (book.text.isEmpty()) { + onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() })) } else { onEvent(ReaderEvent.OnShowHideMenu(false, context)) onEvent( @@ -304,3 +324,5 @@ class ReaderViewModel @AssistedInject constructor( + + diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt index 77985539..c2f2e0bd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt @@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.filled.DisplaySettings import androidx.compose.material.icons.filled.Palette import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -21,19 +21,24 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.Screen -import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem +import ua.acclorite.book_story.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( navigator: Navigator ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + canScroll = { + listState.canScrollForward + } + ) Scaffold( Modifier @@ -42,36 +47,26 @@ fun SettingsScreen( .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { - AnimatedTopAppBar( + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.settings_screen)) + }, + navigationIcon = { + GoBackButton(navigator = navigator) + }, scrollBehavior = scrollBehavior, - isTopBarScrolled = null, - - content1NavigationIcon = { - IconButton(onClick = { navigator.navigateBack() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Go back", - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, - content1Title = { - Text( - stringResource(id = R.string.settings_screen), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - }, - content1Actions = {} + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.elevation() + ) ) } ) { paddingValues -> LazyColumn( Modifier .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) + .padding(top = paddingValues.calculateTopPadding()), + state = listState ) { item { SettingsCategoryItem( @@ -79,7 +74,7 @@ fun SettingsScreen( text = stringResource(id = R.string.general_settings), description = stringResource(id = R.string.general_settings_desc) ) { - navigator.navigate(Screen.GENERAL_SETTINGS) + navigator.navigate(Screen.GENERAL_SETTINGS, false) } } @@ -89,11 +84,19 @@ fun SettingsScreen( text = stringResource(id = R.string.appearance_settings), description = stringResource(id = R.string.appearance_settings_desc) ) { - navigator.navigate(Screen.APPEARANCE_SETTINGS) + navigator.navigate(Screen.APPEARANCE_SETTINGS, false) } } - //todo Reader settings + item { + SettingsCategoryItem( + icon = Icons.AutoMirrored.Filled.MenuBook, + text = stringResource(id = R.string.reader_settings), + description = stringResource(id = R.string.reader_settings_desc) + ) { + navigator.navigate(Screen.READER_SETTINGS, false) + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt index e7fbcf54..afa52624 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt @@ -14,6 +14,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import ua.acclorite.book_story.presentation.components.CategoryTitle +/** + * Checkbox with title. + */ @Composable fun CheckboxWithTitle( selected: Boolean, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt index 8d8fa4d0..76516e72 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt @@ -22,6 +22,9 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.presentation.components.CategoryTitle +/** + * Chips with title. Use list of [ChipItem]s to display chips. + */ @OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class) @Composable fun ChipsWithTitle( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt index 3396ee86..2906f581 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt @@ -28,6 +28,9 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.CategoryTitle +/** + * Color picker with title. + */ @Composable fun ColorPickerWithTitle( value: Color, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt index 3babc5c4..6742a3d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt @@ -17,10 +17,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +/** + * Settings category item. It is used to represent the whole category. + */ @Composable fun SettingsCategoryItem( icon: ImageVector, @@ -53,13 +57,17 @@ fun SettingsCategoryItem( text, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, - fontSize = 18.sp + fontSize = 18.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(2.dp)) Text( description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt index c3c048ec..63515b35 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt @@ -18,6 +18,9 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.presentation.components.CategoryTitle import kotlin.math.roundToInt +/** + * Slider with title. + */ @Composable fun SliderWithTitle( modifier: Modifier = Modifier, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt index 5405f01a..cff4c1bb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt @@ -1,16 +1,16 @@ package ua.acclorite.book_story.presentation.screens.settings.nested.appearance +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -18,29 +18,39 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.ChipItem -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.CategoryTitle +import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.AppearanceSettingsThemeSwitcher +import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle +import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher import ua.acclorite.book_story.ui.DarkTheme +import ua.acclorite.book_story.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppearanceSettings( - mainViewModel: MainViewModel = hiltViewModel(), + mainViewModel: MainViewModel, navigator: Navigator ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + canScroll = { + listState.canScrollForward + } + ) val darkTheme = mainViewModel.darkTheme.collectAsState().value!! + val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!! + val fontColor = mainViewModel.fontColor.collectAsState().value!! Scaffold( Modifier @@ -49,37 +59,40 @@ fun AppearanceSettings( .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { - AnimatedTopAppBar( + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.appearance_settings)) + }, + navigationIcon = { + GoBackButton(navigator = navigator) + }, scrollBehavior = scrollBehavior, - isTopBarScrolled = null, - - content1NavigationIcon = { - IconButton(onClick = { navigator.navigateBack() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Go back", - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, - content1Title = { - Text( - stringResource(id = R.string.appearance_settings), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - }, - content1Actions = {} + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.elevation() + ) ) } ) { paddingValues -> LazyColumn( Modifier .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) + .padding(top = paddingValues.calculateTopPadding()), + state = listState ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + } + item { + CategoryTitle( + title = stringResource(id = R.string.theme_appearance_settings), + color = MaterialTheme.colorScheme.primary + ) + } + item { + Spacer(modifier = Modifier.height(8.dp)) + } + item { ChipsWithTitle( title = stringResource(id = R.string.dark_theme_option), @@ -107,6 +120,46 @@ fun AppearanceSettings( item { AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel) } + + item { + Spacer(modifier = Modifier.height(22.dp)) + } + item { + CategoryTitle( + title = stringResource(id = R.string.colors_appearance_settings), + color = MaterialTheme.colorScheme.primary + ) + } + item { + Spacer(modifier = Modifier.height(8.dp)) + } + + item { + ColorPickerWithTitle( + value = Color(backgroundColor.toULong()), + title = stringResource(id = R.string.background_color_option), + onValueChange = { + mainViewModel.onEvent( + MainEvent.OnChangeBackgroundColor( + it.value.toLong() + ) + ) + } + ) + } + item { + ColorPickerWithTitle( + value = Color(fontColor.toULong()), + title = stringResource(id = R.string.font_color_option), + onValueChange = { + mainViewModel.onEvent( + MainEvent.OnChangeFontColor( + it.value.toLong() + ) + ) + } + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt similarity index 97% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt index d543800d..0a73e1b6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components +package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher import android.os.Build import androidx.compose.foundation.layout.Column @@ -21,6 +21,9 @@ import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.ui.isDark import ua.acclorite.book_story.util.Constants +/** + * Theme switcher. + */ @Composable fun AppearanceSettingsThemeSwitcher( mainViewModel: MainViewModel diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt similarity index 98% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt index df5e3b9b..e4bdd814 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components +package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -33,6 +33,9 @@ import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.ui.colorScheme import ua.acclorite.book_story.util.UIText +/** + * Theme switcher item. + */ @Composable fun AppearanceSettingsThemeSwitcherItem( theme: Pair, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt index 01cc5d76..2d2d8160 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt @@ -7,11 +7,9 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -22,24 +20,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.hilt.navigation.compose.hiltViewModel import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.ChipItem -import ua.acclorite.book_story.presentation.Navigator -import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle +import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.util.Constants @OptIn(ExperimentalMaterial3Api::class) @Composable fun GeneralSettings( - mainViewModel: MainViewModel = hiltViewModel(), + mainViewModel: MainViewModel, navigator: Navigator ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + canScroll = { + listState.canScrollForward + } + ) val context = LocalContext.current as ComponentActivity val language = mainViewModel.language.collectAsState().value!! @@ -51,36 +53,26 @@ fun GeneralSettings( .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { - AnimatedTopAppBar( + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.general_settings)) + }, + navigationIcon = { + GoBackButton(navigator = navigator) + }, scrollBehavior = scrollBehavior, - isTopBarScrolled = null, - - content1NavigationIcon = { - IconButton(onClick = { navigator.navigateBack() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Go back", - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, - content1Title = { - Text( - stringResource(id = R.string.general_settings), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - }, - content1Actions = {} + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.elevation() + ) ) } ) { paddingValues -> LazyColumn( Modifier .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) + .padding(top = paddingValues.calculateTopPadding()), + state = listState ) { item { ChipsWithTitle( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt new file mode 100644 index 00000000..a1b10ba3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt @@ -0,0 +1,217 @@ +package ua.acclorite.book_story.presentation.screens.settings.nested.reader + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.model.ChipItem +import ua.acclorite.book_story.presentation.components.CategoryTitle +import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.data.MainEvent +import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle +import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle +import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle +import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.util.Constants + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderSettings( + mainViewModel: MainViewModel, + navigator: Navigator +) { + val listState = rememberLazyListState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + canScroll = { + listState.canScrollForward + } + ) + + val fontFamily = Constants.FONTS.find { + it.id == mainViewModel.fontFamily.collectAsState().value!! + } ?: Constants.FONTS[0] + val fontStyle = mainViewModel.isItalic.collectAsState().value!! + val fontSize = mainViewModel.fontSize.collectAsState().value!! + val lineHeight = mainViewModel.lineHeight.collectAsState().value!! + val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!! + val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!! + + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.reader_settings)) + }, + navigationIcon = { + GoBackButton(navigator = navigator) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.elevation() + ) + ) + } + ) { paddingValues -> + LazyColumn( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + } + item { + CategoryTitle( + title = stringResource(id = R.string.font_reader_settings), + color = MaterialTheme.colorScheme.primary + ) + } + item { + Spacer(modifier = Modifier.height(8.dp)) + } + item { + ChipsWithTitle( + title = stringResource(id = R.string.font_family_option), + chips = Constants.FONTS + .map { + ChipItem( + it.id, + it.fontName.asString(), + MaterialTheme.typography.labelLarge.copy( + fontFamily = it.font + ), + it.id == fontFamily.id + ) + }, + onClick = { + mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id)) + } + ) + } + item { + ChipsWithTitle( + title = stringResource(id = R.string.font_style_option), + chips = listOf( + ChipItem( + "normal", + stringResource(id = R.string.font_style_normal), + MaterialTheme.typography.labelLarge.copy( + fontFamily = fontFamily.font, + fontStyle = FontStyle.Normal + ), + !fontStyle + ), + ChipItem( + "italic", + stringResource(id = R.string.font_style_italic), + MaterialTheme.typography.labelLarge.copy( + fontFamily = fontFamily.font, + fontStyle = FontStyle.Italic + ), + fontStyle + ), + ), + onClick = { + mainViewModel.onEvent( + MainEvent.OnChangeFontStyle( + when (it.id) { + "italic" -> true + else -> false + } + ) + ) + } + ) + } + item { + SliderWithTitle( + value = fontSize to "pt", + fromValue = 10, + toValue = 35, + title = stringResource(id = R.string.font_size_option), + onValueChange = { + mainViewModel.onEvent( + MainEvent.OnChangeFontSize(it) + ) + } + ) + } + item { + Spacer(modifier = Modifier.height(22.dp)) + } + item { + CategoryTitle( + title = stringResource(id = R.string.text_reader_settings), + color = MaterialTheme.colorScheme.primary + ) + } + item { + Spacer(modifier = Modifier.height(8.dp)) + } + item { + SliderWithTitle( + value = lineHeight to "pt", + fromValue = 1, + toValue = 16, + title = stringResource(id = R.string.line_height_option), + onValueChange = { + mainViewModel.onEvent( + MainEvent.OnChangeLineHeight(it) + ) + } + ) + } + item { + SliderWithTitle( + value = paragraphHeight to "pt", + fromValue = 0, + toValue = 24, + title = stringResource(id = R.string.paragraph_height_option), + onValueChange = { + mainViewModel.onEvent( + MainEvent.OnChangeParagraphHeight(it) + ) + } + ) + } + item { + CheckboxWithTitle( + selected = paragraphIndentation, + title = stringResource(id = R.string.paragraph_indentation_option) + ) { + mainViewModel.onEvent( + MainEvent.OnChangeParagraphIndentation(!paragraphIndentation) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt index 71e701e7..71b8279f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt @@ -17,10 +17,16 @@ object Transitions { val FadeTransitionOut = fadeOut(tween(50)) val SlidingTransitionIn = fadeIn(tween(300)) + - slideInHorizontally(tween(300)) { it / 15 } + slideInHorizontally(tween(300)) { it / 16 } + + val BackSlidingTransitionIn = fadeIn(tween(300)) + + slideInHorizontally(tween(300)) { -it / 16 } val SlidingTransitionOut = fadeOut(tween(200)) + - slideOutHorizontally(tween(200)) { it / 25 } + slideOutHorizontally(tween(300)) { -it / 16 } + + val BackSlidingTransitionOut = fadeOut(tween(200)) + + slideOutHorizontally(tween(300)) { it / 16 } } @Composable diff --git a/app/src/main/java/ua/acclorite/book_story/util/UIText.kt b/app/src/main/java/ua/acclorite/book_story/util/UIText.kt index c859b54d..223ccf24 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/UIText.kt +++ b/app/src/main/java/ua/acclorite/book_story/util/UIText.kt @@ -6,9 +6,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -sealed class UIText { - data class StringValue(val value: String) : UIText() - class StringResource(@StringRes val resId: Int, vararg val args: Any) : UIText() +sealed class UIText(val string: String?) { + data class StringValue(val value: String) : UIText(value) + class StringResource(@StringRes val resId: Int, vararg val args: Any) : UIText(null) @Composable fun asString(): String { diff --git a/app/src/main/res/drawable/app_icon_colored.png b/app/src/main/res/drawable/app_icon_colored.png index e76100d1..3615361a 100644 Binary files a/app/src/main/res/drawable/app_icon_colored.png and b/app/src/main/res/drawable/app_icon_colored.png differ diff --git a/app/src/main/res/drawable/app_icon_monochrome.png b/app/src/main/res/drawable/app_icon_monochrome.png new file mode 100644 index 00000000..0bc46a28 Binary files /dev/null and b/app/src/main/res/drawable/app_icon_monochrome.png differ diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml index a088bfa2..fd0864f8 100644 --- a/app/src/main/res/drawable/splash_icon.xml +++ b/app/src/main/res/drawable/splash_icon.xml @@ -2,7 +2,7 @@ \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/app_icon.xml b/app/src/main/res/mipmap-anydpi-v26/app_icon.xml similarity index 60% rename from app/src/main/res/mipmap-anydpi/app_icon.xml rename to app/src/main/res/mipmap-anydpi-v26/app_icon.xml index be2d22d2..e32ce33f 100644 --- a/app/src/main/res/mipmap-anydpi/app_icon.xml +++ b/app/src/main/res/mipmap-anydpi-v26/app_icon.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/app_icon_round.xml b/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml similarity index 60% rename from app/src/main/res/mipmap-anydpi/app_icon_round.xml rename to app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml index be2d22d2..e32ce33f 100644 --- a/app/src/main/res/mipmap-anydpi/app_icon_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/app_icon.webp b/app/src/main/res/mipmap-hdpi/app_icon.webp index 62ba9e1c..98437d18 100644 Binary files a/app/src/main/res/mipmap-hdpi/app_icon.webp and b/app/src/main/res/mipmap-hdpi/app_icon.webp differ diff --git a/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp index 06f5c8f4..e9f6b85f 100644 Binary files a/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp index 2078e7fb..96b04fce 100644 Binary files a/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp differ diff --git a/app/src/main/res/mipmap-hdpi/app_icon_round.webp b/app/src/main/res/mipmap-hdpi/app_icon_round.webp index 5a201221..bf61c7b9 100644 Binary files a/app/src/main/res/mipmap-hdpi/app_icon_round.webp and b/app/src/main/res/mipmap-hdpi/app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/app_icon.webp b/app/src/main/res/mipmap-mdpi/app_icon.webp index 17f3912c..70ae38f3 100644 Binary files a/app/src/main/res/mipmap-mdpi/app_icon.webp and b/app/src/main/res/mipmap-mdpi/app_icon.webp differ diff --git a/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp index eeb288e4..8196e481 100644 Binary files a/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp index 3e46530f..132ec0d3 100644 Binary files a/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp differ diff --git a/app/src/main/res/mipmap-mdpi/app_icon_round.webp b/app/src/main/res/mipmap-mdpi/app_icon_round.webp index b0889cab..a837a3b4 100644 Binary files a/app/src/main/res/mipmap-mdpi/app_icon_round.webp and b/app/src/main/res/mipmap-mdpi/app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/app_icon.webp b/app/src/main/res/mipmap-xhdpi/app_icon.webp index 73172c04..f7ce11aa 100644 Binary files a/app/src/main/res/mipmap-xhdpi/app_icon.webp and b/app/src/main/res/mipmap-xhdpi/app_icon.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp index 67bc1941..dc7a25de 100644 Binary files a/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp index ee458a14..0587af88 100644 Binary files a/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/app_icon_round.webp b/app/src/main/res/mipmap-xhdpi/app_icon_round.webp index 24508b61..689c93a3 100644 Binary files a/app/src/main/res/mipmap-xhdpi/app_icon_round.webp and b/app/src/main/res/mipmap-xhdpi/app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon.webp b/app/src/main/res/mipmap-xxhdpi/app_icon.webp index c04de250..e5e7fdb3 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp index b14b0285..9cafdb0b 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp index 2e0651e1..5e79d3ca 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp b/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp index 9f81e3a6..af8653bb 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon.webp index a56c37e3..96f71076 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp index 511230fb..70b4d01d 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp index 2614838c..f01fe04a 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp index 7654f8b6..f962189c 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp differ diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6e172b27..b1f63a7f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -5,7 +5,7 @@ Бібліотека Історія - Оглянути + Огляд Налаштування Про застосунок Допомога @@ -61,6 +61,15 @@ Щось пішло не так. Будь ласка, перевірте чи файл цієї книги не пошкоджений. + Хибний формат файлу. + Помилка: %1$s + Цей файл зашифрований. + Цей файл пустий або пошкоджений. + Не знайдено жодного перекладача. + Не знайдено жодного браузера. + + Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її. + @@ -96,6 +105,10 @@ Перемістити цю книгу в \"Прочитано\" Назад в Бібліотеку Скасувати + Копіювати + Перекласти + Словник + Ще раз файли @@ -106,6 +119,7 @@ Невідомий Вам потрібно читати більше! Так тримати! + Браво, ви повністю прочитали цю книгу! Змінити обкладинку Виберіть нову обкладинку з ваших фото Видалити обкладинку @@ -121,24 +135,30 @@ Головні - Мова застосунку + Мова застосунка Зовнішній вигляд - Тема застосунку, темна тема - Шрифт - Текст + Налаштування теми, кольори + Читач + Налаштування шрифта, налаштування текста + + + Шрифт + Текст + Налаштування теми + Кольори - Мова застосунку + Мова застосунка Темна тема - Тема застосунку + Тема застосунка Сімейство шрифтів - Стиль шрифту - Розмір шрифту + Стиль шрифта + Розмір шрифта Висота рядка - Висота абзацу + Висота абзаца Абзацний відступ - Колір фону - Колір шрифту + Колір фона + Колір шрифта Червоний @@ -172,7 +192,6 @@ Обкладинка була успішно змінена. Обкладинка була успішно видалена. Назва була успішно змінена. - Файл \"%1$s\" не знайдено. Ця книга була успішно видалена. Ця книга була успішно переміщена. Скопійовано @@ -180,6 +199,8 @@ Елемент історії був успішно видалений. + Файл \"%1$s\" не знайдено. + Текст, автор та опис книги були успішно оновлені. Перемістити @@ -200,6 +221,7 @@ Дякуємо за те, що прочитали \"%1$s\" з нами. Ми сподіваємось, що вам сподобалася ця книга! + %1$s букв, %2$s слів. За замовчуванням @@ -208,4 +230,11 @@ Сьогодні Вчора + + Оновити текст, автора та опис книги + Продовжити читання + Назад + Обкладинка + Відкрити налаштування читача + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4206141..5d8568b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - 0.8.0 + 0.9.0 Book\'s Story @@ -57,11 +57,20 @@ No description. - File was not found. Please check whether this book\'s file still exist. + File was not found. Please check whether this book\'s file still exists. Something went wrong. Please check whether this book\'s file is not corrupted. + Wrong file format. + Error: %1$s + This file is encrypted. + This file is empty or corrupted. + No translator app found. + No browser app found. + + The book content is empty. Please, make sure that the book content is not empty and update it. + @@ -96,6 +105,10 @@ Move this book to \"Already Read\" Back to Library Undo + Copy + Translate + Dictionary + Retry files @@ -106,6 +119,7 @@ Unknown You need to read more! Keep it up! + Well done, you have read the whole book! Change cover image Choose new cover from your photos Delete cover image @@ -123,9 +137,15 @@ General App language Appearance - App theme, dark theme - Font - Text + Theme preferences, colors + Reader + Font settings, text settings + + + Font + Text + Theme preferences + Colors App language @@ -172,7 +192,6 @@ Cover Image was successfully changed. Cover Image was successfully deleted. Title was successfully changed. - File \"%1$s\" was not found. This book was successfully deleted. This book was successfully moved. Copied @@ -180,6 +199,8 @@ History element was successfully deleted. + File \"%1$s\" was not found. + Book text, author and description were successfully updated. Move @@ -200,6 +221,7 @@ Thank you for reading \"%1$s\" with us. We hope you enjoyed this book! + %1$s characters, %2$s words. Default @@ -208,4 +230,11 @@ Today Yesterday + + Refresh text, author and description of the book + Continue reading + Go back + Cover image + Open reader settings + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 972f848f..ad932758 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,6 +3,7 @@