0.9.4 - About Screen + New scrolling system in Reader(saving exact scroll position)

This commit is contained in:
acclorite 2024-03-20 15:32:20 +02:00
parent 4e948bc5d7
commit 1689e9d7b9
109 changed files with 1870 additions and 994 deletions

View file

@ -137,8 +137,10 @@ dependencies {
implementation("org.slf4j:slf4j-android:latest.release") implementation("org.slf4j:slf4j-android:latest.release")
implementation("org.jsoup:jsoup:latest.release") implementation("org.jsoup:jsoup:latest.release")
// App Compat for Language Switcher
implementation("androidx.appcompat:appcompat:latest.release") implementation("androidx.appcompat:appcompat:latest.release")
implementation("androidx.appcompat:appcompat-resources:latest.release") implementation("androidx.appcompat:appcompat-resources:latest.release")
// Coil for loading bitmaps
implementation("io.coil-kt:coil-compose:2.6.0") implementation("io.coil-kt:coil-compose:2.6.0")
} }

View file

@ -19,19 +19,24 @@
<intent> <intent>
<action android:name="android.intent.action.WEB_SEARCH" /> <action android:name="android.intent.action.WEB_SEARCH" />
</intent> </intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries> </queries>
<application <application
android:name=".util.BookApplication" android:name=".Application"
android:allowBackup="true" android:allowBackup="false"
android:icon="@mipmap/app_icon" android:icon="@mipmap/app_icon"
android:roundIcon="@mipmap/app_icon_round" android:roundIcon="@mipmap/app_icon_round"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Start.Splash" android:theme="@style/Theme.Start.Splash"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu"
tools:ignore="DataExtractionRules">
<activity <activity
android:name=".Activity" android:name=".Activity"
@ -44,6 +49,15 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application> </application>
</manifest> </manifest>

View file

@ -9,6 +9,8 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -18,17 +20,20 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.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.components.custom_navigation_rail.CustomNavigationRail
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.NavigationHost import ua.acclorite.book_story.presentation.data.NavigationHost
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.about.AboutScreen
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
@ -43,11 +48,9 @@ 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.appearance.AppearanceSettings
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings 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.presentation.screens.settings.nested.reader.ReaderSettings
import ua.acclorite.book_story.ui.BooksHistoryResurrectionTheme import ua.acclorite.book_story.presentation.ui.BooksHistoryResurrectionTheme
import ua.acclorite.book_story.ui.DarkTheme import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.isDark
import java.lang.reflect.Field import java.lang.reflect.Field
@ -89,18 +92,26 @@ class Activity : AppCompatActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
val windowClass = calculateWindowSizeClass(activity = this)
val isLoaded by mainViewModel.isReady.collectAsState() val isLoaded by mainViewModel.isReady.collectAsState()
val state by mainViewModel.state.collectAsState()
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE val windowClass = calculateWindowSizeClass(activity = this)
val darkTheme = val tabletUI = remember(windowClass) {
mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM windowClass.widthSizeClass != WindowWidthSizeClass.Compact
val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact }
val localLayoutDirection = LocalLayoutDirection.current
val layoutDirection = remember(localLayoutDirection) {
if (localLayoutDirection == LayoutDirection.Ltr) {
LayoutDirection.Ltr
} else {
LayoutDirection.Rtl
}
}
if (isLoaded) { if (isLoaded) {
BooksHistoryResurrectionTheme( BooksHistoryResurrectionTheme(
theme = theme, theme = state.theme!!,
isDark = darkTheme.isDark() isDark = state.darkTheme!!.isDark()
) { ) {
NavigationHost(startScreen = Screen.LIBRARY) { NavigationHost(startScreen = Screen.LIBRARY) {
val currentScreen by this.getCurrentScreen().collectAsState() val currentScreen by this.getCurrentScreen().collectAsState()
@ -119,24 +130,36 @@ class Activity : AppCompatActivity() {
} }
}, },
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
) {
Box(
modifier = Modifier
.fillMaxSize()
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding( .padding(
start = if (tabletUI) 80.dp else 0.dp, end = it.calculateEndPadding(
bottom = it.calculateBottomPadding() layoutDirection
),
bottom = it.calculateBottomPadding(),
start = if (tabletUI) {
80.dp + it.calculateStartPadding(
layoutDirection
)
} else {
it.calculateStartPadding(
layoutDirection
)
},
) )
) { ) {
composable(screen = Screen.LIBRARY) { composable(screen = Screen.LIBRARY) {
@Suppress("UNCHECKED_CAST")
LibraryScreen( LibraryScreen(
viewModel = libraryViewModel, viewModel = libraryViewModel,
historyViewModel = historyViewModel, historyViewModel = historyViewModel,
browseViewModel = browseViewModel, browseViewModel = browseViewModel,
navigator = this@NavigationHost, navigator = this@NavigationHost
addedBooks = retrieveArgument("added_books") as? List<Book>
?: emptyList()
) )
} }
@ -158,7 +181,11 @@ class Activity : AppCompatActivity() {
} }
if (tabletUI) { if (tabletUI) {
CustomNavigationRail(navigator = this@NavigationHost) CustomNavigationRail(
navigator = this@NavigationHost,
layoutDirection = layoutDirection
)
}
} }
} }
} }
@ -234,6 +261,15 @@ class Activity : AppCompatActivity() {
) )
} }
// About screen
composable(
screen = Screen.ABOUT,
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
AboutScreen(navigator = this@NavigationHost)
}
// Start screen (later) // Start screen (later)
// composable(screen = Screen.START) { // composable(screen = Screen.START) {
// StartScreen() // StartScreen()

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.util package ua.acclorite.book_story
import android.app.Application import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp @HiltAndroidApp
class BookApplication : Application() class Application : Application()

View file

@ -22,6 +22,6 @@ object AppModule {
app, app,
BookDatabase::class.java, BookDatabase::class.java,
"book_db" "book_db"
).build().dao ).allowMainThreadQueries().build().dao
} }
} }

View file

@ -12,6 +12,8 @@ data class BookEntity(
val description: String?, val description: String?,
val text: String, val text: String,
val filePath: String, val filePath: String,
val scrollIndex: Int,
val scrollOffset: Int,
val progress: Float, val progress: Float,
val image: String? = null, val image: String? = null,
val category: Category val category: Category

View file

@ -43,6 +43,9 @@ interface BookDao {
@Query("SELECT * FROM historyentity") @Query("SELECT * FROM historyentity")
suspend fun getHistory(): List<HistoryEntity> suspend fun getHistory(): List<HistoryEntity>
@Query("SELECT * FROM historyentity WHERE bookId = :bookId ORDER BY time DESC LIMIT 1")
fun getLatestHistoryForBook(bookId: Int): HistoryEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHistory( suspend fun insertHistory(
history: List<HistoryEntity> history: List<HistoryEntity>

View file

@ -4,7 +4,7 @@ import android.net.Uri
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -18,6 +18,8 @@ class BookMapperImpl @Inject constructor() : BookMapper {
id = book.id, id = book.id,
title = book.title, title = book.title,
filePath = book.filePath, filePath = book.filePath,
scrollIndex = book.scrollIndex,
scrollOffset = book.scrollOffset,
progress = book.progress, progress = book.progress,
author = book.author.string, author = book.author.string,
text = textAsString, text = textAsString,
@ -37,9 +39,13 @@ class BookMapperImpl @Inject constructor() : BookMapper {
R.string.unknown_author R.string.unknown_author
), ),
description = bookEntity.description, description = bookEntity.description,
scrollIndex = bookEntity.scrollIndex,
scrollOffset = bookEntity.scrollOffset,
progress = bookEntity.progress, progress = bookEntity.progress,
file = if (file.exists()) file else null, file = if (file.exists()) file else null,
text = emptyList(), text = emptyList(),
letters = 0,
words = 0,
filePath = bookEntity.filePath, filePath = bookEntity.filePath,
lastOpened = null, lastOpened = null,
category = bookEntity.category, category = bookEntity.category,

View file

@ -17,6 +17,7 @@ class HistoryMapperImpl @Inject constructor() : HistoryMapper {
return History( return History(
historyEntity.id, historyEntity.id,
bookId = historyEntity.bookId, bookId = historyEntity.bookId,
book = null,
time = historyEntity.time time = historyEntity.time
) )
} }

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import java.io.File import java.io.File

View file

@ -4,7 +4,7 @@ import ua.acclorite.book_story.data.parser.epub.EpubFileParser
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
import ua.acclorite.book_story.data.parser.txt.TxtFileParser import ua.acclorite.book_story.data.parser.txt.TxtFileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import java.io.File import java.io.File
interface TextParser { interface TextParser {

View file

@ -5,8 +5,8 @@ import ua.acclorite.book_story.data.parser.epub.EpubTextParser
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
import ua.acclorite.book_story.data.parser.txt.TxtTextParser import ua.acclorite.book_story.data.parser.txt.TxtTextParser
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject

View file

@ -7,8 +7,8 @@ import nl.siegmann.epublib.epub.EpubReader
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import javax.inject.Inject import javax.inject.Inject
@ -50,6 +50,10 @@ class EpubFileParser @Inject constructor() : FileParser {
author = author, author = author,
description = description?.toString(), description = description?.toString(),
text = emptyList(), text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0,
scrollOffset = 0,
progress = 0f, progress = 0f,
file = file, file = file,
filePath = file.path, filePath = file.path,

View file

@ -7,8 +7,8 @@ import org.jsoup.Jsoup
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream

View file

@ -7,9 +7,8 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -29,8 +28,6 @@ class PdfFileParser @Inject constructor(private val application: Application) :
val fileAuthor = document.documentInformation.author val fileAuthor = document.documentInformation.author
val author = if (fileAuthor != null) UIText.StringValue(fileAuthor) val author = if (fileAuthor != null) UIText.StringValue(fileAuthor)
else UIText.StringResource(R.string.unknown_author) else UIText.StringResource(R.string.unknown_author)
val text = emptyList<StringWithId>()
val description = document.documentInformation.subject ?: null val description = document.documentInformation.subject ?: null
document.close() document.close()
@ -40,7 +37,11 @@ class PdfFileParser @Inject constructor(private val application: Application) :
title = title, title = title,
author = author, author = author,
description = description, description = description,
text = text, text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0,
scrollOffset = 0,
progress = 0f, progress = 0f,
file = file, file = file,
filePath = file.path, filePath = file.path,

View file

@ -7,8 +7,8 @@ import com.tom_roush.pdfbox.text.PDFTextStripper
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -28,11 +28,6 @@ class PdfTextParser @Inject constructor(private val application: Application) :
val pdfStripper = PDFTextStripper() val pdfStripper = PDFTextStripper()
pdfStripper.paragraphStart = "_new_line_" pdfStripper.paragraphStart = "_new_line_"
if (document.isEncrypted) {
document.close()
return Resource.Error(UIText.StringResource(R.string.error_file_encrypted))
}
val oldText = pdfStripper.getText(document) val oldText = pdfStripper.getText(document)
.replace("\r", "") .replace("\r", "")

View file

@ -4,9 +4,8 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -20,14 +19,17 @@ class TxtFileParser @Inject constructor() : FileParser {
try { try {
val title = file.name.trim().dropLast(4) val title = file.name.trim().dropLast(4)
val author = UIText.StringResource(R.string.unknown_author) val author = UIText.StringResource(R.string.unknown_author)
val text = emptyList<StringWithId>()
return Book( return Book(
id = null, id = null,
title = title, title = title,
author = author, author = author,
description = null, description = null,
text = text, text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0,
scrollOffset = 0,
progress = 0f, progress = 0f,
file = file, file = file,
filePath = file.path, filePath = file.path,

View file

@ -5,8 +5,8 @@ import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileReader import java.io.FileReader

View file

@ -5,11 +5,12 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore 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.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
@ -20,12 +21,13 @@ import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.data.calculateFamiliarity import ua.acclorite.book_story.presentation.data.calculateFamiliarity
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.Resource
import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -47,17 +49,15 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> { override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> {
return flow { return flow {
emit(Resource.Loading(true)) emit(Resource.Loading(true))
val books = database.searchBooks(query) val books = database.searchBooks(query)
val history = database.getHistory()
emit( emit(
Resource.Success( Resource.Success(
data = books.map { entity -> data = books.map { entity ->
val book = bookMapper.toBook(entity) val book = bookMapper.toBook(entity)
val lastHistory = history val lastHistory = database.getLatestHistoryForBook(
.filter { it.bookId == book.id } book.id ?: -1
.maxByOrNull { it.time } )
book.copy( book.copy(
lastOpened = lastHistory?.time lastOpened = lastHistory?.time
@ -70,12 +70,12 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun fastGetBooks(query: String): List<Book> { override suspend fun fastGetBooks(query: String): List<Book> {
val books = database.searchBooks(query) val books = database.searchBooks(query)
val history = database.getHistory()
return books.map { entity -> return books.map { entity ->
val book = bookMapper.toBook(entity) val book = bookMapper.toBook(entity)
val lastHistory = val lastHistory = database.getLatestHistoryForBook(
history.filter { it.bookId == book.id }.maxByOrNull { it.time } book.id ?: -1
)
book.copy( book.copy(
lastOpened = lastHistory?.time lastOpened = lastHistory?.time
@ -84,7 +84,18 @@ class BookRepositoryImpl @Inject constructor(
} }
override suspend fun getBooksById(ids: List<Int>): List<Book> { override suspend fun getBooksById(ids: List<Int>): List<Book> {
return database.findBooksById(ids).map { bookMapper.toBook(it) } val books = database.findBooksById(ids)
return books.map { entity ->
val book = bookMapper.toBook(entity)
val lastHistory = database.getLatestHistoryForBook(
book.id ?: -1
)
book.copy(
lastOpened = lastHistory?.time
)
}
} }
override suspend fun getBookTextById(bookId: Int): String { override suspend fun getBookTextById(bookId: Int): String {
@ -92,22 +103,26 @@ class BookRepositoryImpl @Inject constructor(
return book.text return book.text
} }
override suspend fun findBook(
id: Int
): BookEntity {
return database.findBookById(id)
}
override suspend fun insertBooks( override suspend fun insertBooks(
books: List<Pair<Book, CoverImage?>> books: List<Pair<Book, CoverImage?>>
) { ) {
val filesDir = application.filesDir
val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) {
coversDir.mkdirs()
}
val booksWithCover = books.map { val booksWithCover = books.map {
var uri = "" var uri = ""
if (it.second != null) { if (it.second != null) {
try { try {
uri = "${UUID.randomUUID()}.webp" uri = "${UUID.randomUUID()}.webp"
application.openFileOutput(uri, 0x0000).use { stream -> val cover = File(coversDir, uri)
withContext(Dispatchers.IO) {
FileOutputStream(cover).use { stream ->
if ( if (
!it.second!!.compress( !it.second!!.compress(
Bitmap.CompressFormat.WEBP, Bitmap.CompressFormat.WEBP,
@ -118,6 +133,7 @@ class BookRepositoryImpl @Inject constructor(
throw Exception("Couldn't save cover image") throw Exception("Couldn't save cover image")
} }
} }
}
} catch (e: Exception) { } catch (e: Exception) {
uri = "" uri = ""
e.printStackTrace() e.printStackTrace()
@ -126,7 +142,7 @@ class BookRepositoryImpl @Inject constructor(
val book = it.first.copy( val book = it.first.copy(
coverImage = if (uri.isNotBlank()) { coverImage = if (uri.isNotBlank()) {
Uri.fromFile(File("${application.filesDir}/$uri")) Uri.fromFile(File("$coversDir/$uri"))
} else null } else null
) )
@ -175,10 +191,20 @@ class BookRepositoryImpl @Inject constructor(
val book = database.findBookById(bookWithOldCover.id ?: return) val book = database.findBookById(bookWithOldCover.id ?: return)
var uri: String? = null var uri: String? = null
val filesDir = application.filesDir
val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) {
coversDir.mkdirs()
}
if (newCoverImage != null) { if (newCoverImage != null) {
try { try {
uri = "${UUID.randomUUID()}.webp" uri = "${UUID.randomUUID()}.webp"
application.openFileOutput(uri, 0x0000).use { stream -> val cover = File(coversDir, uri)
withContext(Dispatchers.IO) {
FileOutputStream(cover).use { stream ->
if ( if (
!newCoverImage.compress( !newCoverImage.compress(
Bitmap.CompressFormat.WEBP, Bitmap.CompressFormat.WEBP,
@ -189,6 +215,7 @@ class BookRepositoryImpl @Inject constructor(
throw Exception("Couldn't save cover image") throw Exception("Couldn't save cover image")
} }
} }
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return return
@ -197,16 +224,20 @@ class BookRepositoryImpl @Inject constructor(
if (book.image != null) { if (book.image != null) {
try { try {
application.deleteFile( val fileToDelete = File(
book.image.substringAfterLast("/") "$coversDir/${book.image.substringAfterLast("/")}"
) )
if (fileToDelete.exists()) {
fileToDelete.delete()
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
val newCoverImageUri = if (uri != null) { val newCoverImageUri = if (uri != null) {
Uri.fromFile(File("${application.filesDir}/$uri")) Uri.fromFile(File("$coversDir/$uri"))
} else { } else {
null null
} }
@ -228,15 +259,26 @@ class BookRepositoryImpl @Inject constructor(
} }
override suspend fun deleteBooks(books: List<Book>) { override suspend fun deleteBooks(books: List<Book>) {
val filesDir = application.filesDir
val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) {
coversDir.mkdirs()
}
database.deleteBooks( database.deleteBooks(
books.map { books.map {
val book = database.findBookById(it.id ?: return) val book = database.findBookById(it.id ?: return)
if (book.image != null) { if (book.image != null) {
try { try {
application.deleteFile( val fileToDelete = File(
book.image.substringAfterLast("/") "$coversDir/${book.image.substringAfterLast("/")}"
) )
if (fileToDelete.exists()) {
fileToDelete.delete()
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -301,14 +343,28 @@ class BookRepositoryImpl @Inject constructor(
ignoreCase = true ignoreCase = true
) )
} }
val isFileAlreadyAdded = existingBooks.all {
if (!isFileSupported) {
return@filter false
}
val isFileNotAdded = existingBooks.all {
it.filePath.substringAfterLast("/") != it.filePath.substringAfterLast("/") !=
file.path.substringAfterLast("/") file.path.substringAfterLast("/")
} }
if (!isFileNotAdded) {
return@filter false
}
val isQuery = if (query.isEmpty()) true else file.name.lowercase() val isQuery = if (query.isEmpty()) true else file.name.lowercase()
.contains(query.trim().lowercase()) .contains(query.trim().lowercase())
isFileAlreadyAdded && isFileSupported && isQuery if (!isQuery) {
return@filter false
}
true
}.forEach { }.forEach {
filteredFiles.add( filteredFiles.add(
it it
@ -414,7 +470,6 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun getHistory(): Flow<Resource<List<History>>> { override suspend fun getHistory(): Flow<Resource<List<History>>> {
return flow { return flow {
emit(Resource.Loading(true)) emit(Resource.Loading(true))
val history = database.getHistory() val history = database.getHistory()
emit( emit(
@ -429,6 +484,11 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
override suspend fun getLatestBookHistory(bookId: Int): History? {
val history = database.getLatestHistoryForBook(bookId)
return history?.let { historyMapper.toHistory(it) }
}
override suspend fun deleteWholeHistory() { override suspend fun deleteWholeHistory() {
database.deleteWholeHistory() database.deleteWholeHistory()
} }

View file

@ -2,17 +2,26 @@ package ua.acclorite.book_story.domain.model
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
@Immutable @Immutable
data class Book( data class Book(
val id: Int?, val id: Int?,
val title: String, val title: String,
val author: UIText, val author: UIText,
val description: String?, val description: String?,
// val textPath: String,
val text: List<StringWithId> = emptyList(), val text: List<StringWithId> = emptyList(),
val letters: Int,
val words: Int,
val scrollIndex: Int,
val scrollOffset: Int,
val progress: Float, val progress: Float,
val file: File?, val file: File?,
val filePath: String, val filePath: String,
val lastOpened: Long?, val lastOpened: Long?,

View file

@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
data class FontWithName( data class FontWithName(

View file

@ -6,5 +6,6 @@ import androidx.compose.runtime.Immutable
data class History( data class History(
val id: Int?, val id: Int?,
val bookId: Int, val bookId: Int,
val book: Book?,
val time: Long val time: Long
) )

View file

@ -1,8 +1,8 @@
package ua.acclorite.book_story.domain.model package ua.acclorite.book_story.domain.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
sealed class NullableBook( sealed class NullableBook(

View file

@ -2,12 +2,11 @@ package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import java.io.File import java.io.File
interface BookRepository { interface BookRepository {
@ -28,10 +27,6 @@ interface BookRepository {
bookId: Int bookId: Int
): String ): String
suspend fun findBook(
id: Int
): BookEntity
suspend fun insertBooks( suspend fun insertBooks(
books: List<Pair<Book, CoverImage?>> books: List<Pair<Book, CoverImage?>>
) )
@ -71,6 +66,8 @@ interface BookRepository {
suspend fun getHistory(): Flow<Resource<List<History>>> suspend fun getHistory(): Flow<Resource<List<History>>>
suspend fun getLatestBookHistory(bookId: Int): History?
suspend fun deleteWholeHistory() suspend fun deleteWholeHistory()
suspend fun deleteBookHistory(bookId: Int) suspend fun deleteBookHistory(bookId: Int)

View file

@ -1,14 +1,13 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.DataStoreConstants import ua.acclorite.book_story.domain.util.DataStoreConstants
import javax.inject.Inject import javax.inject.Inject
class ChangeLanguage @Inject constructor(private val repository: BookRepository) { class ChangeLanguage @Inject constructor(private val repository: BookRepository) {
@SuppressLint("AppBundleLocaleChanges")
suspend fun execute(language: String) { suspend fun execute(language: String) {
val appLocale = LocaleListCompat.forLanguageTags(language) val appLocale = LocaleListCompat.forLanguageTags(language)
AppCompatDelegate.setApplicationLocales(appLocale) AppCompatDelegate.setApplicationLocales(appLocale)

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import javax.inject.Inject import javax.inject.Inject
class GetBooks @Inject constructor(private val repository: BookRepository) { class GetBooks @Inject constructor(private val repository: BookRepository) {

View file

@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import javax.inject.Inject import javax.inject.Inject
class GetHistory @Inject constructor(private val repository: BookRepository) { class GetHistory @Inject constructor(private val repository: BookRepository) {

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetLatestHistory @Inject constructor(private val repository: BookRepository) {
suspend fun execute(bookId: Int): History? {
return repository.getLatestBookHistory(bookId)
}
}

View file

@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject import javax.inject.Inject
class InsertBooks @Inject constructor(private val repository: BookRepository) { class InsertBooks @Inject constructor(private val repository: BookRepository) {

View file

@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject import javax.inject.Inject
class UpdateCoverImageOfBook @Inject constructor(private val repository: BookRepository) { class UpdateCoverImageOfBook @Inject constructor(private val repository: BookRepository) {

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.util package ua.acclorite.book_story.domain.util
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
@ -8,23 +8,12 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.FontWithName import ua.acclorite.book_story.domain.model.FontWithName
import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
object Constants { object Constants {
// SavedStateHandle constants(actually just all settings params) // Main State Constant
const val LANGUAGE = "language" const val MAIN_STATE = "main_state"
const val THEME = "theme"
const val DARK_THEME = "dark_theme"
const val SHOW_START_SCREEN = "guide"
const val BACKGROUND_COLOR = "background_color"
const val FONT_COLOR = "font_color"
const val FONT = "font"
const val IS_ITALIC = "font_style"
const val FONT_SIZE = "font_size"
const val LINE_HEIGHT = "line_height"
const val PARAGRAPH_HEIGHT = "paragraph_height"
const val PARAGRAPH_INDENTATION = "paragraph_indentation"
// Supported file extensions // Supported file extensions
val EXTENSIONS = listOf(".txt", ".pdf", ".epub") val EXTENSIONS = listOf(".txt", ".pdf", ".epub")
@ -182,6 +171,10 @@ object Constants {
UIText.StringValue(""), UIText.StringValue(""),
null, null,
emptyList(), emptyList(),
0,
0,
0,
0,
0f, 0f,
null, null,
"", "",

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.util package ua.acclorite.book_story.domain.util
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
@ -10,7 +10,6 @@ object DataStoreConstants {
val THEME = stringPreferencesKey("theme") val THEME = stringPreferencesKey("theme")
val DARK_THEME = stringPreferencesKey("dark_theme") val DARK_THEME = stringPreferencesKey("dark_theme")
val SHOW_START_SCREEN = booleanPreferencesKey("guide") val SHOW_START_SCREEN = booleanPreferencesKey("guide")
val BACKGROUND_COLOR = longPreferencesKey("background_color") val BACKGROUND_COLOR = longPreferencesKey("background_color")
val FONT_COLOR = longPreferencesKey("font_color") val FONT_COLOR = longPreferencesKey("font_color")
val FONT = stringPreferencesKey("font") val FONT = stringPreferencesKey("font")

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.util package ua.acclorite.book_story.domain.util
sealed class Resource<T>(val data: T? = null, val message: UIText? = null) { sealed class Resource<T>(val data: T? = null, val message: UIText? = null) {
class Success<T>(data: T?) : Resource<T>(data) class Success<T>(data: T?) : Resource<T>(data)

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.util package ua.acclorite.book_story.domain.util
import android.graphics.Bitmap import android.graphics.Bitmap

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.util package ua.acclorite.book_story.domain.util
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes

View file

@ -28,7 +28,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info. * Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info.
@ -102,10 +102,12 @@ fun AnimatedTopAppBar(
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
) { ) {
TopAppBar( TopAppBar(
windowInsets = WindowInsets.statusBars,
navigationIcon = content1NavigationIcon, navigationIcon = content1NavigationIcon,
title = content1Title, title = content1Title,
actions = content1Actions, actions = content1Actions,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
modifier = Modifier.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = animatedContainerColor, containerColor = animatedContainerColor,
scrolledContainerColor = scrolledContainerColor scrolledContainerColor = scrolledContainerColor
@ -120,10 +122,12 @@ fun AnimatedTopAppBar(
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
) { ) {
TopAppBar( TopAppBar(
windowInsets = WindowInsets.statusBars,
navigationIcon = content2NavigationIcon, navigationIcon = content2NavigationIcon,
title = content2Title, title = content2Title,
actions = content2Actions, actions = content2Actions,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
modifier = Modifier.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = animatedContainerColor, containerColor = animatedContainerColor,
scrolledContainerColor = scrolledContainerColor scrolledContainerColor = scrolledContainerColor
@ -139,10 +143,12 @@ fun AnimatedTopAppBar(
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
) { ) {
TopAppBar( TopAppBar(
windowInsets = WindowInsets.statusBars,
navigationIcon = content3NavigationIcon, navigationIcon = content3NavigationIcon,
title = content3Title, title = content3Title,
actions = content3Actions, actions = content3Actions,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
modifier = Modifier.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = animatedContainerColor, containerColor = animatedContainerColor,
scrolledContainerColor = scrolledContainerColor scrolledContainerColor = scrolledContainerColor

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Checkbox. Has a Circle shape. * Custom Checkbox. Has a Circle shape.

View file

@ -11,13 +11,14 @@ 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. * Go back arrow button. Prevents double or triple clicking on it while going back action is performed.
*/ */
@Composable @Composable
fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) { fun GoBackButton(navigator: Navigator, enabled: Boolean = true, customOnClick: () -> Unit = {}) {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack, icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.go_back_content_desc), contentDescription = stringResource(id = R.string.go_back_content_desc),
disableOnClick = true disableOnClick = true,
enabled = enabled
) { ) {
navigator.navigateBack()
customOnClick() customOnClick()
navigator.navigateBack()
} }
} }

View file

@ -14,7 +14,7 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Bottom navigation bar, uses default [NavigationBar]. * Bottom navigation bar, uses default [NavigationBar].

View file

@ -36,14 +36,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Dialog with custom content. * Custom Dialog with custom content.
* *
* @param modifier Modifier to be applied. * @param modifier Modifier to be applied.
* @param properties Dialog properties.
* @param drawableIcon Drawable icon to show(1/2). * @param drawableIcon Drawable icon to show(1/2).
* @param imageVectorIcon Image vector icon to show(2/2). * @param imageVectorIcon Image vector icon to show(2/2).
* @param backgroundTransparency The transparency of the background behind the dialog. * @param backgroundTransparency The transparency of the background behind the dialog.
@ -60,7 +59,6 @@ import ua.acclorite.book_story.ui.elevation
@Composable @Composable
fun CustomDialogWithContent( fun CustomDialogWithContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties(),
drawableIcon: Painter? = null, drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null, imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f, backgroundTransparency: Float = 0.3f,
@ -76,7 +74,10 @@ fun CustomDialogWithContent(
var actionClicked by remember { mutableStateOf(false) } var actionClicked by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = { onDismiss() }, onDismissRequest = { onDismiss() },
properties = properties properties = DialogProperties(
dismissOnBackPress = !actionClicked,
dismissOnClickOutside = !actionClicked
)
) { ) {
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount( (LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
backgroundTransparency backgroundTransparency

View file

@ -37,15 +37,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Dialog with lazy column. * Custom Dialog with lazy column.
* *
* @param modifier Modifier to be applied. * @param modifier Modifier to be applied.
* @param properties Dialog properties.
* @param drawableIcon Drawable icon to show(1/2). * @param drawableIcon Drawable icon to show(1/2).
* @param imageVectorIcon Image vector icon to show(2/2). * @param imageVectorIcon Image vector icon to show(2/2).
* @param backgroundTransparency The transparency of the background behind the dialog. * @param backgroundTransparency The transparency of the background behind the dialog.
@ -62,7 +61,6 @@ import ua.acclorite.book_story.ui.elevation
@Composable @Composable
fun CustomDialogWithLazyColumn( fun CustomDialogWithLazyColumn(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties(),
drawableIcon: Painter? = null, drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null, imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f, backgroundTransparency: Float = 0.3f,
@ -78,7 +76,10 @@ fun CustomDialogWithLazyColumn(
var actionClicked by remember { mutableStateOf(false) } var actionClicked by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = { onDismiss() }, onDismissRequest = { onDismiss() },
properties = properties properties = DialogProperties(
dismissOnBackPress = !actionClicked,
dismissOnClickOutside = !actionClicked
)
) { ) {
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount( (LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
backgroundTransparency backgroundTransparency

View file

@ -2,8 +2,13 @@ package ua.acclorite.book_story.presentation.components.custom_navigation_rail
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -18,19 +23,21 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Navigation Rail. It is used to be shown on Tablets. * Custom Navigation Rail. It is used to be shown on Tablets.
*/ */
@Composable @Composable
fun CustomNavigationRail( fun BoxScope.CustomNavigationRail(
navigator: Navigator navigator: Navigator,
layoutDirection: LayoutDirection
) { ) {
var currentScreen: Screen? by remember { mutableStateOf(null) } var currentScreen: Screen? by remember { mutableStateOf(null) }
@ -44,10 +51,19 @@ fun CustomNavigationRail(
NavigationRail( NavigationRail(
modifier = Modifier modifier = Modifier
.width(80.dp) .align(
Alignment.CenterStart
)
.background(MaterialTheme.elevation()) .background(MaterialTheme.elevation())
.padding(
start = WindowInsets.navigationBars
.asPaddingValues()
.calculateStartPadding(layoutDirection)
)
.width(80.dp)
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
containerColor = MaterialTheme.elevation() containerColor = MaterialTheme.elevation(),
windowInsets = WindowInsets(0, 0, 0, 0)
) { ) {
Column( Column(
Modifier Modifier

View file

@ -12,5 +12,5 @@ sealed class MainEvent {
data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent() data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent()
data class OnChangeBackgroundColor(val color: Long) : MainEvent() data class OnChangeBackgroundColor(val color: Long) : MainEvent()
data class OnChangeFontColor(val color: Long) : MainEvent() data class OnChangeFontColor(val color: Long) : MainEvent()
data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent()
} }

View file

@ -0,0 +1,104 @@
package ua.acclorite.book_story.presentation.data
import android.os.Parcel
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.Theme
/**
* Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset.
*/
@Immutable
data class MainState(
val language: String? = null,
val theme: Theme? = null,
val darkTheme: DarkTheme? = null,
val fontFamily: String? = null,
val isItalic: Boolean? = null,
val fontSize: Int? = null,
val lineHeight: Int? = null,
val paragraphHeight: Int? = null,
val paragraphIndentation: Boolean? = null,
val backgroundColor: Long? = null,
val fontColor: Long? = null,
val showStartScreen: Boolean? = null,
) : Parcelable {
constructor(parcel: Parcel) : this(
// String
language = parcel.readString(),
// String
theme = Theme.valueOf(parcel.readString() ?: Theme.BLUE.name),
// String
darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name),
// String
fontFamily = parcel.readString(),
// Boolean
isItalic = if (parcel.readByte().toInt() != 0) parcel.readByte().toInt() == 1 else null,
// Int
fontSize = if (parcel.readByte().toInt() != 0) parcel.readInt() else null,
// Int
lineHeight = if (parcel.readByte().toInt() != 0) parcel.readInt() else null,
// Int
paragraphHeight = if (parcel.readByte().toInt() != 0) parcel.readInt() else null,
// Boolean
paragraphIndentation = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null,
// Long
backgroundColor = if (parcel.readByte().toInt() != 0) parcel.readLong() else null,
// Long
fontColor = if (parcel.readByte().toInt() != 0) parcel.readLong() else null,
// Boolean
showStartScreen = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null,
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
// Language
parcel.writeString(language)
// Theme
parcel.writeString(theme?.name)
// Dark Theme
parcel.writeString(darkTheme?.name)
// Font Family
parcel.writeString(fontFamily)
// Is Italic
parcel.writeByte((if (isItalic != null) 1 else 0).toByte())
isItalic?.let { parcel.writeByte(if (it) 1 else 0) }
// Font Size
parcel.writeByte((if (fontSize != null) 1 else 0).toByte())
fontSize?.let { parcel.writeInt(it) }
// Line Height
parcel.writeByte((if (lineHeight != null) 1 else 0).toByte())
lineHeight?.let { parcel.writeInt(it) }
// Paragraph Height
parcel.writeByte((if (paragraphHeight != null) 1 else 0).toByte())
paragraphHeight?.let { parcel.writeInt(it) }
// Paragraph Indentation
parcel.writeByte((if (paragraphIndentation != null) 1 else 0).toByte())
paragraphIndentation?.let { parcel.writeByte(if (it) 1 else 0) }
// Background color
parcel.writeByte((if (backgroundColor != null) 1 else 0).toByte())
backgroundColor?.let { parcel.writeLong(it) }
// Font color
parcel.writeByte((if (fontColor != null) 1 else 0).toByte())
fontColor?.let { parcel.writeLong(it) }
// Show Start Screen
parcel.writeByte((if (showStartScreen != null) 1 else 0).toByte())
showStartScreen?.let { parcel.writeByte(if (it) 1 else 0) }
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MainState> {
override fun createFromParcel(parcel: Parcel): MainState {
return MainState(parcel)
}
override fun newArray(size: Int): Array<MainState?> {
return arrayOfNulls(size)
}
}
}

View file

@ -8,7 +8,6 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -17,13 +16,13 @@ import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.use_case.ChangeLanguage 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.GetDatastore
import ua.acclorite.book_story.domain.use_case.SetDatastore import ua.acclorite.book_story.domain.use_case.SetDatastore
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DarkTheme import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.ui.toDarkTheme import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.ui.toTheme import ua.acclorite.book_story.presentation.ui.toTheme
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.DataStoreConstants
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -42,212 +41,143 @@ class MainViewModel @Inject constructor(
private val _isReady = MutableStateFlow(false) private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow() val isReady = _isReady.asStateFlow()
/* -- Language ----------------------------------------------------- */ private val _state: MutableStateFlow<MainState> = MutableStateFlow(
private var _language: String = stateHandle[Constants.MAIN_STATE] ?: MainState()
stateHandle[Constants.LANGUAGE] )
?: if ( val state = _state.asStateFlow()
Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }
) {
Locale.getDefault().language.take(2)
} else {
"en"
}
set(value) {
field = value
stateHandle[Constants.LANGUAGE] = value
}
val language: StateFlow<String?>
get() = stateHandle.getStateFlow(Constants.LANGUAGE, null)
/* -- Theme -------------------------------------------------------- */
private var _theme: Theme =
stateHandle[Constants.THEME]
?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.DYNAMIC else Theme.BLUE
set(value) {
field = value
stateHandle[Constants.THEME] = value
}
val theme: StateFlow<Theme?>
get() = stateHandle.getStateFlow(Constants.THEME, null)
/* -- Dark Theme --------------------------------------------------- */
private var _darkTheme: DarkTheme =
stateHandle[Constants.DARK_THEME] ?: DarkTheme.FOLLOW_SYSTEM
set(value) {
field = value
stateHandle[Constants.DARK_THEME] = value
}
val darkTheme: StateFlow<DarkTheme?>
get() = stateHandle.getStateFlow(Constants.DARK_THEME, null)
/* -- Show Start Screen -------------------------------------------- */
private var _showStartScreen: Boolean =
stateHandle[Constants.SHOW_START_SCREEN] ?: true
set(value) {
field = value
stateHandle[Constants.SHOW_START_SCREEN] = value
}
val showStartScreen: StateFlow<Boolean?>
get() = stateHandle.getStateFlow(Constants.SHOW_START_SCREEN, null)
/* -- Background Color --------------------------------------------- */
private var _backgroundColor: Long =
stateHandle[Constants.BACKGROUND_COLOR] ?: Color.DarkGray.value.toLong()
set(value) {
field = value
stateHandle[Constants.BACKGROUND_COLOR] = value
}
val backgroundColor: StateFlow<Long?>
get() = stateHandle.getStateFlow(Constants.BACKGROUND_COLOR, null)
/* -- Font Color --------------------------------------------------- */
private var _fontColor: Long =
stateHandle[Constants.FONT_COLOR] ?: Color.LightGray.value.toLong()
set(value) {
field = value
stateHandle[Constants.FONT_COLOR] = value
}
val fontColor: StateFlow<Long?>
get() = stateHandle.getStateFlow(Constants.FONT_COLOR, null)
/* -- Font Family -------------------------------------------------- */
private var _fontFamily: String =
stateHandle[Constants.FONT] ?: Constants.FONTS[0].id
set(value) {
field = value
stateHandle[Constants.FONT] = value
}
val fontFamily: StateFlow<String?>
get() = stateHandle.getStateFlow(Constants.FONT, null)
/* -- Is Italic ---------------------------------------------------- */
private var _isItalic: Boolean =
stateHandle[Constants.IS_ITALIC] ?: false
set(value) {
field = value
stateHandle[Constants.IS_ITALIC] = value
}
val isItalic: StateFlow<Boolean?>
get() = stateHandle.getStateFlow(Constants.IS_ITALIC, null)
/* -- Font Size ---------------------------------------------------- */
private var _fontSize: Int =
stateHandle[Constants.FONT_SIZE] ?: 16
set(value) {
field = value
stateHandle[Constants.FONT_SIZE] = value
}
val fontSize: StateFlow<Int?>
get() = stateHandle.getStateFlow(Constants.FONT_SIZE, null)
/* -- Line Height -------------------------------------------------- */
private var _lineHeight: Int =
stateHandle[Constants.LINE_HEIGHT] ?: 4
set(value) {
field = value
stateHandle[Constants.LINE_HEIGHT] = value
}
val lineHeight: StateFlow<Int?>
get() = stateHandle.getStateFlow(Constants.LINE_HEIGHT, null)
/* -- Paragraph Height --------------------------------------------- */
private var _paragraphHeight: Int =
stateHandle[Constants.PARAGRAPH_HEIGHT] ?: 8
set(value) {
field = value
stateHandle[Constants.PARAGRAPH_HEIGHT] = value
}
val paragraphHeight: StateFlow<Int?>
get() = stateHandle.getStateFlow(Constants.PARAGRAPH_HEIGHT, null)
/* -- Paragraph Indentation ---------------------------------------- */
private var _paragraphIndentation: Boolean =
stateHandle[Constants.PARAGRAPH_INDENTATION] ?: false
set(value) {
field = value
stateHandle[Constants.PARAGRAPH_INDENTATION] = value
}
val paragraphIndentation: StateFlow<Boolean?>
get() = stateHandle.getStateFlow(Constants.PARAGRAPH_INDENTATION, null)
/* --------------------------------------------------------------- */
fun onEvent(event: MainEvent) { fun onEvent(event: MainEvent) {
when (event) { when (event) {
is MainEvent.OnChangeLanguage -> { is MainEvent.OnChangeLanguage -> {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
changeLanguage.execute(event.lang) changeLanguage.execute(event.lang)
_language = event.lang _state.updateWithSavedHandle {
it.copy(
language = event.lang
)
}
} }
} }
is MainEvent.OnChangeDarkTheme -> { is MainEvent.OnChangeDarkTheme -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme) setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme)
_darkTheme = event.darkTheme.toDarkTheme() _state.updateWithSavedHandle {
it.copy(
darkTheme = event.darkTheme.toDarkTheme()
)
}
} }
} }
is MainEvent.OnChangeTheme -> { is MainEvent.OnChangeTheme -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME, event.theme) setDatastore.execute(DataStoreConstants.THEME, event.theme)
_theme = event.theme.toTheme() _state.updateWithSavedHandle {
it.copy(
theme = event.theme.toTheme()
)
}
} }
} }
is MainEvent.OnChangeFontFamily -> { is MainEvent.OnChangeFontFamily -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT, event.fontFamily) setDatastore.execute(DataStoreConstants.FONT, event.fontFamily)
_fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id _state.updateWithSavedHandle {
it.copy(
fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id
?: Constants.FONTS[0].id ?: Constants.FONTS[0].id
)
}
} }
} }
is MainEvent.OnChangeFontStyle -> { is MainEvent.OnChangeFontStyle -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle) setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle)
_isItalic = event.fontStyle _state.updateWithSavedHandle {
it.copy(
isItalic = event.fontStyle
)
}
} }
} }
is MainEvent.OnChangeFontSize -> { is MainEvent.OnChangeFontSize -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize) setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize)
_fontSize = event.fontSize _state.updateWithSavedHandle {
it.copy(
fontSize = event.fontSize
)
}
} }
} }
is MainEvent.OnChangeLineHeight -> { is MainEvent.OnChangeLineHeight -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight) setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight)
_lineHeight = event.lineHeight _state.updateWithSavedHandle {
it.copy(
lineHeight = event.lineHeight
)
}
} }
} }
is MainEvent.OnChangeParagraphHeight -> { is MainEvent.OnChangeParagraphHeight -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight) setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight)
_paragraphHeight = event.paragraphHeight _state.updateWithSavedHandle {
it.copy(
paragraphHeight = event.paragraphHeight
)
}
} }
} }
is MainEvent.OnChangeParagraphIndentation -> { is MainEvent.OnChangeParagraphIndentation -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool) setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool)
_paragraphIndentation = event.bool _state.updateWithSavedHandle {
it.copy(
paragraphIndentation = event.bool
)
}
} }
} }
is MainEvent.OnChangeBackgroundColor -> { is MainEvent.OnChangeBackgroundColor -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color) setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color)
_backgroundColor = event.color _state.updateWithSavedHandle {
it.copy(
backgroundColor = event.color
)
}
} }
} }
is MainEvent.OnChangeFontColor -> { is MainEvent.OnChangeFontColor -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color) setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color)
_fontColor = event.color _state.updateWithSavedHandle {
it.copy(
fontColor = event.color
)
}
}
}
is MainEvent.OnChangeShowStartScreen -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool)
_state.updateWithSavedHandle {
it.copy(
showStartScreen = event.bool
)
}
} }
} }
} }
@ -263,20 +193,22 @@ class MainViewModel @Inject constructor(
} }
val isDataReady = combine( val isDataReady = combine(
language, _state
theme, ) {
darkTheme, val value = it.first()
showStartScreen,
backgroundColor, return@combine value.language != null &&
fontColor, value.theme != null &&
fontFamily, value.darkTheme != null &&
isItalic, value.fontFamily != null &&
fontSize, value.isItalic != null &&
lineHeight, value.fontSize != null &&
paragraphHeight, value.lineHeight != null &&
paragraphIndentation value.paragraphHeight != null &&
) { values -> value.paragraphIndentation != null &&
values.all { it != null } value.backgroundColor != null &&
value.fontColor != null &&
value.showStartScreen != null
} }
val isReady = combine( val isReady = combine(
@ -300,7 +232,14 @@ class MainViewModel @Inject constructor(
// Language // Language
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
getDatastore getDatastore
.execute(DataStoreConstants.LANGUAGE, _language) .execute(
DataStoreConstants.LANGUAGE,
if (Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }) {
Locale.getDefault().language.take(2)
} else {
"en"
}
)
.first { .first {
onEvent(MainEvent.OnChangeLanguage(it)) onEvent(MainEvent.OnChangeLanguage(it))
it.isNotBlank() it.isNotBlank()
@ -310,7 +249,11 @@ class MainViewModel @Inject constructor(
// Theme // Theme
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.THEME, _theme.toString()) .execute(
DataStoreConstants.THEME,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.DYNAMIC.name
else Theme.BLUE.name
)
.first { .first {
onEvent(MainEvent.OnChangeTheme(it)) onEvent(MainEvent.OnChangeTheme(it))
it.isNotBlank() it.isNotBlank()
@ -320,7 +263,7 @@ class MainViewModel @Inject constructor(
// Dark Theme // Dark Theme
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.DARK_THEME, _darkTheme.toString()) .execute(DataStoreConstants.DARK_THEME, DarkTheme.FOLLOW_SYSTEM.name)
.first { .first {
onEvent(MainEvent.OnChangeDarkTheme(it)) onEvent(MainEvent.OnChangeDarkTheme(it))
it.isNotBlank() it.isNotBlank()
@ -330,11 +273,9 @@ class MainViewModel @Inject constructor(
// Show Start Screen // Show Start Screen
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.SHOW_START_SCREEN, _showStartScreen) .execute(DataStoreConstants.SHOW_START_SCREEN, true)
.first { .first {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, it) onEvent(MainEvent.OnChangeShowStartScreen(it))
_showStartScreen = it
true true
} }
} }
@ -342,7 +283,7 @@ class MainViewModel @Inject constructor(
// Background Color // Background Color
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.BACKGROUND_COLOR, _backgroundColor) .execute(DataStoreConstants.BACKGROUND_COLOR, Color.DarkGray.value.toLong())
.first { .first {
onEvent(MainEvent.OnChangeBackgroundColor(it)) onEvent(MainEvent.OnChangeBackgroundColor(it))
true true
@ -352,7 +293,7 @@ class MainViewModel @Inject constructor(
// Font Color // Font Color
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.FONT_COLOR, _fontColor) .execute(DataStoreConstants.FONT_COLOR, Color.LightGray.value.toLong())
.first { .first {
onEvent(MainEvent.OnChangeFontColor(it)) onEvent(MainEvent.OnChangeFontColor(it))
true true
@ -362,7 +303,7 @@ class MainViewModel @Inject constructor(
// Font Family // Font Family
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.FONT, _fontFamily) .execute(DataStoreConstants.FONT, Constants.FONTS[0].id)
.first { .first {
onEvent(MainEvent.OnChangeFontFamily(it)) onEvent(MainEvent.OnChangeFontFamily(it))
true true
@ -372,7 +313,7 @@ class MainViewModel @Inject constructor(
// Font Style // Font Style
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.IS_ITALIC, _isItalic) .execute(DataStoreConstants.IS_ITALIC, false)
.first { .first {
onEvent(MainEvent.OnChangeFontStyle(it)) onEvent(MainEvent.OnChangeFontStyle(it))
true true
@ -382,7 +323,7 @@ class MainViewModel @Inject constructor(
// Font Size // Font Size
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.FONT_SIZE, _fontSize) .execute(DataStoreConstants.FONT_SIZE, 16)
.first { .first {
onEvent(MainEvent.OnChangeFontSize(it)) onEvent(MainEvent.OnChangeFontSize(it))
it > 0 it > 0
@ -392,7 +333,7 @@ class MainViewModel @Inject constructor(
// Line Height // Line Height
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.LINE_HEIGHT, _lineHeight) .execute(DataStoreConstants.LINE_HEIGHT, 4)
.first { .first {
onEvent(MainEvent.OnChangeLineHeight(it)) onEvent(MainEvent.OnChangeLineHeight(it))
true true
@ -402,7 +343,7 @@ class MainViewModel @Inject constructor(
// Paragraph Height // Paragraph Height
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.PARAGRAPH_HEIGHT, _paragraphHeight) .execute(DataStoreConstants.PARAGRAPH_HEIGHT, 8)
.first { .first {
onEvent(MainEvent.OnChangeParagraphHeight(it)) onEvent(MainEvent.OnChangeParagraphHeight(it))
true true
@ -412,7 +353,7 @@ class MainViewModel @Inject constructor(
// Paragraph Indentation // Paragraph Indentation
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore
.execute(DataStoreConstants.PARAGRAPH_INDENTATION, _paragraphIndentation) .execute(DataStoreConstants.PARAGRAPH_INDENTATION, false)
.first { .first {
onEvent(MainEvent.OnChangeParagraphIndentation(it)) onEvent(MainEvent.OnChangeParagraphIndentation(it))
true true
@ -421,4 +362,17 @@ class MainViewModel @Inject constructor(
} }
/**
* Updates [MutableStateFlow] along with [SavedStateHandle].
*/
private fun <T> MutableStateFlow<T>.updateWithSavedHandle(
const: String = Constants.MAIN_STATE,
function: (T) -> T
) {
val nextValue = function(value)
update {
stateHandle[const] = nextValue
nextValue
}
}
} }

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -28,7 +29,7 @@ import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
private const val CURRENT_SCREEN = "current_screen" private const val CURRENT_SCREEN = "current_screen"
@ -56,12 +57,12 @@ enum class Screen {
START START
} }
@Immutable
data class Argument( data class Argument(
val key: String, val key: String,
val arg: Any? val arg: Any?
) )
/** /**
* Navigator. Using for navigation between screens. * Navigator. Using for navigation between screens.
* *
@ -181,7 +182,6 @@ class Navigator @AssistedInject constructor(
} }
} }
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(startScreen: Screen): Navigator fun create(startScreen: Screen): Navigator

View file

@ -0,0 +1,258 @@
package ua.acclorite.book_story.presentation.screens.about
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.components.GoBackButton
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.about.components.AboutItem
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(
viewModel: AboutViewModel = hiltViewModel(),
navigator: Navigator
) {
val context = LocalContext.current
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
Scaffold(
Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
LargeTopAppBar(
title = {
Text(stringResource(id = R.string.about_screen))
},
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(16.dp))
}
item {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(id = R.drawable.app_icon_monochrome),
contentDescription = stringResource(id = R.string.app_icon_content_desc),
modifier = Modifier
.size(180.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
item {
Spacer(modifier = Modifier.height(36.dp))
Divider(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.outlineVariant
)
}
item {
AboutItem(
title = stringResource(id = R.string.app_version_option),
description = stringResource(
id = R.string.app_version_option_desc,
stringResource(id = R.string.app_version)
),
isOnClickEnabled = false
)
}
item {
AboutItem(
title = stringResource(id = R.string.all_releases_option),
description = stringResource(
id = R.string.all_releases_option_desc
)
) {
Toast.makeText(
context,
"${context.getString(R.string.app_version_option)}:" +
" ${context.getString(R.string.app_version)}",
Toast.LENGTH_LONG
).show()
viewModel.onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.releases_page),
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
}
}
item {
AboutItem(
title = stringResource(id = R.string.issues_option),
description = stringResource(
id = R.string.issues_option_desc
)
) {
viewModel.onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.issues_page),
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
}
}
item {
AboutItem(
title = stringResource(id = R.string.project_page_option),
description = stringResource(
id = R.string.project_page_option_desc
)
) {
viewModel.onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.project_page),
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
}
}
item {
AboutItem(
title = stringResource(id = R.string.my_page_option),
description = stringResource(
id = R.string.my_page_option_desc
)
) {
viewModel.onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.my_page),
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
}
}
item {
AboutItem(
title = stringResource(id = R.string.support_option),
description = stringResource(
id = R.string.support_option_desc
)
) {
viewModel.onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.support_page),
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
}
}
item {
AboutItem(
title = stringResource(id = R.string.slava_ukraini_option),
description = null
) {
Toast.makeText(
context,
context.getString(R.string.slava_ukraini_option_desc),
Toast.LENGTH_SHORT
).show()
}
}
item { Spacer(modifier = Modifier.height(48.dp)) }
}
}
}

View file

@ -0,0 +1,58 @@
package ua.acclorite.book_story.presentation.screens.about.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* About screen item.
*/
@Composable
fun AboutItem(
modifier: Modifier = Modifier,
title: String,
description: String?,
verticalPadding: Dp = 12.dp,
isOnClickEnabled: Boolean = true,
onClick: () -> Unit = {}
) {
Column(
modifier
.fillMaxWidth()
.clickable(enabled = isOnClickEnabled) {
onClick()
}
.padding(horizontal = 18.dp, vertical = verticalPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Text(
title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
fontSize = 18.sp
)
Spacer(modifier = Modifier.height(2.dp))
description?.let {
Text(
it,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}

View file

@ -0,0 +1,11 @@
package ua.acclorite.book_story.presentation.screens.about.data
import android.content.Context
sealed class AboutEvent {
data class OnNavigateToBrowserPage(
val page: String,
val context: Context,
val noAppsFound: () -> Unit
) : AboutEvent()
}

View file

@ -0,0 +1,43 @@
package ua.acclorite.book_story.presentation.screens.about.data
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AboutViewModel @Inject constructor(
) : ViewModel() {
fun onEvent(event: AboutEvent) {
when (event) {
is AboutEvent.OnNavigateToBrowserPage -> {
viewModelScope.launch {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(event.page)
)
if (intent.resolveActivity(event.context.packageManager) != null) {
event.context.startActivity(intent)
return@launch
}
event.noAppsFound()
}
}
}
}
}

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.presentation.screens.book_info package ua.acclorite.book_story.presentation.screens.book_info
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -55,9 +56,7 @@ import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSnackbar import ua.acclorite.book_story.presentation.components.CustomSnackbar
import ua.acclorite.book_story.presentation.components.GoBackButton 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.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.BookInfoBackground
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection
@ -74,9 +73,9 @@ 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.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent 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.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
@ -99,8 +98,8 @@ fun BookInfoScreen(
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnUpdateBook( BookInfoEvent.OnUpdateBook(
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnLoadList) historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
}, },
snackbarState, snackbarState,
context context
@ -138,6 +137,7 @@ fun BookInfoScreen(
if (state.showMoveDialog) { if (state.showMoveDialog) {
BookInfoMoveDialog( BookInfoMoveDialog(
libraryViewModel = libraryViewModel, libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
viewModel = viewModel, viewModel = viewModel,
navigator = navigator navigator = navigator
) )
@ -158,7 +158,7 @@ fun BookInfoScreen(
isTopBarScrolled = null, isTopBarScrolled = null,
content1NavigationIcon = { content1NavigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator, enabled = !state.isRefreshing)
}, },
content1Title = { content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) { DefaultTransition(visible = firstVisibleItemIndex > 0) {
@ -181,8 +181,8 @@ fun BookInfoScreen(
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnUpdateBook( BookInfoEvent.OnUpdateBook(
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnLoadList) historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
}, },
snackbarState, snackbarState,
context context
@ -218,9 +218,8 @@ fun BookInfoScreen(
) { ) {
viewModel.onEvent(BookInfoEvent.OnUpdateTitle( viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
refreshList = { refreshList = {
libraryViewModel.onEvent( libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
LibraryEvent.OnLoadList historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
)
} }
)) ))
Toast.makeText( Toast.makeText(
@ -282,11 +281,9 @@ fun BookInfoScreen(
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
navigator.navigate( if (!state.isRefreshing) {
Screen.READER, false, Argument( viewModel.onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator))
"book", state.book.id }
)
)
}, },
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
modifier = Modifier modifier = Modifier
@ -332,4 +329,10 @@ fun BookInfoScreen(
) )
} }
} }
BackHandler {
if (!state.isRefreshing) {
navigator.navigateBack()
}
}
} }

View file

@ -45,7 +45,7 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent 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.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Info section. * Info section.
@ -65,7 +65,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
modifier = Modifier modifier = Modifier
.height(195.dp) .height(195.dp)
.width(130.dp) .width(130.dp)
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation()) .background(MaterialTheme.elevation())
.combinedClickable( .combinedClickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
@ -82,7 +82,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
contentDescription = stringResource(id = R.string.cover_image_content_desc), contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.large), .clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} else { } else {

View file

@ -33,13 +33,11 @@ fun BookInfoStatisticSection(book: Book) {
.removeTrailingZero() .removeTrailingZero()
}%" }%"
} }
val description = if (book.progress < 1f) stringResource( val description = stringResource(
id = R.string.you_read_query, if (book.progress == 1f) R.string.read_done
progress else if (book.progress > 0.2f) R.string.read_keep
) + " " + stringResource(
if (book.progress > 0.2f) R.string.read_keep
else R.string.read_more else R.string.read_more
) else stringResource(id = R.string.read_done) )
Column( Column(
Modifier Modifier

View file

@ -31,8 +31,8 @@ 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.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent 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.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one. * Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
@ -58,8 +58,8 @@ fun BookInfoChangeCoverBottomSheet(
uri, uri,
context, context,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnLoadList) historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
} }
) )
) )
@ -98,12 +98,14 @@ fun BookInfoChangeCoverBottomSheet(
text = stringResource(id = R.string.delete_cover), text = stringResource(id = R.string.delete_cover),
description = stringResource(id = R.string.delete_cover_desc) description = stringResource(id = R.string.delete_cover_desc)
) { ) {
viewModel.onEvent(BookInfoEvent.OnDeleteCover( viewModel.onEvent(
BookInfoEvent.OnDeleteCover(
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnLoadList) historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
} }
)) )
)
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.cover_image_deleted), context.getString(R.string.cover_image_deleted),

View file

@ -22,8 +22,8 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent 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.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale

View file

@ -18,6 +18,8 @@ import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableD
import ua.acclorite.book_story.presentation.data.Navigator 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.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
@ -27,6 +29,7 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
@Composable @Composable
fun BookInfoMoveDialog( fun BookInfoMoveDialog(
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
viewModel: BookInfoViewModel, viewModel: BookInfoViewModel,
navigator: Navigator navigator: Navigator
) { ) {
@ -54,9 +57,8 @@ fun BookInfoMoveDialog(
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnMoveBook( BookInfoEvent.OnMoveBook(
refreshList = { refreshList = {
libraryViewModel.onEvent( libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
LibraryEvent.OnLoadList historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
)
}, },
updatePage = { updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it)) libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))

View file

@ -4,26 +4,27 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
sealed class BookInfoEvent { sealed class BookInfoEvent {
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent() data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
data object OnShowHideDetailsBottomSheet : BookInfoEvent() data object OnShowHideDetailsBottomSheet : BookInfoEvent()
data class OnDeleteCover(val refreshList: () -> Unit) : BookInfoEvent() data class OnDeleteCover(val refreshList: (Book) -> Unit) : BookInfoEvent()
data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: () -> Unit) : data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: (Book) -> Unit) :
BookInfoEvent() BookInfoEvent()
data object OnShowHideEditTitle : BookInfoEvent() data object OnShowHideEditTitle : BookInfoEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
data class OnTitleValueChange(val value: String) : BookInfoEvent() data class OnTitleValueChange(val value: String) : BookInfoEvent()
data class OnUpdateTitle(val refreshList: () -> Unit) : BookInfoEvent() data class OnUpdateTitle(val refreshList: (Book) -> Unit) : BookInfoEvent()
data object OnShowHideMoreDropDown : BookInfoEvent() data object OnShowHideMoreDropDown : BookInfoEvent()
data object OnShowHideDeleteDialog : BookInfoEvent() data object OnShowHideDeleteDialog : BookInfoEvent()
data object OnShowHideMoveDialog : BookInfoEvent() data object OnShowHideMoveDialog : BookInfoEvent()
data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent() data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent()
data class OnMoveBook( data class OnMoveBook(
val refreshList: () -> Unit, val refreshList: (Book) -> Unit,
val updatePage: (Int) -> Unit, val updatePage: (Int) -> Unit,
val navigator: Navigator val navigator: Navigator
) : BookInfoEvent() ) : BookInfoEvent()
@ -44,8 +45,12 @@ sealed class BookInfoEvent {
) : BookInfoEvent() ) : BookInfoEvent()
data class OnUpdateBook( data class OnUpdateBook(
val refreshList: () -> Unit, val refreshList: (Book) -> Unit,
val snackbarState: SnackbarHostState, val snackbarState: SnackbarHostState,
val context: Context val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data class OnNavigateToReaderScreen(
val navigator: Navigator
) : BookInfoEvent()
} }

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Constants import ua.acclorite.book_story.domain.util.Constants
@Immutable @Immutable
data class BookInfoState( data class BookInfoState(

View file

@ -17,16 +17,21 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.R 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.Category
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook 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.DeleteBooks
import ua.acclorite.book_story.domain.use_case.GetBookFromFile import ua.acclorite.book_story.domain.use_case.GetBookFromFile
import ua.acclorite.book_story.domain.use_case.GetBooksById import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook
import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@ -35,6 +40,7 @@ class BookInfoViewModel @Inject constructor(
private val updateBooks: UpdateBooks, private val updateBooks: UpdateBooks,
private val updateBooksWithText: UpdateBooksWithText, private val updateBooksWithText: UpdateBooksWithText,
private val updateCoverImageOfBook: UpdateCoverImageOfBook, private val updateCoverImageOfBook: UpdateCoverImageOfBook,
private val insertHistory: InsertHistory,
private val deleteBooks: DeleteBooks, private val deleteBooks: DeleteBooks,
private val getBookFromFile: GetBookFromFile, private val getBookFromFile: GetBookFromFile,
private val getBookById: GetBooksById private val getBookById: GetBooksById
@ -65,7 +71,7 @@ class BookInfoViewModel @Inject constructor(
_state.value.book, _state.value.book,
image image
) )
event.refreshList()
val newCoverImage = getBookById.execute( val newCoverImage = getBookById.execute(
listOf( listOf(
_state.value.book.id ?: return@launch _state.value.book.id ?: return@launch
@ -80,6 +86,7 @@ class BookInfoViewModel @Inject constructor(
showChangeCoverBottomSheet = false showChangeCoverBottomSheet = false
) )
} }
event.refreshList(_state.value.book)
} }
} }
@ -93,8 +100,6 @@ class BookInfoViewModel @Inject constructor(
bookWithOldCover = _state.value.book, bookWithOldCover = _state.value.book,
newCoverImage = null newCoverImage = null
) )
event.refreshList()
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -103,6 +108,7 @@ class BookInfoViewModel @Inject constructor(
showChangeCoverBottomSheet = false showChangeCoverBottomSheet = false
) )
} }
event.refreshList(_state.value.book)
} }
} }
@ -155,8 +161,6 @@ class BookInfoViewModel @Inject constructor(
_state.value.book.copy(title = title) _state.value.book.copy(title = title)
) )
) )
event.refreshList()
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -164,6 +168,8 @@ class BookInfoViewModel @Inject constructor(
) )
) )
} }
event.refreshList(_state.value.book)
onEvent(BookInfoEvent.OnShowHideEditTitle) onEvent(BookInfoEvent.OnShowHideEditTitle)
} }
} }
@ -223,13 +229,16 @@ class BookInfoViewModel @Inject constructor(
) )
} }
val book = _state.value.book.copy( _state.update {
category = _state.value.selectedCategory it.copy(
book = it.book.copy(
category = it.selectedCategory
) )
updateBooks.execute(listOf(book)) )
}
event.refreshList() updateBooks.execute(listOf(_state.value.book))
event.refreshList(_state.value.book)
event.updatePage( event.updatePage(
Category.entries.dropLastWhile { Category.entries.dropLastWhile {
it != _state.value.selectedCategory it != _state.value.selectedCategory
@ -375,7 +384,7 @@ class BookInfoViewModel @Inject constructor(
) )
) )
) )
event.refreshList() event.refreshList(_state.value.book)
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
@ -394,20 +403,42 @@ class BookInfoViewModel @Inject constructor(
} }
} }
} }
is BookInfoEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch {
_state.value.book.id?.let {
insertHistory.execute(
listOf(
History(
null,
it,
null,
Date().time
)
)
)
}
event.navigator.navigate(
Screen.READER,
false,
Argument(
"book", _state.value.book
)
)
}
}
} }
} }
fun init(navigator: Navigator) { fun init(navigator: Navigator) {
viewModelScope.launch { viewModelScope.launch {
val bookIndex = navigator.retrieveArgument("book") as? Int val book = navigator.retrieveArgument("book") as? Book
if (bookIndex == null || bookIndex < 0) { if (book == null) {
navigator.navigateBack() navigator.navigateBack()
return@launch return@launch
} }
val book = getBookById.execute(listOf(bookIndex)).first()
_state.update { _state.update {
BookInfoState(book = book) BookInfoState(book = book)
} }

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.presentation.screens.browse package ua.acclorite.book_story.presentation.screens.browse
import android.Manifest import android.Manifest
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@ -41,6 +42,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -63,9 +65,9 @@ import ua.acclorite.book_story.presentation.screens.browse.components.adding_dia
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent 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.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@ -79,6 +81,7 @@ fun BrowseScreen(
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel,
navigator: Navigator navigator: Navigator
) { ) {
val context = LocalContext.current
val permissionState = rememberPermissionState( val permissionState = rememberPermissionState(
permission = Manifest.permission.READ_EXTERNAL_STORAGE permission = Manifest.permission.READ_EXTERNAL_STORAGE
) )
@ -130,20 +133,13 @@ fun BrowseScreen(
content1Visibility = !state.hasSelectedItems && !state.showSearch, content1Visibility = !state.hasSelectedItems && !state.showSearch,
content1NavigationIcon = {}, content1NavigationIcon = {},
content1Title = { content1Title = {
Text( Text(stringResource(id = R.string.browse_screen))
stringResource(id = R.string.browse_screen),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}, },
content1Actions = { content1Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc), contentDescription = stringResource(id = R.string.search_content_desc),
disableOnClick = false, disableOnClick = true
enabled = !state.showSearch
) { ) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide) viewModel.onEvent(BrowseEvent.OnSearchShowHide)
} }
@ -167,8 +163,6 @@ fun BrowseScreen(
id = R.string.selected_items_count_query, id = R.string.selected_items_count_query,
state.selectedItemsCount.coerceAtLeast(1) state.selectedItemsCount.coerceAtLeast(1)
), ),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
) )
@ -265,6 +259,16 @@ fun BrowseScreen(
modifier = Modifier modifier = Modifier
.animateItemPlacement(), .animateItemPlacement(),
hasSelectedFiles = state.selectableFiles.any { it.second }, hasSelectedFiles = state.selectableFiles.any { it.second },
onLongClick = {
Toast.makeText(
context,
context.getString(
R.string.file_path_query,
selectableFile.first.path
),
Toast.LENGTH_LONG
).show()
},
onClick = { onClick = {
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
} }

View file

@ -2,9 +2,10 @@ package ua.acclorite.book_story.presentation.screens.browse.components
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -33,9 +34,9 @@ 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 import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.CustomCheckbox import ua.acclorite.book_story.presentation.components.CustomCheckbox
import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.util.Selected
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -44,11 +45,14 @@ import java.util.Locale
/** /**
* Browse list element item. Can be selected. * Browse list element item. Can be selected.
*/ */
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun BrowseFileItem( fun BrowseFileItem(
file: Pair<File, Selected>, file: Pair<File, Selected>,
hasSelectedFiles: Boolean, hasSelectedFiles: Boolean,
modifier: Modifier, onClick: () -> Unit modifier: Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit
) { ) {
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) } val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
val lastModified = remember { pattern.format(Date(file.first.lastModified())) } val lastModified = remember { pattern.format(Date(file.first.lastModified())) }
@ -85,9 +89,14 @@ fun BrowseFileItem(
.padding(horizontal = 8.dp, vertical = 3.dp) .padding(horizontal = 8.dp, vertical = 3.dp)
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(animatedBackgroundColor) .background(animatedBackgroundColor)
.clickable { .combinedClickable(
onLongClick = {
onLongClick()
},
onClick = {
onClick() onClick()
} }
)
.padding(horizontal = 8.dp, vertical = 7.dp) .padding(horizontal = 8.dp, vertical = 7.dp)
) { ) {
Row( Row(

View file

@ -54,6 +54,7 @@ fun BrowseAddingDialog(
navigator, navigator,
resetScroll = { resetScroll = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0)) libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0))
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
} }
) )
) )
@ -83,7 +84,9 @@ fun BrowseAddingDialog(
} }
} else { } else {
items(state.selectedBooks) { book -> items(state.selectedBooks) { book ->
BrowseAddingDialogItem(result = book) { BrowseAddingDialogItem(
result = book
) {
if (it) { if (it) {
viewModel.onEvent(BrowseEvent.OnSelectBook(book)) viewModel.onEvent(BrowseEvent.OnSelectBook(book))
} else { } else {

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -23,8 +22,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.CustomCheckbox import ua.acclorite.book_story.presentation.components.CustomCheckbox
import ua.acclorite.book_story.util.Selected
/** /**
* Adding dialog item. * Adding dialog item.
@ -64,13 +63,6 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
} }
} }
} else { } else {
val icon =
if (result.first.message?.asString() == stringResource(R.string.error_file_encrypted)) {
Icons.Default.Lock
} else {
Icons.Default.Error
}
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -81,7 +73,7 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = icon, imageVector = Icons.Default.Error,
contentDescription = stringResource(id = R.string.error_content_desc), contentDescription = stringResource(id = R.string.error_content_desc),
modifier = Modifier.size(26.dp), modifier = Modifier.size(26.dp),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error

View file

@ -7,8 +7,8 @@ import androidx.compose.ui.focus.FocusRequester
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.util.Selected
import java.io.File import java.io.File
sealed class BrowseEvent { sealed class BrowseEvent {

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.presentation.screens.browse.data
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.util.Selected import ua.acclorite.book_story.domain.util.Selected
import java.io.File import java.io.File
@Immutable @Immutable
@ -18,9 +18,6 @@ data class BrowseState(
val selectedItemsCount: Int = 0, val selectedItemsCount: Int = 0,
val hasSelectedItems: Boolean = false, val hasSelectedItems: Boolean = false,
val scrollIndex: Int = 0,
val scrollOffset: Int = 0,
val showSearch: Boolean = false, val showSearch: Boolean = false,
val searchQuery: String = "", val searchQuery: String = "",
val hasFocused: Boolean = false, val hasFocused: Boolean = false,

View file

@ -23,9 +23,9 @@ 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.GetBooksFromFiles
import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice
import ua.acclorite.book_story.domain.use_case.InsertBooks import ua.acclorite.book_story.domain.use_case.InsertBooks
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.util.Resource
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@ -41,6 +41,7 @@ class BrowseViewModel @Inject constructor(
val state = _state.asStateFlow() val state = _state.asStateFlow()
private var job: Job? = null private var job: Job? = null
private var job2: Job? = null
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -127,7 +128,8 @@ class BrowseViewModel @Inject constructor(
} }
is BrowseEvent.OnRefreshList -> { is BrowseEvent.OnRefreshList -> {
viewModelScope.launch(Dispatchers.IO) { job2?.cancel()
job2 = viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = true, isRefreshing = true,
@ -169,20 +171,19 @@ class BrowseViewModel @Inject constructor(
is BrowseEvent.OnSelectFile -> { is BrowseEvent.OnSelectFile -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val indexOfFile = _state.value.selectableFiles.indexOf(event.file) val editedList = _state.value.selectableFiles.map {
if (event.file.first.path == it.first.path) {
if (indexOfFile == -1) { it.copy(
return@launch second = !it.second
}
val editedList = _state.value.selectableFiles.toMutableList()
editedList[indexOfFile] = editedList[indexOfFile].copy(
second = !editedList[indexOfFile].second
) )
} else {
it
}
}
_state.update { _state.update {
it.copy( it.copy(
selectableFiles = editedList.toList(), selectableFiles = editedList,
selectedItemsCount = editedList.filter { file -> file.second }.size, selectedItemsCount = editedList.filter { file -> file.second }.size,
hasSelectedItems = editedList.any { file -> file.second } hasSelectedItems = editedList.any { file -> file.second }
) )
@ -267,7 +268,7 @@ class BrowseViewModel @Inject constructor(
) )
} }
job?.cancel() job?.cancel()
job = viewModelScope.launch { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
getFilesFromDownloads() getFilesFromDownloads()
} }
@ -352,6 +353,7 @@ class BrowseViewModel @Inject constructor(
_state.update { _state.update {
it.copy( it.copy(
isLoading = true, isLoading = true,
hasSelectedItems = false,
listState = LazyListState(0, 0) listState = LazyListState(0, 0)
) )
} }

View file

@ -62,9 +62,9 @@ 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.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent 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.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import java.util.UUID import java.util.UUID
@OptIn( @OptIn(
@ -79,7 +79,6 @@ fun HistoryScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val books = libraryViewModel.state.collectAsState().value.books.map { it.first }
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = { onRefresh = {
@ -108,19 +107,14 @@ fun HistoryScreen(
content1NavigationIcon = {}, content1NavigationIcon = {},
content1Title = { content1Title = {
Text( Text(
stringResource(id = R.string.history_screen), stringResource(id = R.string.history_screen)
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
) )
}, },
content1Actions = { content1Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc), contentDescription = stringResource(id = R.string.search_content_desc),
disableOnClick = false, disableOnClick = true,
enabled = !state.showSearch
) { ) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide) viewModel.onEvent(HistoryEvent.OnSearchShowHide)
} }
@ -133,6 +127,7 @@ fun HistoryScreen(
enabled = !state.isLoading enabled = !state.isLoading
&& !state.isRefreshing && !state.isRefreshing
&& state.history.isNotEmpty() && state.history.isNotEmpty()
&& !state.showDeleteWholeHistoryDialog
) { ) {
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
} }
@ -214,7 +209,6 @@ fun HistoryScreen(
state = state.listState, state = state.listState,
contentPadding = PaddingValues(vertical = 12.dp) contentPadding = PaddingValues(vertical = 12.dp)
) { ) {
if (!state.isLoading) {
state.history.forEachIndexed { index, groupedHistory -> state.history.forEachIndexed { index, groupedHistory ->
item(key = groupedHistory.title) { item(key = groupedHistory.title) {
if (index > 0) { if (index > 0) {
@ -230,32 +224,30 @@ fun HistoryScreen(
}, },
padding = 16.dp padding = 16.dp
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
items( items(
groupedHistory.history, key = { it.id ?: UUID.randomUUID() } groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
) { ) {
val book = remember {
books.find { book -> book.id == it.bookId }
} ?: return@items
HistoryItem( HistoryItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
history = it, history = it,
book = book, isOnClickEnabled = !state.isRefreshing,
onBodyClick = { onBodyClick = {
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
false, false,
Argument("book", book.id) Argument("book", it.book)
) )
}, },
onTitleClick = { onTitleClick = {
navigator.navigate( viewModel.onEvent(
Screen.READER, HistoryEvent.OnNavigateToReaderScreen(
false, navigator,
Argument("book", book.id) it.book
)
) )
}, },
isDeleteEnabled = !state.isRefreshing, isDeleteEnabled = !state.isRefreshing,
@ -266,7 +258,9 @@ fun HistoryScreen(
snackbarState = snackbarState, snackbarState = snackbarState,
context = context, context = context,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
} }
) )
) )
@ -276,10 +270,11 @@ fun HistoryScreen(
} }
} }
} }
}
AnimatedVisibility( AnimatedVisibility(
visible = !state.isLoading && state.history.isEmpty() && !state.isRefreshing, visible = !state.isLoading
&& state.history.isEmpty()
&& !state.isRefreshing,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(0)) exit = fadeOut(tween(0))

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
@ -30,11 +31,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow 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
import coil.compose.AsyncImage import coil.compose.AsyncImage
import ua.acclorite.book_story.R 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.domain.model.History
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -46,7 +47,7 @@ import java.util.Locale
fun HistoryItem( fun HistoryItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
history: History, history: History,
book: Book, isOnClickEnabled: Boolean,
onBodyClick: () -> Unit, onBodyClick: () -> Unit,
onTitleClick: () -> Unit, onTitleClick: () -> Unit,
isDeleteEnabled: Boolean, isDeleteEnabled: Boolean,
@ -61,7 +62,7 @@ fun HistoryItem(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable(enabled = isOnClickEnabled) {
onBodyClick() onBodyClick()
} }
.padding(top = 6.dp, bottom = 6.dp, start = 16.dp, end = 10.dp) .padding(top = 6.dp, bottom = 6.dp, start = 16.dp, end = 10.dp)
@ -74,16 +75,16 @@ fun HistoryItem(
modifier = Modifier modifier = Modifier
.height(90.dp) .height(90.dp)
.width(60.dp) .width(60.dp)
.clip(MaterialTheme.shapes.medium) .clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.elevation()) .background(MaterialTheme.elevation())
) { ) {
if (book.coverImage != null) { if (history.book?.coverImage != null) {
AsyncImage( AsyncImage(
model = book.coverImage, model = history.book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc), contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.medium), .clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} else { } else {
@ -108,14 +109,16 @@ fun HistoryItem(
modifier = Modifier.fillMaxHeight() modifier = Modifier.fillMaxHeight()
) { ) {
Text( Text(
book.title, history.book?.title ?: return,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
lineHeight = 21.sp,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable( .clickable(
interactionSource = null, interactionSource = null,
indication = null, indication = null,
enabled = isOnClickEnabled,
onClick = onTitleClick onClick = onTitleClick
), ),
maxLines = 2, maxLines = 2,
@ -135,7 +138,7 @@ fun HistoryItem(
} }
Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) { Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) {
IconButton( IconButton(
enabled = isDeleteEnabled, enabled = isDeleteEnabled && isOnClickEnabled,
onClick = { onClick = {
onDeleteClick() onDeleteClick()
} }

View file

@ -3,7 +3,9 @@ package ua.acclorite.book_story.presentation.screens.history.data
import android.content.Context import android.content.Context
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.presentation.data.Navigator
sealed class HistoryEvent { sealed class HistoryEvent {
data object OnRefreshList : HistoryEvent() data object OnRefreshList : HistoryEvent()
@ -20,4 +22,9 @@ sealed class HistoryEvent {
data class OnSearchQueryChange(val query: String) : HistoryEvent() data class OnSearchQueryChange(val query: String) : HistoryEvent()
data object OnSearchShowHide : HistoryEvent() data object OnSearchShowHide : HistoryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent()
data class OnUpdateBook(val book: Book) : HistoryEvent()
data class OnNavigateToReaderScreen(
val navigator: Navigator,
val book: Book?
) : HistoryEvent()
} }

View file

@ -15,8 +15,4 @@ data class HistoryState(
val showSearch: Boolean = false, val showSearch: Boolean = false,
val searchQuery: String = "", val searchQuery: String = "",
val hasFocused: Boolean = false, val hasFocused: Boolean = false,
val scrollIndex: Int = 0,
val scrollOffset: Int = 0,
) )

View file

@ -20,9 +20,12 @@ 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.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetHistory import ua.acclorite.book_story.domain.use_case.GetHistory
import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Screen
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -40,6 +43,7 @@ class HistoryViewModel @Inject constructor(
private var job: Job? = null private var job: Job? = null
private var job2: Job? = null private var job2: Job? = null
private var job3: Job? = null
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -55,7 +59,8 @@ class HistoryViewModel @Inject constructor(
fun onEvent(event: HistoryEvent) { fun onEvent(event: HistoryEvent) {
when (event) { when (event) {
is HistoryEvent.OnRefreshList -> { is HistoryEvent.OnRefreshList -> {
viewModelScope.launch(Dispatchers.IO) { job3?.cancel()
job3 = viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = true, isRefreshing = true,
@ -115,6 +120,7 @@ class HistoryViewModel @Inject constructor(
deleteHistory.execute( deleteHistory.execute(
listOf(event.historyToDelete) listOf(event.historyToDelete)
) )
onEvent(HistoryEvent.OnRefreshList) onEvent(HistoryEvent.OnRefreshList)
event.refreshList() event.refreshList()
@ -172,7 +178,7 @@ class HistoryViewModel @Inject constructor(
) )
} }
job?.cancel() job?.cancel()
job = viewModelScope.launch { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
getHistoryFromDatabase() getHistoryFromDatabase()
} }
@ -188,6 +194,54 @@ class HistoryViewModel @Inject constructor(
} }
} }
} }
is HistoryEvent.OnUpdateBook -> {
viewModelScope.launch(Dispatchers.IO) {
val history = _state.value.history.map {
it.copy(
history = it.history.map { history ->
if (history.bookId == event.book.id) {
history.copy(
book = event.book
)
} else {
history
}
}
)
}
_state.update {
it.copy(
history = history
)
}
}
}
is HistoryEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch {
event.book?.id?.let {
insertHistory.execute(
listOf(
History(
null,
it,
null,
Date().time
)
)
)
}
event.navigator.navigate(
Screen.READER,
false,
Argument(
"book", event.book
)
)
}
}
} }
} }
@ -210,9 +264,10 @@ class HistoryViewModel @Inject constructor(
getHistory.execute().collect { result -> getHistory.execute().collect { result ->
when (result) { when (result) {
is Resource.Success -> { is Resource.Success -> {
val history = result.data?.sortedByDescending { it.time } ?: emptyList() val historyWithoutBook =
result.data?.sortedByDescending { it.time } ?: emptyList()
if (history.isEmpty()) { if (historyWithoutBook.isEmpty()) {
_state.update { _state.update {
it.copy( it.copy(
history = emptyList(), history = emptyList(),
@ -223,8 +278,26 @@ class HistoryViewModel @Inject constructor(
} }
val books = getBooksById.execute( val books = getBooksById.execute(
history.map { it.bookId }.distinct() historyWithoutBook.map { it.bookId }.distinct()
) )
if (books.isEmpty()) {
_state.update {
it.copy(
history = emptyList(),
isLoading = false
)
}
return@collect
}
val history = historyWithoutBook.map {
val book = books.find { book -> book.id == it.bookId }!!
it.copy(
book = book
)
}
val groupedHistory = mutableListOf<GroupedHistory>() val groupedHistory = mutableListOf<GroupedHistory>()
history history
@ -278,20 +351,3 @@ class HistoryViewModel @Inject constructor(
} }
} }

View file

@ -68,6 +68,7 @@ import kotlinx.coroutines.launch
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.MoreDropDown
@ -83,10 +84,9 @@ import ua.acclorite.book_story.presentation.screens.library.components.dialog.Li
import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog 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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import ua.acclorite.book_story.util.Selected
import java.util.UUID import java.util.UUID
@OptIn( @OptIn(
@ -98,8 +98,7 @@ fun LibraryScreen(
viewModel: LibraryViewModel, viewModel: LibraryViewModel,
browseViewModel: BrowseViewModel, browseViewModel: BrowseViewModel,
historyViewModel: HistoryViewModel, historyViewModel: HistoryViewModel,
navigator: Navigator, navigator: Navigator
addedBooks: List<Book>
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
@ -114,21 +113,21 @@ fun LibraryScreen(
) )
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if (addedBooks.isNotEmpty()) {
viewModel.onEvent(LibraryEvent.OnPreloadBooks(addedBooks))
navigator.clearArgument("added_books")
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.onEvent(LibraryEvent.OnScrollToPage(state.currentPage, pagerState)) viewModel.onEvent(LibraryEvent.OnScrollToPage(state.currentPage, pagerState))
} }
LaunchedEffect(pagerState.currentPage) { LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != state.currentPage) {
viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage)) viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
} }
}
if (state.showMoveDialog) { if (state.showMoveDialog) {
LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState) LibraryMoveDialog(
viewModel = viewModel,
historyViewModel = historyViewModel,
pagerState = pagerState
)
} }
if (state.showDeleteDialog) { if (state.showDeleteDialog) {
LibraryDeleteDialog( LibraryDeleteDialog(
@ -156,13 +155,7 @@ fun LibraryScreen(
content1NavigationIcon = {}, content1NavigationIcon = {},
content1Title = { content1Title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(stringResource(id = R.string.library_screen))
stringResource(id = R.string.library_screen),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = state.books.size.toString(), text = state.books.size.toString(),
@ -179,8 +172,7 @@ fun LibraryScreen(
CustomIconButton( CustomIconButton(
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc), contentDescription = stringResource(id = R.string.search_content_desc),
disableOnClick = false, disableOnClick = true,
enabled = !state.showSearch
) { ) {
viewModel.onEvent(LibraryEvent.OnSearchShowHide) viewModel.onEvent(LibraryEvent.OnSearchShowHide)
} }
@ -204,8 +196,6 @@ fun LibraryScreen(
id = R.string.selected_items_count_query, id = R.string.selected_items_count_query,
state.selectedItemsCount.coerceAtLeast(1) state.selectedItemsCount.coerceAtLeast(1)
), ),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
) )
@ -216,7 +206,11 @@ fun LibraryScreen(
contentDescription = stringResource( contentDescription = stringResource(
id = R.string.move_books_content_desc id = R.string.move_books_content_desc
), ),
disableOnClick = true enabled = !state.isLoading
&& !state.isRefreshing
&& !state.showMoveDialog,
disableOnClick = false,
) { ) {
viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog)
} }
@ -225,7 +219,10 @@ fun LibraryScreen(
contentDescription = stringResource( contentDescription = stringResource(
id = R.string.delete_books_content_desc id = R.string.delete_books_content_desc
), ),
disableOnClick = true enabled = !state.isLoading
&& !state.isRefreshing
&& !state.showDeleteDialog,
disableOnClick = false
) { ) {
viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog)
} }
@ -310,7 +307,9 @@ fun LibraryScreen(
LaunchedEffect(state.books) { LaunchedEffect(state.books) {
categorizedBooks.clear() categorizedBooks.clear()
categorizedBooks.addAll(state.books.filter { it.first.category == category } categorizedBooks.addAll(
state.books
.filter { it.first.category == category }
.sortedWith( .sortedWith(
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened } compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
.thenBy { it.first.title } .thenBy { it.first.title }
@ -323,8 +322,7 @@ fun LibraryScreen(
DefaultTransition(visible = !state.isLoading && !categoryIsLoading) { DefaultTransition(visible = !state.isLoading && !categoryIsLoading) {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(120.dp), columns = GridCells.Adaptive(120.dp),
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(),
contentPadding = PaddingValues(8.dp) contentPadding = PaddingValues(8.dp)
) { ) {
items( items(
@ -341,7 +339,7 @@ fun LibraryScreen(
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
false, false,
Argument("book", it.first.id) Argument("book", it.first)
) )
} }
}, },
@ -351,10 +349,11 @@ fun LibraryScreen(
} }
}, },
onButtonClick = { onButtonClick = {
navigator.navigate( viewModel.onEvent(
Screen.READER, LibraryEvent.OnNavigateToReaderScreen(
false, navigator,
Argument("book", it.first.id) it.first
)
) )
} }
) )
@ -363,7 +362,9 @@ fun LibraryScreen(
} }
AnimatedVisibility( AnimatedVisibility(
visible = !state.isLoading && !state.isRefreshing && categorizedBooks.isEmpty() visible = !state.isLoading
&& !state.isRefreshing
&& categorizedBooks.isEmpty()
&& !categoryIsLoading, && !categoryIsLoading,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,

View file

@ -37,10 +37,10 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import ua.acclorite.book_story.util.Selected
/** /**
* Library list element item. * Library list element item.
@ -93,7 +93,7 @@ fun LibraryBookItem(
modifier = Modifier modifier = Modifier
.aspectRatio(1f / 1.5f) .aspectRatio(1f / 1.5f)
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation()) .background(MaterialTheme.elevation())
.combinedClickable( .combinedClickable(
onClick = { onClick = {
@ -110,7 +110,7 @@ fun LibraryBookItem(
contentDescription = stringResource(id = R.string.cover_image_content_desc), contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.large), .clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} else { } else {

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.library.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -13,7 +14,6 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -22,7 +22,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -30,7 +30,7 @@ import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent 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.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Tab row, either scrollable or static, depends on screen width. * Tab row, either scrollable or static, depends on screen width.
@ -60,83 +60,34 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: Pa
) )
} }
if (LocalConfiguration.current.screenWidthDp > 450) { Box(Modifier.fillMaxWidth()) {
TabRow( Divider(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(), .fillMaxWidth(),
selectedTabIndex = pagerState.currentPage, color = MaterialTheme.colorScheme.surfaceVariant
containerColor = MaterialTheme.colorScheme.surface,
divider = {
Divider(color = MaterialTheme.colorScheme.surfaceVariant)
},
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
.padding(horizontal = 16.dp)
.clip(
RoundedCornerShape(
topStartPercent = 100,
topEndPercent = 100
) )
),
color = MaterialTheme.colorScheme.primary
)
}
) {
tabItems.forEachIndexed { index, tabItem ->
Tab(
modifier = Modifier,
selected = pagerState.currentPage == index,
onClick = {
viewModel.onEvent(LibraryEvent.OnScrollToPage(index, pagerState))
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
tabItem.first,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = tabItem.second.toString(),
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation(6.dp))
.padding(horizontal = 8.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
}
}
)
}
}
} else {
ScrollableTabRow( ScrollableTabRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.padding(bottom = 0.5.dp),
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surface, containerColor = Color.Transparent,
divider = {
Divider(color = MaterialTheme.colorScheme.surfaceVariant)
},
edgePadding = 0.dp, edgePadding = 0.dp,
divider = {},
indicator = { tabPositions -> indicator = { tabPositions ->
TabRowDefaults.Indicator( TabRowDefaults.Indicator(
Modifier Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage]) .tabIndicatorOffset(tabPositions[pagerState.currentPage])
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(end = 5.5.dp)
.clip( .clip(
RoundedCornerShape( RoundedCornerShape(
topStartPercent = 100, 100
topEndPercent = 100
) )
), ),
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary,
height = 3.dp
) )
} }
) { ) {

View file

@ -15,6 +15,8 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn 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.components.custom_dialog.SelectableDialogItem
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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
@ -23,7 +25,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) { fun LibraryMoveDialog(
viewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
pagerState: PagerState
) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@ -38,7 +44,14 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
isActionEnabled = true, isActionEnabled = true,
onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) }, onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) },
onAction = { onAction = {
viewModel.onEvent(LibraryEvent.OnMoveBooks(pagerState)) viewModel.onEvent(
LibraryEvent.OnMoveBooks(
pagerState,
refreshList = {
historyViewModel.onEvent(HistoryEvent.OnLoadList)
}
)
)
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.books_moved), context.getString(R.string.books_moved),

View file

@ -7,10 +7,10 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Selected import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.Navigator
sealed class LibraryEvent { sealed class LibraryEvent {
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
data object OnRefreshList : LibraryEvent() data object OnRefreshList : LibraryEvent()
data object OnLoadList : LibraryEvent() data object OnLoadList : LibraryEvent()
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent() data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
@ -24,8 +24,12 @@ sealed class LibraryEvent {
data object OnClearSelectedBooks : LibraryEvent() data object OnClearSelectedBooks : LibraryEvent()
data object OnShowHideMoveDialog : LibraryEvent() data object OnShowHideMoveDialog : LibraryEvent()
data class OnSelectCategory(val category: Category) : LibraryEvent() data class OnSelectCategory(val category: Category) : LibraryEvent()
data class OnMoveBooks(val pagerState: PagerState) : LibraryEvent() data class OnMoveBooks(val pagerState: PagerState, val refreshList: () -> Unit) : LibraryEvent()
data object OnShowHideDeleteDialog : LibraryEvent() data object OnShowHideDeleteDialog : LibraryEvent()
data class OnDeleteBooks(val refreshList: () -> Unit) : LibraryEvent() data class OnDeleteBooks(val refreshList: () -> Unit) : LibraryEvent()
data class OnUpdateBook(val book: Book) : LibraryEvent() data class OnUpdateBook(val book: Book) : LibraryEvent()
data class OnNavigateToReaderScreen(
val navigator: Navigator,
val book: Book
) : LibraryEvent()
} }

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.presentation.screens.library.data
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Selected import ua.acclorite.book_story.domain.util.Selected
@Immutable @Immutable
data class LibraryState( data class LibraryState(
@ -25,6 +25,4 @@ data class LibraryState(
val selectedCategory: Category = Category.READING, val selectedCategory: Category = Category.READING,
val showDeleteDialog: Boolean = false, val showDeleteDialog: Boolean = false,
) )

View file

@ -12,10 +12,15 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.use_case.DeleteBooks import ua.acclorite.book_story.domain.use_case.DeleteBooks
import ua.acclorite.book_story.domain.use_case.GetBooks import ua.acclorite.book_story.domain.use_case.GetBooks
import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Screen
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -23,7 +28,8 @@ import javax.inject.Inject
class LibraryViewModel @Inject constructor( class LibraryViewModel @Inject constructor(
private val getBooks: GetBooks, private val getBooks: GetBooks,
private val updateBooks: UpdateBooks, private val updateBooks: UpdateBooks,
private val deleteBooks: DeleteBooks private val deleteBooks: DeleteBooks,
private val insertHistory: InsertHistory
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(LibraryState()) private val _state = MutableStateFlow(LibraryState())
@ -33,6 +39,7 @@ class LibraryViewModel @Inject constructor(
val isReady = _isReady.asStateFlow() val isReady = _isReady.asStateFlow()
private var job: Job? = null private var job: Job? = null
private var job2: Job? = null
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -69,16 +76,9 @@ class LibraryViewModel @Inject constructor(
} }
} }
is LibraryEvent.OnPreloadBooks -> {
_state.update {
it.copy(
books = event.books.map { book -> Pair(book, false) }
)
}
}
is LibraryEvent.OnRefreshList -> { is LibraryEvent.OnRefreshList -> {
viewModelScope.launch(Dispatchers.IO) { job2?.cancel()
job2 = viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = true, isRefreshing = true,
@ -101,7 +101,8 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
isLoading = true isLoading = true,
hasSelectedItems = false
) )
} }
getBooksFromDatabase() getBooksFromDatabase()
@ -149,7 +150,7 @@ class LibraryViewModel @Inject constructor(
) )
} }
job?.cancel() job?.cancel()
job = viewModelScope.launch { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
getBooksFromDatabase() getBooksFromDatabase()
} }
@ -157,20 +158,19 @@ class LibraryViewModel @Inject constructor(
is LibraryEvent.OnSelectBook -> { is LibraryEvent.OnSelectBook -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val indexOfBook = _state.value.books.indexOf(event.book) val editedList = _state.value.books.map {
if (it.first.id == event.book.first.id) {
if (indexOfBook == -1) { it.copy(
return@launch second = event.select ?: !it.second
}
val editedList = _state.value.books.toMutableList()
editedList[indexOfBook] = editedList[indexOfBook].copy(
second = event.select ?: !editedList[indexOfBook].second
) )
} else {
it
}
}
_state.update { _state.update {
it.copy( it.copy(
books = editedList.toList(), books = editedList,
selectedItemsCount = editedList.filter { book -> book.second }.size, selectedItemsCount = editedList.filter { book -> book.second }.size,
hasSelectedItems = editedList.any { book -> book.second } hasSelectedItems = editedList.any { book -> book.second }
) )
@ -234,6 +234,7 @@ class LibraryViewModel @Inject constructor(
updateBooks.execute(books) updateBooks.execute(books)
getBooksFromDatabase() getBooksFromDatabase()
event.refreshList()
onEvent(LibraryEvent.OnClearSelectedBooks) onEvent(LibraryEvent.OnClearSelectedBooks)
onEvent( onEvent(
@ -278,14 +279,16 @@ class LibraryViewModel @Inject constructor(
is LibraryEvent.OnUpdateBook -> { is LibraryEvent.OnUpdateBook -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val books = _state.value.books.toMutableList() val books = _state.value.books.map {
val index = books.indexOfFirst { it.first.id == event.book.id } if (it.first.id == event.book.id) {
it.copy(
if (index == -1) { first = event.book
return@launch )
} else {
it
}
} }
books[index] = Pair(event.book, books[index].second)
_state.update { _state.update {
it.copy( it.copy(
books = books books = books
@ -293,6 +296,30 @@ class LibraryViewModel @Inject constructor(
} }
} }
} }
is LibraryEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch {
event.book.id?.let {
insertHistory.execute(
listOf(
History(
null,
it,
null,
Date().time
)
)
)
}
event.navigator.navigate(
Screen.READER,
false,
Argument(
"book", event.book
)
)
}
}
} }
} }

View file

@ -28,6 +28,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -54,6 +55,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.is_messages.IsError 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.MainViewModel
@ -69,8 +71,7 @@ import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.Re
import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet 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.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import ua.acclorite.book_story.util.Constants
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -86,8 +87,10 @@ fun ReaderScreen(
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f) val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
val state by viewModel.state.collectAsState()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val state by viewModel.state.collectAsState()
val canScrollForward by remember { derivedStateOf { listState.canScrollForward } }
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(
@ -106,47 +109,45 @@ fun ReaderScreen(
} }
} }
val fontFamily = Constants.FONTS.find { val mainState by mainViewModel.state.collectAsState()
it.id == mainViewModel.fontFamily.collectAsState().value!! val fontFamily = remember(mainState.fontFamily) {
Constants.FONTS.find {
it.id == mainState.fontFamily
} ?: Constants.FONTS[0] } ?: Constants.FONTS[0]
val fontStyle = when (mainViewModel.isItalic.collectAsState().value!!) {
false -> FontStyle.Normal
true -> FontStyle.Italic
} }
val fontSize = mainViewModel.fontSize.collectAsState().value!!
val lineHeight = mainViewModel.lineHeight.collectAsState().value!!
val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!!
val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!!
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
val fontColor = mainViewModel.fontColor.collectAsState().value!!
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init( viewModel.init(
navigator, navigator,
context, context,
listState,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnLoadList) historyViewModel.onEvent(HistoryEvent.OnLoadList)
}, },
listState = listState,
onLoaded = { onLoaded = {
loading = false loading = false
} }
) )
} }
LaunchedEffect(canScrollForward) {
if (!canScrollForward && !state.showMenu && !loading) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
}
}
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { snapshotFlow {
listState.firstVisibleItemIndex listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(1000).collectLatest { }.debounce(500).collectLatest { items ->
if (!loading) { if (!loading) {
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (it > 0) { val progress = if (items.first > 0) {
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) { if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
1f 1f
} else { } else {
(it.toFloat() / (state.book.text.lastIndex).toFloat()) (items.first.toFloat() / (state.book.text.lastIndex).toFloat())
} }
} else { } else {
0f 0f
@ -154,14 +155,13 @@ fun ReaderScreen(
viewModel.onEvent( viewModel.onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
progress, progress = progress,
navigator, navigator = navigator,
firstVisibleItemIndex = items.first,
firstVisibleItemOffset = items.second,
refreshList = { book -> refreshList = { book ->
libraryViewModel.onEvent( libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
LibraryEvent.OnUpdateBook( historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
book
)
)
} }
) )
) )
@ -186,7 +186,10 @@ fun ReaderScreen(
) { ) {
ReaderTopBar( ReaderTopBar(
viewModel = viewModel, viewModel = viewModel,
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
navigator = navigator, navigator = navigator,
listState = listState,
containerColor = systemBarsColor containerColor = systemBarsColor
) )
} }
@ -200,8 +203,9 @@ fun ReaderScreen(
ReaderBottomBar( ReaderBottomBar(
viewModel = viewModel, viewModel = viewModel,
libraryViewModel = libraryViewModel, libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
listState = listState,
navigator = navigator, navigator = navigator,
scrollState = listState,
systemBarsColor = systemBarsColor systemBarsColor = systemBarsColor
) )
} }
@ -280,36 +284,44 @@ fun ReaderScreen(
itemsIndexed( itemsIndexed(
state.book.text, key = { _, key -> key.id } state.book.text, key = { _, key -> key.id }
) { index, line -> ) { index, line ->
val text = val text = remember(mainState.paragraphIndentation) {
remember(paragraphIndentation) { "${if (paragraphIndentation) " " else ""}${line.line}" } "${if (mainState.paragraphIndentation!!) " " else ""}${line.line}"
val fontCol = remember(fontColor) { Color(fontColor.toULong()) } }
val backgroundCol = val fontColor = remember(mainState.fontColor) {
remember(backgroundColor) { Color(backgroundColor.toULong()) } Color(mainState.fontColor!!.toULong())
val lineHeightSp = remember(fontSize, lineHeight) { (fontSize + lineHeight).sp } }
val backgroundColor = remember(mainState.backgroundColor) {
Color(mainState.backgroundColor!!.toULong())
}
val lineHeight = remember(mainState.fontSize, mainState.lineHeight) {
(mainState.fontSize!! + mainState.lineHeight!!).sp
}
Column( Column(
Modifier Modifier
.background(backgroundCol) .background(backgroundColor)
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
top = if (index == 0) 36.dp else 0.dp, top = if (index == 0) 18.dp else 0.dp,
start = 18.dp, start = 18.dp,
end = 18.dp, end = 18.dp,
bottom = if (index == state.book.text.lastIndex) 36.dp bottom = if (index == state.book.text.lastIndex) 18.dp
else (paragraphHeight * 3).dp else (mainState.paragraphHeight!! * 3).dp
) )
) { ) {
Text( Text(
text = text, text = text,
color = fontCol, color = fontColor,
style = TextStyle( style = TextStyle(
lineBreak = LineBreak.Paragraph lineBreak = LineBreak.Paragraph
), ),
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = fontStyle, fontStyle = when (mainState.isItalic!!) {
fontSize = fontSize.sp, true -> FontStyle.Italic
lineHeight = lineHeightSp false -> FontStyle.Normal
},
fontSize = mainState.fontSize!!.sp,
lineHeight = lineHeight
) )
} }
} }
@ -319,7 +331,9 @@ fun ReaderScreen(
DisableSelection { DisableSelection {
ReaderEndItem( ReaderEndItem(
libraryViewModel = libraryViewModel, libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
viewModel = viewModel, viewModel = viewModel,
listState = listState,
navigator = navigator navigator = navigator
) )
} }
@ -343,10 +357,21 @@ fun ReaderScreen(
icon = painterResource(id = R.drawable.error), icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.go_back), actionTitle = stringResource(id = R.string.go_back),
action = { action = {
navigator.navigateBack()
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(LibraryEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList) viewModel.onEvent(
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
it.navigateBack()
}
)
)
} }
) )
} else { } else {
@ -369,7 +394,19 @@ fun ReaderScreen(
} }
BackHandler { BackHandler {
navigator.navigateBack() viewModel.onEvent(
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
it.navigateBack()
}
)
)
} }
} }

View file

@ -8,11 +8,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -32,6 +33,8 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel 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.ReaderEvent
@ -44,7 +47,9 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
@Composable @Composable
fun ReaderEndItem( fun ReaderEndItem(
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
viewModel: ReaderViewModel, viewModel: ReaderViewModel,
listState: LazyListState,
navigator: Navigator navigator: Navigator
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
@ -54,12 +59,11 @@ fun ReaderEndItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.windowInsetsPadding(WindowInsets.navigationBarsIgnoringVisibility)
.padding( .padding(
top = 24.dp, horizontal = 36.dp,
start = 24.dp, vertical = 108.dp + WindowInsets.navigationBarsIgnoringVisibility
end = 24.dp, .asPaddingValues()
bottom = 8.dp .calculateBottomPadding()
), ),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@ -72,7 +76,11 @@ fun ReaderEndItem(
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
stringResource(id = R.string.letters_and_words, state.letters, state.words), stringResource(
id = R.string.letters_and_words,
state.book.letters,
state.book.words
),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@ -83,15 +91,15 @@ fun ReaderEndItem(
if (state.book.category != Category.ALREADY_READ) { if (state.book.category != Category.ALREADY_READ) {
viewModel.onEvent( viewModel.onEvent(
ReaderEvent.OnMoveBookToAlreadyRead( ReaderEvent.OnMoveBookToAlreadyRead(
refreshList = { onUpdateCategories = {
libraryViewModel.onEvent( libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
LibraryEvent.OnLoadList historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
)
}, },
updatePage = { updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it)) libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))
}, },
navigator = navigator, navigator = navigator,
listState = listState,
context = context context = context
) )
) )
@ -103,11 +111,19 @@ fun ReaderEndItem(
).show() ).show()
} else { } else {
viewModel.onEvent( viewModel.onEvent(
ReaderEvent.OnShowSystemBars( ReaderEvent.OnGoBack(
context context,
navigator,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
it.navigate(Screen.LIBRARY, true)
}
) )
) )
navigator.navigate(Screen.LIBRARY, true)
} }
}, },
contentPadding = PaddingValues(horizontal = 12.dp), contentPadding = PaddingValues(horizontal = 12.dp),

View file

@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Image
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -34,7 +35,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Reader start item. Displays at the beginning of the book. * Reader start item. Displays at the beginning of the book.
@ -49,7 +50,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility)
.padding(24.dp) .padding(horizontal = 24.dp, vertical = 80.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier verticalAlignment = Alignment.CenterVertically, modifier = Modifier
@ -59,7 +60,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
modifier = Modifier modifier = Modifier
.height(135.dp) .height(135.dp)
.width(90.dp) .width(90.dp)
.clip(MaterialTheme.shapes.medium) .clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.elevation()) .background(MaterialTheme.elevation())
) { ) {
if (state.book.coverImage != null) { if (state.book.coverImage != null) {
@ -68,7 +69,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
contentDescription = stringResource(id = R.string.cover_image_content_desc), contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.medium), .clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} else { } else {

View file

@ -24,6 +24,8 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel 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.ReaderEvent
@ -36,8 +38,9 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
fun ReaderBottomBar( fun ReaderBottomBar(
viewModel: ReaderViewModel, viewModel: ReaderViewModel,
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
listState: LazyListState,
navigator: Navigator, navigator: Navigator,
scrollState: LazyListState,
systemBarsColor: Color systemBarsColor: Color
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
@ -74,20 +77,19 @@ fun ReaderBottomBar(
Slider( Slider(
value = state.book.progress, value = state.book.progress,
onValueChange = { onValueChange = {
viewModel.onEvent(ReaderEvent.OnScroll(listState, it))
viewModel.onEvent( viewModel.onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
progress = it, progress = it,
navigator = navigator, navigator = navigator,
firstVisibleItemIndex = listState.firstVisibleItemIndex,
firstVisibleItemOffset = 0,
refreshList = { book -> refreshList = { book ->
libraryViewModel.onEvent( libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
LibraryEvent.OnUpdateBook( historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
book
)
)
} }
) )
) )
viewModel.onEvent(ReaderEvent.OnScroll(scrollState, it))
}, },
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
activeTrackColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary,

View file

@ -5,7 +5,9 @@ import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -26,12 +28,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomIconButton
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.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
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.data.ReaderEvent 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.presentation.screens.reader.data.ReaderViewModel
@ -40,7 +44,14 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) { fun ReaderTopBar(
viewModel: ReaderViewModel,
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
listState: LazyListState,
navigator: Navigator,
containerColor: Color
) {
val context = LocalContext.current as ComponentActivity val context = LocalContext.current as ComponentActivity
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
@ -56,8 +67,25 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) { CustomIconButton(
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.go_back_content_desc),
disableOnClick = true
) {
viewModel.onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
it.navigateBack()
}
)
)
} }
}, },
title = { title = {
@ -74,12 +102,21 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
interactionSource = null, interactionSource = null,
indication = null, indication = null,
onClick = { onClick = {
navigator.navigateWithoutBackStack( viewModel.onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
it.navigateWithoutBackStack(
Screen.BOOK_INFO, Screen.BOOK_INFO,
true, true
Argument( )
"book", }
state.book.id
) )
) )
} }

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ChipItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
@ -44,9 +45,8 @@ import ua.acclorite.book_story.presentation.screens.settings.components.Checkbox
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
import ua.acclorite.book_story.ui.ElevationDefaults import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import ua.acclorite.book_story.util.Constants
/** /**
* Settings bottom sheet. Has General and Colors categories. * Settings bottom sheet. Has General and Colors categories.
@ -63,16 +63,13 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
val navigationBarPadding = val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val fontFamily = Constants.FONTS.find { val state by mainViewModel.state.collectAsState()
it.id == mainViewModel.fontFamily.collectAsState().value!!
val fontFamily = remember(state.fontFamily) {
Constants.FONTS.find {
it.id == state.fontFamily
} ?: Constants.FONTS[0] } ?: 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!!
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
val fontColor = mainViewModel.fontColor.collectAsState().value!!
val scrimColor = if (currentPage == 1) Color.Transparent val scrimColor = if (currentPage == 1) Color.Transparent
else BottomSheetDefaults.ScrimColor else BottomSheetDefaults.ScrimColor
@ -161,7 +158,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal fontStyle = FontStyle.Normal
), ),
!fontStyle !state.isItalic!!
), ),
ChipItem( ChipItem(
"italic", "italic",
@ -170,7 +167,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
), ),
fontStyle state.isItalic!!
), ),
), ),
onClick = { onClick = {
@ -187,7 +184,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = fontSize to "pt", value = state.fontSize!! to "pt",
fromValue = 10, fromValue = 10,
toValue = 35, toValue = 35,
title = stringResource(id = R.string.font_size_option), title = stringResource(id = R.string.font_size_option),
@ -212,7 +209,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = lineHeight to "pt", value = state.lineHeight!! to "pt",
fromValue = 1, fromValue = 1,
toValue = 16, toValue = 16,
title = stringResource(id = R.string.line_height_option), title = stringResource(id = R.string.line_height_option),
@ -225,7 +222,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = paragraphHeight to "pt", value = state.paragraphHeight!! to "pt",
fromValue = 0, fromValue = 0,
toValue = 24, toValue = 24,
title = stringResource(id = R.string.paragraph_height_option), title = stringResource(id = R.string.paragraph_height_option),
@ -238,11 +235,11 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
CheckboxWithTitle( CheckboxWithTitle(
selected = paragraphIndentation, selected = state.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option) title = stringResource(id = R.string.paragraph_indentation_option)
) { ) {
mainViewModel.onEvent( mainViewModel.onEvent(
MainEvent.OnChangeParagraphIndentation(!paragraphIndentation) MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
) )
} }
} }
@ -254,7 +251,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
value = Color(backgroundColor.toULong()), value = Color(state.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option), title = stringResource(id = R.string.background_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( mainViewModel.onEvent(
@ -267,7 +264,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
value = Color(fontColor.toULong()), value = Color(state.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option), title = stringResource(id = R.string.font_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( mainViewModel.onEvent(

View file

@ -12,8 +12,7 @@ import ua.acclorite.book_story.presentation.data.Navigator
sealed class ReaderEvent { sealed class ReaderEvent {
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent() data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
data class OnLoadText( data class OnLoadText(
val scrollState: LazyListState, val listState: LazyListState,
val navigator: Navigator,
val refreshList: (Book) -> Unit, val refreshList: (Book) -> Unit,
val onLoaded: () -> Unit, val onLoaded: () -> Unit,
val onTextIsEmpty: () -> Unit val onTextIsEmpty: () -> Unit
@ -22,19 +21,29 @@ sealed class ReaderEvent {
data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) : data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) :
ReaderEvent() ReaderEvent()
data class OnShowSystemBars(val context: ComponentActivity) : ReaderEvent() data class OnGoBack(
val context: ComponentActivity,
val navigator: Navigator,
val listState: LazyListState,
val refreshList: (Book) -> Unit,
val navigate: (Navigator) -> Unit
) : ReaderEvent()
data class OnChangeProgress( data class OnChangeProgress(
val progress: Float, val progress: Float,
val navigator: Navigator, val navigator: Navigator,
val firstVisibleItemIndex: Int,
val firstVisibleItemOffset: Int,
val refreshList: (Book) -> Unit val refreshList: (Book) -> Unit
) : ReaderEvent() ) : ReaderEvent()
data class OnScroll(val scrollState: LazyListState, val progress: Float) : ReaderEvent() data class OnScroll(val listState: LazyListState, val progress: Float) : ReaderEvent()
data object OnShowHideSettingsBottomSheet : ReaderEvent() data object OnShowHideSettingsBottomSheet : ReaderEvent()
data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent() data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent()
data class OnMoveBookToAlreadyRead( data class OnMoveBookToAlreadyRead(
val context: ComponentActivity, val context: ComponentActivity,
val refreshList: () -> Unit, val listState: LazyListState,
val onUpdateCategories: (Book) -> Unit,
val updatePage: (Int) -> Unit, val updatePage: (Int) -> Unit,
val navigator: Navigator val navigator: Navigator
) : ReaderEvent() ) : ReaderEvent()

View file

@ -2,17 +2,13 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
data class ReaderState( data class ReaderState(
val book: Book = Constants.EMPTY_BOOK, val book: Book = Constants.EMPTY_BOOK,
val letters: Int = 0,
val words: Int = 0,
val errorMessage: UIText? = null, val errorMessage: UIText? = null,
val showMenu: Boolean = false, val showMenu: Boolean = false,
val showSettingsBottomSheet: Boolean = false, val showSettingsBottomSheet: Boolean = false,
val currentPage: Int = 0, val currentPage: Int = 0,

View file

@ -22,17 +22,14 @@ import kotlinx.coroutines.launch
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.use_case.GetBooksById import ua.acclorite.book_story.domain.use_case.GetLatestHistory
import ua.acclorite.book_story.domain.use_case.GetText 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.domain.use_case.UpdateBooks
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.util.UIText
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -40,9 +37,8 @@ import kotlin.math.roundToInt
@HiltViewModel @HiltViewModel
class ReaderViewModel @Inject constructor( class ReaderViewModel @Inject constructor(
private val updateBooks: UpdateBooks, private val updateBooks: UpdateBooks,
private val insertHistory: InsertHistory,
private val getText: GetText, private val getText: GetText,
private val getBooksById: GetBooksById private val getLatestHistory: GetLatestHistory
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(ReaderState()) private val _state = MutableStateFlow(ReaderState())
@ -67,7 +63,6 @@ class ReaderViewModel @Inject constructor(
event.onTextIsEmpty() event.onTextIsEmpty()
} }
val time = Date().time
val letters = text val letters = text
.replace("\n", "") .replace("\n", "")
.length .length
@ -76,50 +71,46 @@ class ReaderViewModel @Inject constructor(
.split("\\s+".toRegex()) .split("\\s+".toRegex())
.size .size
val history = _state.value.book.id?.let {
getLatestHistory.execute(
it
)
}
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
text = text text = text
.split("\n") .split("\n")
.map { line -> StringWithId(line.trim()) }, .map { line -> StringWithId(line.trim()) },
lastOpened = time lastOpened = history?.time,
),
letters = letters, letters = letters,
words = words words = words
) )
)
} }
updateBooks.execute( updateBooks.execute(
listOf(_state.value.book) listOf(_state.value.book)
) )
insertHistory.execute(
listOf(
History(
null,
_state.value.book.id!!,
time
)
)
)
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
event.navigator.putArgument(
Argument("book", _state.value.book.id)
)
viewModelScope.launch { viewModelScope.launch {
snapshotFlow { snapshotFlow {
event.scrollState.layoutInfo.totalItemsCount event.listState.layoutInfo.totalItemsCount
}.collectLatest { itemsCount -> }.collectLatest { itemsCount ->
val scrollTo = val index = _state.value.book.scrollIndex
(_state.value.book.text.size * _state.value.book.progress) val offset = _state.value.book.scrollOffset
.toInt()
if (itemsCount >= _state.value.book.text.size) { if (itemsCount >= _state.value.book.text.size) {
if (scrollTo > 0) { if (index > 0 || offset > 0) {
var loaded = false var loaded = false
for (i in 1..100) { for (i in 1..100) {
try { try {
event.scrollState.scrollToItem(scrollTo) event.listState.scrollToItem(
index,
offset
)
loaded = true loaded = true
break break
} catch (e: Exception) { } catch (e: Exception) {
@ -143,6 +134,7 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnShowHideMenu -> { is ReaderEvent.OnShowHideMenu -> {
val shouldShow = event.show ?: !_state.value.showMenu val shouldShow = event.show ?: !_state.value.showMenu
val insetsController = WindowCompat.getInsetsController( val insetsController = WindowCompat.getInsetsController(
event.context.window, event.context.window,
event.context.window.decorView event.context.window.decorView
@ -166,20 +158,57 @@ class ReaderViewModel @Inject constructor(
} }
} }
is ReaderEvent.OnShowSystemBars -> { is ReaderEvent.OnGoBack -> {
viewModelScope.launch {
val insetsController = WindowCompat.getInsetsController( val insetsController = WindowCompat.getInsetsController(
event.context.window, event.context.window,
event.context.window.decorView event.context.window.decorView
) )
val firstVisibleItemIndex = event.listState.firstVisibleItemIndex
val firstVisibleItemOffset = event.listState.firstVisibleItemScrollOffset
val lastVisibleItemIndex = event.listState.layoutInfo
.visibleItemsInfo.last().index
val progress = if (firstVisibleItemIndex > 0) {
if (lastVisibleItemIndex >= (event.listState.layoutInfo.totalItemsCount - 1)) {
1f
} else {
(firstVisibleItemIndex.toFloat() / (_state.value.book.text.lastIndex)
.toFloat())
}
} else {
0f
}
_state.update {
it.copy(
book = it.book.copy(
progress = progress,
scrollIndex = firstVisibleItemIndex,
scrollOffset = firstVisibleItemOffset
)
)
}
updateBooks.execute(
listOf(_state.value.book)
)
event.navigator.putArgument(
Argument("book", _state.value.book)
)
event.refreshList(_state.value.book)
insetsController.show(WindowInsetsCompat.Type.systemBars()) insetsController.show(WindowInsetsCompat.Type.systemBars())
event.navigate(event.navigator)
}
} }
is ReaderEvent.OnScroll -> { is ReaderEvent.OnScroll -> {
viewModelScope.launch { viewModelScope.launch {
val scrollTo = (_state.value.book.text.size * event.progress).roundToInt() val scrollTo = (_state.value.book.text.size * event.progress).roundToInt()
event.scrollState.scrollToItem( event.listState.scrollToItem(
scrollTo scrollTo
) )
} }
@ -190,7 +219,9 @@ class ReaderViewModel @Inject constructor(
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
progress = event.progress progress = event.progress,
scrollIndex = event.firstVisibleItemIndex,
scrollOffset = event.firstVisibleItemOffset
) )
) )
} }
@ -199,13 +230,17 @@ class ReaderViewModel @Inject constructor(
listOf(_state.value.book) listOf(_state.value.book)
) )
event.navigator.putArgument( event.navigator.putArgument(
Argument("book", _state.value.book.id) Argument(
"book",
_state.value.book
)
) )
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
} }
} }
is ReaderEvent.OnShowHideSettingsBottomSheet -> { is ReaderEvent.OnShowHideSettingsBottomSheet -> {
viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0, currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0,
@ -213,6 +248,7 @@ class ReaderViewModel @Inject constructor(
) )
} }
} }
}
is ReaderEvent.OnScrollToSettingsPage -> { is ReaderEvent.OnScrollToSettingsPage -> {
viewModelScope.launch { viewModelScope.launch {
@ -222,16 +258,32 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnMoveBookToAlreadyRead -> { is ReaderEvent.OnMoveBookToAlreadyRead -> {
viewModelScope.launch { viewModelScope.launch {
onEvent(ReaderEvent.OnShowSystemBars(event.context)) onEvent(
ReaderEvent.OnGoBack(
val book = _state.value.book.copy( event.context,
category = Category.ALREADY_READ, event.navigator,
progress = 1f event.listState,
refreshList = {},
navigate = {}
)
) )
updateBooks.execute(listOf(book))
event.refreshList() val firstVisibleItemIndex = event.listState.firstVisibleItemIndex
val firstVisibleItemOffset = event.listState.firstVisibleItemScrollOffset
_state.update {
it.copy(
book = it.book.copy(
category = Category.ALREADY_READ,
progress = 1f,
scrollIndex = firstVisibleItemIndex,
scrollOffset = firstVisibleItemOffset
)
)
}
updateBooks.execute(listOf(_state.value.book))
event.onUpdateCategories(_state.value.book)
event.updatePage( event.updatePage(
Category.entries.dropLastWhile { Category.entries.dropLastWhile {
it != Category.ALREADY_READ it != Category.ALREADY_READ
@ -298,30 +350,30 @@ class ReaderViewModel @Inject constructor(
fun init( fun init(
navigator: Navigator, navigator: Navigator,
context: ComponentActivity, context: ComponentActivity,
scrollState: LazyListState, listState: LazyListState,
refreshList: (Book) -> Unit, refreshList: (Book) -> Unit,
onLoaded: () -> Unit onLoaded: () -> Unit
) { ) {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val bookIndex = navigator.retrieveArgument("book") as? Int val book = navigator.retrieveArgument("book") as? Book
if (bookIndex == null || bookIndex < 0) { if (book == null) {
navigator.navigateBack() navigator.navigateBack()
return@launch return@launch
} }
val book = getBooksById.execute(listOf(bookIndex)).first() viewModelScope.launch {
onEvent(ReaderEvent.OnShowHideMenu(false, context))
}
_state.update { _state.update {
ReaderState(book = book) ReaderState(book = book)
} }
onEvent(ReaderEvent.OnShowHideMenu(false, context))
onEvent( onEvent(
ReaderEvent.OnLoadText( ReaderEvent.OnLoadText(
scrollState,
navigator,
refreshList = { refreshList(it) }, refreshList = { refreshList(it) },
listState = listState,
onLoaded = { onLoaded = {
onLoaded() onLoaded()
}, },

View file

@ -29,7 +29,7 @@ import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -39,7 +39,7 @@ fun SettingsScreen(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = { canScroll = {
listState.canScrollForward listState.canScrollForward || listState.canScrollBackward
} }
) )
@ -71,6 +71,10 @@ fun SettingsScreen(
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = listState
) { ) {
item {
Spacer(modifier = Modifier.height(16.dp))
}
item { item {
SettingsCategoryItem( SettingsCategoryItem(
icon = Icons.Default.DisplaySettings, icon = Icons.Default.DisplaySettings,

View file

@ -17,6 +17,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -32,8 +33,8 @@ 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.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle 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.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher
import ua.acclorite.book_story.ui.DarkTheme import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -44,13 +45,11 @@ fun AppearanceSettings(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = { canScroll = {
listState.canScrollForward listState.canScrollForward || listState.canScrollBackward
} }
) )
val darkTheme = mainViewModel.darkTheme.collectAsState().value!! val state by mainViewModel.state.collectAsState()
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
val fontColor = mainViewModel.fontColor.collectAsState().value!!
Scaffold( Scaffold(
Modifier Modifier
@ -81,7 +80,7 @@ fun AppearanceSettings(
state = listState state = listState
) { ) {
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
item { item {
CategoryTitle( CategoryTitle(
@ -105,7 +104,7 @@ fun AppearanceSettings(
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system) DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
}, },
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,
it == darkTheme it == state.darkTheme
) )
}, },
) { ) {
@ -136,7 +135,7 @@ fun AppearanceSettings(
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
value = Color(backgroundColor.toULong()), value = Color(state.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option), title = stringResource(id = R.string.background_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( mainViewModel.onEvent(
@ -149,7 +148,7 @@ fun AppearanceSettings(
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
value = Color(fontColor.toULong()), value = Color(state.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option), title = stringResource(id = R.string.font_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( mainViewModel.onEvent(

View file

@ -10,17 +10,18 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.ui.isDark import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.util.Constants
/** /**
* Theme switcher. * Theme switcher.
@ -29,8 +30,7 @@ import ua.acclorite.book_story.util.Constants
fun AppearanceSettingsThemeSwitcher( fun AppearanceSettingsThemeSwitcher(
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
val theme = mainViewModel.theme.collectAsState().value!! val state by mainViewModel.state.collectAsState()
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
val themes = remember { val themes = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
@ -56,8 +56,8 @@ fun AppearanceSettingsThemeSwitcher(
AppearanceSettingsThemeSwitcherItem( AppearanceSettingsThemeSwitcherItem(
theme = themeEntry, theme = themeEntry,
darkTheme = darkTheme.isDark(), darkTheme = state.darkTheme!!.isDark(),
selected = theme == themeEntry.first selected = state.theme == themeEntry.first
) { ) {
mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
} }

View file

@ -31,9 +31,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.ui.colorScheme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.presentation.ui.colorScheme
/** /**
* Theme switcher item. * Theme switcher item.

View file

@ -17,19 +17,20 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ChipItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator 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.components.ChipsWithTitle
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import ua.acclorite.book_story.util.Constants
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -40,11 +41,11 @@ fun GeneralSettings(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = { canScroll = {
listState.canScrollForward listState.canScrollForward || listState.canScrollBackward
} }
) )
val language = mainViewModel.language.collectAsState().value!! val state by mainViewModel.state.collectAsState()
Scaffold( Scaffold(
Modifier Modifier
@ -74,6 +75,9 @@ fun GeneralSettings(
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = listState
) { ) {
item {
Spacer(modifier = Modifier.height(8.dp))
}
item { item {
ChipsWithTitle( ChipsWithTitle(
title = stringResource(id = R.string.language_option), title = stringResource(id = R.string.language_option),
@ -82,7 +86,7 @@ fun GeneralSettings(
it.first, it.first,
it.second, it.second,
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,
it.first == language it.first == state.language
) )
}.sortedBy { it.title } }.sortedBy { it.title }
) { ) {

View file

@ -17,6 +17,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -24,6 +26,7 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ChipItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
@ -32,8 +35,7 @@ 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.CheckboxWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle 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.presentation.screens.settings.components.SliderWithTitle
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.presentation.ui.elevation
import ua.acclorite.book_story.util.Constants
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -44,18 +46,16 @@ fun ReaderSettings(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = { canScroll = {
listState.canScrollForward listState.canScrollForward || listState.canScrollBackward
} }
) )
val fontFamily = Constants.FONTS.find { val state by mainViewModel.state.collectAsState()
it.id == mainViewModel.fontFamily.collectAsState().value!! val fontFamily = remember(state.fontFamily) {
Constants.FONTS.find {
it.id == state.fontFamily
} ?: Constants.FONTS[0] } ?: 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( Scaffold(
Modifier Modifier
@ -86,7 +86,7 @@ fun ReaderSettings(
state = listState state = listState
) { ) {
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
item { item {
CategoryTitle( CategoryTitle(
@ -127,7 +127,7 @@ fun ReaderSettings(
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal fontStyle = FontStyle.Normal
), ),
!fontStyle !state.isItalic!!
), ),
ChipItem( ChipItem(
"italic", "italic",
@ -136,7 +136,7 @@ fun ReaderSettings(
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
), ),
fontStyle state.isItalic!!
), ),
), ),
onClick = { onClick = {
@ -153,7 +153,7 @@ fun ReaderSettings(
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = fontSize to "pt", value = state.fontSize!! to "pt",
fromValue = 10, fromValue = 10,
toValue = 35, toValue = 35,
title = stringResource(id = R.string.font_size_option), title = stringResource(id = R.string.font_size_option),
@ -178,7 +178,7 @@ fun ReaderSettings(
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = lineHeight to "pt", value = state.lineHeight!! to "pt",
fromValue = 1, fromValue = 1,
toValue = 16, toValue = 16,
title = stringResource(id = R.string.line_height_option), title = stringResource(id = R.string.line_height_option),
@ -191,7 +191,7 @@ fun ReaderSettings(
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = paragraphHeight to "pt", value = state.paragraphHeight!! to "pt",
fromValue = 0, fromValue = 0,
toValue = 24, toValue = 24,
title = stringResource(id = R.string.paragraph_height_option), title = stringResource(id = R.string.paragraph_height_option),
@ -204,11 +204,11 @@ fun ReaderSettings(
} }
item { item {
CheckboxWithTitle( CheckboxWithTitle(
selected = paragraphIndentation, selected = state.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option) title = stringResource(id = R.string.paragraph_indentation_option)
) { ) {
mainViewModel.onEvent( mainViewModel.onEvent(
MainEvent.OnChangeParagraphIndentation(!paragraphIndentation) MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
) )
} }
} }

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.ui package ua.acclorite.book_story.presentation.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
@ -6,13 +6,13 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import ua.acclorite.book_story.ui.theme.aquaTheme import ua.acclorite.book_story.presentation.ui.theme.aquaTheme
import ua.acclorite.book_story.ui.theme.blueTheme import ua.acclorite.book_story.presentation.ui.theme.blueTheme
import ua.acclorite.book_story.ui.theme.greenTheme import ua.acclorite.book_story.presentation.ui.theme.greenTheme
import ua.acclorite.book_story.ui.theme.pinkTheme import ua.acclorite.book_story.presentation.ui.theme.pinkTheme
import ua.acclorite.book_story.ui.theme.purpleTheme import ua.acclorite.book_story.presentation.ui.theme.purpleTheme
import ua.acclorite.book_story.ui.theme.redTheme import ua.acclorite.book_story.presentation.ui.theme.redTheme
import ua.acclorite.book_story.ui.theme.yellowTheme import ua.acclorite.book_story.presentation.ui.theme.yellowTheme
enum class Theme { enum class Theme {
DYNAMIC, DYNAMIC,
@ -29,16 +29,7 @@ enum class Theme {
* Converting [String] into [Theme]. * Converting [String] into [Theme].
*/ */
fun String.toTheme(): Theme { fun String.toTheme(): Theme {
return when (this) { return Theme.valueOf(this)
"DYNAMIC" -> Theme.DYNAMIC
"PURPLE" -> Theme.PURPLE
"GREEN" -> Theme.GREEN
"PINK" -> Theme.PINK
"YELLOW" -> Theme.YELLOW
"RED" -> Theme.RED
"AQUA" -> Theme.AQUA
else -> Theme.BLUE
}
} }
/** /**

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.ui package ua.acclorite.book_story.presentation.ui
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.ui package ua.acclorite.book_story.presentation.ui
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
@ -22,11 +22,7 @@ enum class DarkTheme {
} }
fun String.toDarkTheme(): DarkTheme { fun String.toDarkTheme(): DarkTheme {
return when (this) { return DarkTheme.valueOf(this)
"OFF" -> DarkTheme.OFF
"ON" -> DarkTheme.ON
else -> DarkTheme.FOLLOW_SYSTEM
}
} }
@Composable @Composable

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.ui package ua.acclorite.book_story.presentation.ui
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.ui package ua.acclorite.book_story.presentation.ui
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

Some files were not shown because too many files have changed in this diff Show more