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.jsoup:jsoup:latest.release")
// App Compat for Language Switcher
implementation("androidx.appcompat:appcompat:latest.release")
implementation("androidx.appcompat:appcompat-resources:latest.release")
// Coil for loading bitmaps
implementation("io.coil-kt:coil-compose:2.6.0")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,6 +43,9 @@ interface BookDao {
@Query("SELECT * FROM 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)
suspend fun insertHistory(
history: List<HistoryEntity>

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.data.parser
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

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.txt.TxtFileParser
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 javax.inject.Inject

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.data.parser
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
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.txt.TxtTextParser
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource
import ua.acclorite.book_story.util.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import java.io.File
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.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText
import java.io.File
import java.io.FileInputStream
import javax.inject.Inject
@ -50,6 +50,10 @@ class EpubFileParser @Inject constructor() : FileParser {
author = author,
description = description?.toString(),
text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
file = file,
filePath = file.path,

View file

@ -7,8 +7,8 @@ import org.jsoup.Jsoup
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource
import ua.acclorite.book_story.util.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import java.io.BufferedReader
import java.io.File
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.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText
import java.io.File
import javax.inject.Inject
@ -29,8 +28,6 @@ class PdfFileParser @Inject constructor(private val application: Application) :
val fileAuthor = document.documentInformation.author
val author = if (fileAuthor != null) UIText.StringValue(fileAuthor)
else UIText.StringResource(R.string.unknown_author)
val text = emptyList<StringWithId>()
val description = document.documentInformation.subject ?: null
document.close()
@ -40,7 +37,11 @@ class PdfFileParser @Inject constructor(private val application: Application) :
title = title,
author = author,
description = description,
text = text,
text = emptyList(),
letters = 0,
words = 0,
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
file = file,
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.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.Resource
import ua.acclorite.book_story.util.UIText
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import java.io.File
import javax.inject.Inject
@ -28,11 +28,6 @@ class PdfTextParser @Inject constructor(private val application: Application) :
val pdfStripper = PDFTextStripper()
pdfStripper.paragraphStart = "_new_line_"
if (document.isEncrypted) {
document.close()
return Resource.Error(UIText.StringResource(R.string.error_file_encrypted))
}
val oldText = pdfStripper.getText(document)
.replace("\r", "")

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book
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
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 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 javax.inject.Inject

View file

@ -3,7 +3,7 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.History
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
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.repository.BookRepository
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject
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.repository.BookRepository
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject
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.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.Category
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 {
// SavedStateHandle constants(actually just all settings params)
const val LANGUAGE = "language"
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"
// Main State Constant
const val MAIN_STATE = "main_state"
// Supported file extensions
val EXTENSIONS = listOf(".txt", ".pdf", ".epub")
@ -182,6 +171,10 @@ object Constants {
UIText.StringValue(""),
null,
emptyList(),
0,
0,
0,
0,
0f,
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.intPreferencesKey
@ -10,7 +10,6 @@ object DataStoreConstants {
val THEME = stringPreferencesKey("theme")
val DARK_THEME = stringPreferencesKey("dark_theme")
val SHOW_START_SCREEN = booleanPreferencesKey("guide")
val BACKGROUND_COLOR = longPreferencesKey("background_color")
val FONT_COLOR = longPreferencesKey("font_color")
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) {
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

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.util
package ua.acclorite.book_story.domain.util
import android.content.Context
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.unit.dp
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.
@ -102,10 +102,12 @@ fun AnimatedTopAppBar(
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
) {
TopAppBar(
windowInsets = WindowInsets.statusBars,
navigationIcon = content1NavigationIcon,
title = content1Title,
actions = content1Actions,
scrollBehavior = scrollBehavior,
modifier = Modifier.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = animatedContainerColor,
scrolledContainerColor = scrolledContainerColor
@ -120,10 +122,12 @@ fun AnimatedTopAppBar(
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
) {
TopAppBar(
windowInsets = WindowInsets.statusBars,
navigationIcon = content2NavigationIcon,
title = content2Title,
actions = content2Actions,
scrollBehavior = scrollBehavior,
modifier = Modifier.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = animatedContainerColor,
scrolledContainerColor = scrolledContainerColor
@ -139,10 +143,12 @@ fun AnimatedTopAppBar(
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
) {
TopAppBar(
windowInsets = WindowInsets.statusBars,
navigationIcon = content3NavigationIcon,
title = content3Title,
actions = content3Actions,
scrollBehavior = scrollBehavior,
modifier = Modifier.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = animatedContainerColor,
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 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.

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.
*/
@Composable
fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) {
fun GoBackButton(navigator: Navigator, enabled: Boolean = true, customOnClick: () -> Unit = {}) {
CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.go_back_content_desc),
disableOnClick = true
disableOnClick = true,
enabled = enabled
) {
navigator.navigateBack()
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.presentation.data.Navigator
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].

View file

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

View file

@ -37,15 +37,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
/**
* Custom Dialog with lazy column.
*
* @param modifier Modifier to be applied.
* @param properties Dialog properties.
* @param drawableIcon Drawable icon to show(1/2).
* @param imageVectorIcon Image vector icon to show(2/2).
* @param backgroundTransparency The transparency of the background behind the dialog.
@ -62,7 +61,6 @@ import ua.acclorite.book_story.ui.elevation
@Composable
fun CustomDialogWithLazyColumn(
modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties(),
drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f,
@ -78,7 +76,10 @@ fun CustomDialogWithLazyColumn(
var actionClicked by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = { onDismiss() },
properties = properties
properties = DialogProperties(
dismissOnBackPress = !actionClicked,
dismissOnClickOutside = !actionClicked
)
) {
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
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.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
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.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
@ -18,19 +23,21 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.elevation
/**
* Custom Navigation Rail. It is used to be shown on Tablets.
*/
@Composable
fun CustomNavigationRail(
navigator: Navigator
fun BoxScope.CustomNavigationRail(
navigator: Navigator,
layoutDirection: LayoutDirection
) {
var currentScreen: Screen? by remember { mutableStateOf(null) }
@ -44,10 +51,19 @@ fun CustomNavigationRail(
NavigationRail(
modifier = Modifier
.width(80.dp)
.align(
Alignment.CenterStart
)
.background(MaterialTheme.elevation())
.padding(
start = WindowInsets.navigationBars
.asPaddingValues()
.calculateStartPadding(layoutDirection)
)
.width(80.dp)
.padding(horizontal = 12.dp),
containerColor = MaterialTheme.elevation()
containerColor = MaterialTheme.elevation(),
windowInsets = WindowInsets(0, 0, 0, 0)
) {
Column(
Modifier

View file

@ -12,5 +12,5 @@ sealed class MainEvent {
data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent()
data class OnChangeBackgroundColor(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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
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.GetDatastore
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.ui.DarkTheme
import ua.acclorite.book_story.ui.Theme
import ua.acclorite.book_story.ui.toDarkTheme
import ua.acclorite.book_story.ui.toTheme
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.DataStoreConstants
import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toTheme
import java.util.Locale
import javax.inject.Inject
@ -42,212 +41,143 @@ class MainViewModel @Inject constructor(
private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow()
/* -- Language ----------------------------------------------------- */
private var _language: String =
stateHandle[Constants.LANGUAGE]
?: if (
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)
/* --------------------------------------------------------------- */
private val _state: MutableStateFlow<MainState> = MutableStateFlow(
stateHandle[Constants.MAIN_STATE] ?: MainState()
)
val state = _state.asStateFlow()
fun onEvent(event: MainEvent) {
when (event) {
is MainEvent.OnChangeLanguage -> {
viewModelScope.launch(Dispatchers.Main) {
changeLanguage.execute(event.lang)
_language = event.lang
_state.updateWithSavedHandle {
it.copy(
language = event.lang
)
}
}
}
is MainEvent.OnChangeDarkTheme -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme)
_darkTheme = event.darkTheme.toDarkTheme()
_state.updateWithSavedHandle {
it.copy(
darkTheme = event.darkTheme.toDarkTheme()
)
}
}
}
is MainEvent.OnChangeTheme -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME, event.theme)
_theme = event.theme.toTheme()
_state.updateWithSavedHandle {
it.copy(
theme = event.theme.toTheme()
)
}
}
}
is MainEvent.OnChangeFontFamily -> {
viewModelScope.launch(Dispatchers.IO) {
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
)
}
}
}
is MainEvent.OnChangeFontStyle -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle)
_isItalic = event.fontStyle
_state.updateWithSavedHandle {
it.copy(
isItalic = event.fontStyle
)
}
}
}
is MainEvent.OnChangeFontSize -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize)
_fontSize = event.fontSize
_state.updateWithSavedHandle {
it.copy(
fontSize = event.fontSize
)
}
}
}
is MainEvent.OnChangeLineHeight -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight)
_lineHeight = event.lineHeight
_state.updateWithSavedHandle {
it.copy(
lineHeight = event.lineHeight
)
}
}
}
is MainEvent.OnChangeParagraphHeight -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight)
_paragraphHeight = event.paragraphHeight
_state.updateWithSavedHandle {
it.copy(
paragraphHeight = event.paragraphHeight
)
}
}
}
is MainEvent.OnChangeParagraphIndentation -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool)
_paragraphIndentation = event.bool
_state.updateWithSavedHandle {
it.copy(
paragraphIndentation = event.bool
)
}
}
}
is MainEvent.OnChangeBackgroundColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color)
_backgroundColor = event.color
_state.updateWithSavedHandle {
it.copy(
backgroundColor = event.color
)
}
}
}
is MainEvent.OnChangeFontColor -> {
viewModelScope.launch(Dispatchers.IO) {
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(
language,
theme,
darkTheme,
showStartScreen,
backgroundColor,
fontColor,
fontFamily,
isItalic,
fontSize,
lineHeight,
paragraphHeight,
paragraphIndentation
) { values ->
values.all { it != null }
_state
) {
val value = it.first()
return@combine value.language != null &&
value.theme != null &&
value.darkTheme != null &&
value.fontFamily != null &&
value.isItalic != null &&
value.fontSize != null &&
value.lineHeight != null &&
value.paragraphHeight != null &&
value.paragraphIndentation != null &&
value.backgroundColor != null &&
value.fontColor != null &&
value.showStartScreen != null
}
val isReady = combine(
@ -300,7 +232,14 @@ class MainViewModel @Inject constructor(
// Language
viewModelScope.launch(Dispatchers.Main) {
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 {
onEvent(MainEvent.OnChangeLanguage(it))
it.isNotBlank()
@ -310,7 +249,11 @@ class MainViewModel @Inject constructor(
// Theme
viewModelScope.launch(Dispatchers.IO) {
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 {
onEvent(MainEvent.OnChangeTheme(it))
it.isNotBlank()
@ -320,7 +263,7 @@ class MainViewModel @Inject constructor(
// Dark Theme
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.DARK_THEME, _darkTheme.toString())
.execute(DataStoreConstants.DARK_THEME, DarkTheme.FOLLOW_SYSTEM.name)
.first {
onEvent(MainEvent.OnChangeDarkTheme(it))
it.isNotBlank()
@ -330,11 +273,9 @@ class MainViewModel @Inject constructor(
// Show Start Screen
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.SHOW_START_SCREEN, _showStartScreen)
.execute(DataStoreConstants.SHOW_START_SCREEN, true)
.first {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, it)
_showStartScreen = it
onEvent(MainEvent.OnChangeShowStartScreen(it))
true
}
}
@ -342,7 +283,7 @@ class MainViewModel @Inject constructor(
// Background Color
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.BACKGROUND_COLOR, _backgroundColor)
.execute(DataStoreConstants.BACKGROUND_COLOR, Color.DarkGray.value.toLong())
.first {
onEvent(MainEvent.OnChangeBackgroundColor(it))
true
@ -352,7 +293,7 @@ class MainViewModel @Inject constructor(
// Font Color
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.FONT_COLOR, _fontColor)
.execute(DataStoreConstants.FONT_COLOR, Color.LightGray.value.toLong())
.first {
onEvent(MainEvent.OnChangeFontColor(it))
true
@ -362,7 +303,7 @@ class MainViewModel @Inject constructor(
// Font Family
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.FONT, _fontFamily)
.execute(DataStoreConstants.FONT, Constants.FONTS[0].id)
.first {
onEvent(MainEvent.OnChangeFontFamily(it))
true
@ -372,7 +313,7 @@ class MainViewModel @Inject constructor(
// Font Style
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.IS_ITALIC, _isItalic)
.execute(DataStoreConstants.IS_ITALIC, false)
.first {
onEvent(MainEvent.OnChangeFontStyle(it))
true
@ -382,7 +323,7 @@ class MainViewModel @Inject constructor(
// Font Size
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.FONT_SIZE, _fontSize)
.execute(DataStoreConstants.FONT_SIZE, 16)
.first {
onEvent(MainEvent.OnChangeFontSize(it))
it > 0
@ -392,7 +333,7 @@ class MainViewModel @Inject constructor(
// Line Height
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.LINE_HEIGHT, _lineHeight)
.execute(DataStoreConstants.LINE_HEIGHT, 4)
.first {
onEvent(MainEvent.OnChangeLineHeight(it))
true
@ -402,7 +343,7 @@ class MainViewModel @Inject constructor(
// Paragraph Height
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.PARAGRAPH_HEIGHT, _paragraphHeight)
.execute(DataStoreConstants.PARAGRAPH_HEIGHT, 8)
.first {
onEvent(MainEvent.OnChangeParagraphHeight(it))
true
@ -412,7 +353,7 @@ class MainViewModel @Inject constructor(
// Paragraph Indentation
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.PARAGRAPH_INDENTATION, _paragraphIndentation)
.execute(DataStoreConstants.PARAGRAPH_INDENTATION, false)
.first {
onEvent(MainEvent.OnChangeParagraphIndentation(it))
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.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.Modifier
@ -28,7 +29,7 @@ import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
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"
@ -56,12 +57,12 @@ enum class Screen {
START
}
@Immutable
data class Argument(
val key: String,
val arg: Any?
)
/**
* Navigator. Using for navigation between screens.
*
@ -181,7 +182,6 @@ class Navigator @AssistedInject constructor(
}
}
@AssistedFactory
interface Factory {
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
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
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.CustomSnackbar
import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection
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.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
@ -99,8 +98,8 @@ fun BookInfoScreen(
viewModel.onEvent(
BookInfoEvent.OnUpdateBook(
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
snackbarState,
context
@ -138,6 +137,7 @@ fun BookInfoScreen(
if (state.showMoveDialog) {
BookInfoMoveDialog(
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
viewModel = viewModel,
navigator = navigator
)
@ -158,7 +158,7 @@ fun BookInfoScreen(
isTopBarScrolled = null,
content1NavigationIcon = {
GoBackButton(navigator = navigator)
GoBackButton(navigator = navigator, enabled = !state.isRefreshing)
},
content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) {
@ -181,8 +181,8 @@ fun BookInfoScreen(
viewModel.onEvent(
BookInfoEvent.OnUpdateBook(
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
snackbarState,
context
@ -218,9 +218,8 @@ fun BookInfoScreen(
) {
viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
refreshList = {
libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
}
))
Toast.makeText(
@ -282,11 +281,9 @@ fun BookInfoScreen(
FloatingActionButton(
onClick = {
navigator.navigate(
Screen.READER, false, Argument(
"book", state.book.id
)
)
if (!state.isRefreshing) {
viewModel.onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator))
}
},
shape = MaterialTheme.shapes.large,
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.presentation.screens.book_info.data.BookInfoEvent
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.
@ -65,7 +65,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
modifier = Modifier
.height(195.dp)
.width(130.dp)
.clip(MaterialTheme.shapes.large)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation())
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
@ -82,7 +82,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large),
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
} else {

View file

@ -33,13 +33,11 @@ fun BookInfoStatisticSection(book: Book) {
.removeTrailingZero()
}%"
}
val description = if (book.progress < 1f) stringResource(
id = R.string.you_read_query,
progress
) + " " + stringResource(
if (book.progress > 0.2f) R.string.read_keep
val description = stringResource(
if (book.progress == 1f) R.string.read_done
else if (book.progress > 0.2f) R.string.read_keep
else R.string.read_more
) else stringResource(id = R.string.read_done)
)
Column(
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.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
/**
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
@ -58,8 +58,8 @@ fun BookInfoChangeCoverBottomSheet(
uri,
context,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
}
)
)
@ -98,12 +98,14 @@ fun BookInfoChangeCoverBottomSheet(
text = stringResource(id = R.string.delete_cover),
description = stringResource(id = R.string.delete_cover_desc)
) {
viewModel.onEvent(BookInfoEvent.OnDeleteCover(
viewModel.onEvent(
BookInfoEvent.OnDeleteCover(
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
}
))
)
)
Toast.makeText(
context,
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.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
import java.text.SimpleDateFormat
import java.util.Date
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.screens.book_info.data.BookInfoEvent
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.LibraryViewModel
@ -27,6 +29,7 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
@Composable
fun BookInfoMoveDialog(
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
viewModel: BookInfoViewModel,
navigator: Navigator
) {
@ -54,9 +57,8 @@ fun BookInfoMoveDialog(
viewModel.onEvent(
BookInfoEvent.OnMoveBook(
refreshList = {
libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))

View file

@ -4,26 +4,27 @@ import android.content.Context
import android.net.Uri
import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.data.Navigator
sealed class BookInfoEvent {
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
data object OnShowHideDetailsBottomSheet : BookInfoEvent()
data class OnDeleteCover(val refreshList: () -> Unit) : BookInfoEvent()
data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: () -> Unit) :
data class OnDeleteCover(val refreshList: (Book) -> Unit) : BookInfoEvent()
data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: (Book) -> Unit) :
BookInfoEvent()
data object OnShowHideEditTitle : BookInfoEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : 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 OnShowHideDeleteDialog : BookInfoEvent()
data object OnShowHideMoveDialog : BookInfoEvent()
data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent()
data class OnMoveBook(
val refreshList: () -> Unit,
val refreshList: (Book) -> Unit,
val updatePage: (Int) -> Unit,
val navigator: Navigator
) : BookInfoEvent()
@ -44,8 +45,12 @@ sealed class BookInfoEvent {
) : BookInfoEvent()
data class OnUpdateBook(
val refreshList: () -> Unit,
val refreshList: (Book) -> Unit,
val snackbarState: SnackbarHostState,
val context: Context
) : 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 ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.domain.util.Constants
@Immutable
data class BookInfoState(

View file

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

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.presentation.screens.browse
import android.Manifest
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
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.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(
ExperimentalMaterial3Api::class,
@ -79,6 +81,7 @@ fun BrowseScreen(
libraryViewModel: LibraryViewModel,
navigator: Navigator
) {
val context = LocalContext.current
val permissionState = rememberPermissionState(
permission = Manifest.permission.READ_EXTERNAL_STORAGE
)
@ -130,20 +133,13 @@ fun BrowseScreen(
content1Visibility = !state.hasSelectedItems && !state.showSearch,
content1NavigationIcon = {},
content1Title = {
Text(
stringResource(id = R.string.browse_screen),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
Text(stringResource(id = R.string.browse_screen))
},
content1Actions = {
CustomIconButton(
icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc),
disableOnClick = false,
enabled = !state.showSearch
disableOnClick = true
) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide)
}
@ -167,8 +163,6 @@ fun BrowseScreen(
id = R.string.selected_items_count_query,
state.selectedItemsCount.coerceAtLeast(1)
),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
@ -265,6 +259,16 @@ fun BrowseScreen(
modifier = Modifier
.animateItemPlacement(),
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 = {
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.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.Box
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.sp
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.ui.DefaultTransition
import ua.acclorite.book_story.util.Selected
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
@ -44,11 +45,14 @@ import java.util.Locale
/**
* Browse list element item. Can be selected.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BrowseFileItem(
file: Pair<File, Selected>,
hasSelectedFiles: Boolean,
modifier: Modifier, onClick: () -> Unit
modifier: Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
val lastModified = remember { pattern.format(Date(file.first.lastModified())) }
@ -85,9 +89,14 @@ fun BrowseFileItem(
.padding(horizontal = 8.dp, vertical = 3.dp)
.clip(RoundedCornerShape(10.dp))
.background(animatedBackgroundColor)
.clickable {
.combinedClickable(
onLongClick = {
onLongClick()
},
onClick = {
onClick()
}
)
.padding(horizontal = 8.dp, vertical = 7.dp)
) {
Row(

View file

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

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -23,8 +22,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.CustomCheckbox
import ua.acclorite.book_story.util.Selected
/**
* Adding dialog item.
@ -64,13 +63,6 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
}
}
} else {
val icon =
if (result.first.message?.asString() == stringResource(R.string.error_file_encrypted)) {
Icons.Default.Lock
} else {
Icons.Default.Error
}
Row(
Modifier
.fillMaxWidth()
@ -81,7 +73,7 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
imageVector = Icons.Default.Error,
contentDescription = stringResource(id = R.string.error_content_desc),
modifier = Modifier.size(26.dp),
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.PermissionState
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.util.Selected
import java.io.File
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.runtime.Immutable
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
@Immutable
@ -18,9 +18,6 @@ data class BrowseState(
val selectedItemsCount: Int = 0,
val hasSelectedItems: Boolean = false,
val scrollIndex: Int = 0,
val scrollOffset: Int = 0,
val showSearch: Boolean = false,
val searchQuery: String = "",
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.GetFilesFromDevice
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.Screen
import ua.acclorite.book_story.util.Resource
import javax.inject.Inject
@OptIn(ExperimentalPermissionsApi::class)
@ -41,6 +41,7 @@ class BrowseViewModel @Inject constructor(
val state = _state.asStateFlow()
private var job: Job? = null
private var job2: Job? = null
init {
viewModelScope.launch(Dispatchers.IO) {
@ -127,7 +128,8 @@ class BrowseViewModel @Inject constructor(
}
is BrowseEvent.OnRefreshList -> {
viewModelScope.launch(Dispatchers.IO) {
job2?.cancel()
job2 = viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
isRefreshing = true,
@ -169,20 +171,19 @@ class BrowseViewModel @Inject constructor(
is BrowseEvent.OnSelectFile -> {
viewModelScope.launch(Dispatchers.IO) {
val indexOfFile = _state.value.selectableFiles.indexOf(event.file)
if (indexOfFile == -1) {
return@launch
}
val editedList = _state.value.selectableFiles.toMutableList()
editedList[indexOfFile] = editedList[indexOfFile].copy(
second = !editedList[indexOfFile].second
val editedList = _state.value.selectableFiles.map {
if (event.file.first.path == it.first.path) {
it.copy(
second = !it.second
)
} else {
it
}
}
_state.update {
it.copy(
selectableFiles = editedList.toList(),
selectableFiles = editedList,
selectedItemsCount = editedList.filter { file -> file.second }.size,
hasSelectedItems = editedList.any { file -> file.second }
)
@ -267,7 +268,7 @@ class BrowseViewModel @Inject constructor(
)
}
job?.cancel()
job = viewModelScope.launch {
job = viewModelScope.launch(Dispatchers.IO) {
delay(500)
getFilesFromDownloads()
}
@ -352,6 +353,7 @@ class BrowseViewModel @Inject constructor(
_state.update {
it.copy(
isLoading = true,
hasSelectedItems = false,
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.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
import java.util.UUID
@OptIn(
@ -79,7 +79,6 @@ fun HistoryScreen(
) {
val context = LocalContext.current
val state by viewModel.state.collectAsState()
val books = libraryViewModel.state.collectAsState().value.books.map { it.first }
val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing,
onRefresh = {
@ -108,19 +107,14 @@ fun HistoryScreen(
content1NavigationIcon = {},
content1Title = {
Text(
stringResource(id = R.string.history_screen),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
stringResource(id = R.string.history_screen)
)
},
content1Actions = {
CustomIconButton(
icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc),
disableOnClick = false,
enabled = !state.showSearch
disableOnClick = true,
) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide)
}
@ -133,6 +127,7 @@ fun HistoryScreen(
enabled = !state.isLoading
&& !state.isRefreshing
&& state.history.isNotEmpty()
&& !state.showDeleteWholeHistoryDialog
) {
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
}
@ -214,7 +209,6 @@ fun HistoryScreen(
state = state.listState,
contentPadding = PaddingValues(vertical = 12.dp)
) {
if (!state.isLoading) {
state.history.forEachIndexed { index, groupedHistory ->
item(key = groupedHistory.title) {
if (index > 0) {
@ -230,32 +224,30 @@ fun HistoryScreen(
},
padding = 16.dp
)
Spacer(modifier = Modifier.height(8.dp))
}
items(
groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
) {
val book = remember {
books.find { book -> book.id == it.bookId }
} ?: return@items
HistoryItem(
modifier = Modifier.animateItemPlacement(),
history = it,
book = book,
isOnClickEnabled = !state.isRefreshing,
onBodyClick = {
navigator.navigate(
Screen.BOOK_INFO,
false,
Argument("book", book.id)
Argument("book", it.book)
)
},
onTitleClick = {
navigator.navigate(
Screen.READER,
false,
Argument("book", book.id)
viewModel.onEvent(
HistoryEvent.OnNavigateToReaderScreen(
navigator,
it.book
)
)
},
isDeleteEnabled = !state.isRefreshing,
@ -266,7 +258,9 @@ fun HistoryScreen(
snackbarState = snackbarState,
context = context,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
}
)
)
@ -276,10 +270,11 @@ fun HistoryScreen(
}
}
}
}
AnimatedVisibility(
visible = !state.isLoading && state.history.isEmpty() && !state.isRefreshing,
visible = !state.isLoading
&& state.history.isEmpty()
&& !state.isRefreshing,
modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn,
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.presentation.ui.elevation
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -46,7 +47,7 @@ import java.util.Locale
fun HistoryItem(
modifier: Modifier = Modifier,
history: History,
book: Book,
isOnClickEnabled: Boolean,
onBodyClick: () -> Unit,
onTitleClick: () -> Unit,
isDeleteEnabled: Boolean,
@ -61,7 +62,7 @@ fun HistoryItem(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.clickable {
.clickable(enabled = isOnClickEnabled) {
onBodyClick()
}
.padding(top = 6.dp, bottom = 6.dp, start = 16.dp, end = 10.dp)
@ -74,16 +75,16 @@ fun HistoryItem(
modifier = Modifier
.height(90.dp)
.width(60.dp)
.clip(MaterialTheme.shapes.medium)
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.elevation())
) {
if (book.coverImage != null) {
if (history.book?.coverImage != null) {
AsyncImage(
model = book.coverImage,
model = history.book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
.clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop
)
} else {
@ -108,14 +109,16 @@ fun HistoryItem(
modifier = Modifier.fillMaxHeight()
) {
Text(
book.title,
history.book?.title ?: return,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
lineHeight = 21.sp,
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = null,
indication = null,
enabled = isOnClickEnabled,
onClick = onTitleClick
),
maxLines = 2,
@ -135,7 +138,7 @@ fun HistoryItem(
}
Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) {
IconButton(
enabled = isDeleteEnabled,
enabled = isDeleteEnabled && isOnClickEnabled,
onClick = {
onDeleteClick()
}

View file

@ -3,7 +3,9 @@ package ua.acclorite.book_story.presentation.screens.history.data
import android.content.Context
import androidx.compose.material3.SnackbarHostState
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.presentation.data.Navigator
sealed class HistoryEvent {
data object OnRefreshList : HistoryEvent()
@ -20,4 +22,9 @@ sealed class HistoryEvent {
data class OnSearchQueryChange(val query: String) : HistoryEvent()
data object OnSearchShowHide : 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 searchQuery: String = "",
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.GetHistory
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.util.Calendar
import java.util.Date
import java.util.Locale
import javax.inject.Inject
@ -40,6 +43,7 @@ class HistoryViewModel @Inject constructor(
private var job: Job? = null
private var job2: Job? = null
private var job3: Job? = null
init {
viewModelScope.launch(Dispatchers.IO) {
@ -55,7 +59,8 @@ class HistoryViewModel @Inject constructor(
fun onEvent(event: HistoryEvent) {
when (event) {
is HistoryEvent.OnRefreshList -> {
viewModelScope.launch(Dispatchers.IO) {
job3?.cancel()
job3 = viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
isRefreshing = true,
@ -115,6 +120,7 @@ class HistoryViewModel @Inject constructor(
deleteHistory.execute(
listOf(event.historyToDelete)
)
onEvent(HistoryEvent.OnRefreshList)
event.refreshList()
@ -172,7 +178,7 @@ class HistoryViewModel @Inject constructor(
)
}
job?.cancel()
job = viewModelScope.launch {
job = viewModelScope.launch(Dispatchers.IO) {
delay(500)
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 ->
when (result) {
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 {
it.copy(
history = emptyList(),
@ -223,8 +278,26 @@ class HistoryViewModel @Inject constructor(
}
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>()
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.domain.model.Book
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.CustomIconButton
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.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Selected
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
import java.util.UUID
@OptIn(
@ -98,8 +98,7 @@ fun LibraryScreen(
viewModel: LibraryViewModel,
browseViewModel: BrowseViewModel,
historyViewModel: HistoryViewModel,
navigator: Navigator,
addedBooks: List<Book>
navigator: Navigator
) {
val state by viewModel.state.collectAsState()
val pagerState = rememberPagerState(
@ -114,21 +113,21 @@ fun LibraryScreen(
)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if (addedBooks.isNotEmpty()) {
viewModel.onEvent(LibraryEvent.OnPreloadBooks(addedBooks))
navigator.clearArgument("added_books")
}
}
LaunchedEffect(Unit) {
viewModel.onEvent(LibraryEvent.OnScrollToPage(state.currentPage, pagerState))
}
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != state.currentPage) {
viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
}
}
if (state.showMoveDialog) {
LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState)
LibraryMoveDialog(
viewModel = viewModel,
historyViewModel = historyViewModel,
pagerState = pagerState
)
}
if (state.showDeleteDialog) {
LibraryDeleteDialog(
@ -156,13 +155,7 @@ fun LibraryScreen(
content1NavigationIcon = {},
content1Title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
stringResource(id = R.string.library_screen),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
Text(stringResource(id = R.string.library_screen))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = state.books.size.toString(),
@ -179,8 +172,7 @@ fun LibraryScreen(
CustomIconButton(
icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc),
disableOnClick = false,
enabled = !state.showSearch
disableOnClick = true,
) {
viewModel.onEvent(LibraryEvent.OnSearchShowHide)
}
@ -204,8 +196,6 @@ fun LibraryScreen(
id = R.string.selected_items_count_query,
state.selectedItemsCount.coerceAtLeast(1)
),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
@ -216,7 +206,11 @@ fun LibraryScreen(
contentDescription = stringResource(
id = R.string.move_books_content_desc
),
disableOnClick = true
enabled = !state.isLoading
&& !state.isRefreshing
&& !state.showMoveDialog,
disableOnClick = false,
) {
viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog)
}
@ -225,7 +219,10 @@ fun LibraryScreen(
contentDescription = stringResource(
id = R.string.delete_books_content_desc
),
disableOnClick = true
enabled = !state.isLoading
&& !state.isRefreshing
&& !state.showDeleteDialog,
disableOnClick = false
) {
viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog)
}
@ -310,7 +307,9 @@ fun LibraryScreen(
LaunchedEffect(state.books) {
categorizedBooks.clear()
categorizedBooks.addAll(state.books.filter { it.first.category == category }
categorizedBooks.addAll(
state.books
.filter { it.first.category == category }
.sortedWith(
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
.thenBy { it.first.title }
@ -323,8 +322,7 @@ fun LibraryScreen(
DefaultTransition(visible = !state.isLoading && !categoryIsLoading) {
LazyVerticalGrid(
columns = GridCells.Adaptive(120.dp),
modifier = Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp)
) {
items(
@ -341,7 +339,7 @@ fun LibraryScreen(
navigator.navigate(
Screen.BOOK_INFO,
false,
Argument("book", it.first.id)
Argument("book", it.first)
)
}
},
@ -351,10 +349,11 @@ fun LibraryScreen(
}
},
onButtonClick = {
navigator.navigate(
Screen.READER,
false,
Argument("book", it.first.id)
viewModel.onEvent(
LibraryEvent.OnNavigateToReaderScreen(
navigator,
it.first
)
)
}
)
@ -363,7 +362,9 @@ fun LibraryScreen(
}
AnimatedVisibility(
visible = !state.isLoading && !state.isRefreshing && categorizedBooks.isEmpty()
visible = !state.isLoading
&& !state.isRefreshing
&& categorizedBooks.isEmpty()
&& !categoryIsLoading,
modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn,

View file

@ -37,10 +37,10 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R
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.removeTrailingZero
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Selected
import ua.acclorite.book_story.presentation.ui.elevation
/**
* Library list element item.
@ -93,7 +93,7 @@ fun LibraryBookItem(
modifier = Modifier
.aspectRatio(1f / 1.5f)
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation())
.combinedClickable(
onClick = {
@ -110,7 +110,7 @@ fun LibraryBookItem(
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large),
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
} 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.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -13,7 +14,6 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
@ -22,7 +22,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.unit.dp
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.presentation.screens.library.data.LibraryEvent
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.
@ -60,83 +60,34 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: Pa
)
}
if (LocalConfiguration.current.screenWidthDp > 450) {
TabRow(
Box(Modifier.fillMaxWidth()) {
Divider(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage,
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.surfaceVariant
)
),
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(
modifier = Modifier
.fillMaxWidth(),
.fillMaxWidth()
.padding(bottom = 0.5.dp),
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surface,
divider = {
Divider(color = MaterialTheme.colorScheme.surfaceVariant)
},
containerColor = Color.Transparent,
edgePadding = 0.dp,
divider = {},
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
.padding(horizontal = 16.dp)
.padding(end = 5.5.dp)
.clip(
RoundedCornerShape(
topStartPercent = 100,
topEndPercent = 100
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.presentation.components.custom_dialog.CustomDialogWithLazyColumn
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.LibraryViewModel
@ -23,7 +25,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
fun LibraryMoveDialog(
viewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
pagerState: PagerState
) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current
@ -38,7 +44,14 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
isActionEnabled = true,
onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) },
onAction = {
viewModel.onEvent(LibraryEvent.OnMoveBooks(pagerState))
viewModel.onEvent(
LibraryEvent.OnMoveBooks(
pagerState,
refreshList = {
historyViewModel.onEvent(HistoryEvent.OnLoadList)
}
)
)
Toast.makeText(
context,
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 ua.acclorite.book_story.domain.model.Book
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 {
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
data object OnRefreshList : LibraryEvent()
data object OnLoadList : LibraryEvent()
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
@ -24,8 +24,12 @@ sealed class LibraryEvent {
data object OnClearSelectedBooks : LibraryEvent()
data object OnShowHideMoveDialog : 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 class OnDeleteBooks(val refreshList: () -> Unit) : 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 ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Selected
import ua.acclorite.book_story.domain.util.Selected
@Immutable
data class LibraryState(
@ -25,6 +25,4 @@ data class LibraryState(
val selectedCategory: Category = Category.READING,
val showDeleteDialog: Boolean = false,
)
)

View file

@ -12,10 +12,15 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
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.GetBooks
import ua.acclorite.book_story.domain.use_case.InsertHistory
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
@OptIn(ExperimentalFoundationApi::class)
@ -23,7 +28,8 @@ import javax.inject.Inject
class LibraryViewModel @Inject constructor(
private val getBooks: GetBooks,
private val updateBooks: UpdateBooks,
private val deleteBooks: DeleteBooks
private val deleteBooks: DeleteBooks,
private val insertHistory: InsertHistory
) : ViewModel() {
private val _state = MutableStateFlow(LibraryState())
@ -33,6 +39,7 @@ class LibraryViewModel @Inject constructor(
val isReady = _isReady.asStateFlow()
private var job: Job? = null
private var job2: Job? = null
init {
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 -> {
viewModelScope.launch(Dispatchers.IO) {
job2?.cancel()
job2 = viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
isRefreshing = true,
@ -101,7 +101,8 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
isLoading = true
isLoading = true,
hasSelectedItems = false
)
}
getBooksFromDatabase()
@ -149,7 +150,7 @@ class LibraryViewModel @Inject constructor(
)
}
job?.cancel()
job = viewModelScope.launch {
job = viewModelScope.launch(Dispatchers.IO) {
delay(500)
getBooksFromDatabase()
}
@ -157,20 +158,19 @@ class LibraryViewModel @Inject constructor(
is LibraryEvent.OnSelectBook -> {
viewModelScope.launch(Dispatchers.IO) {
val indexOfBook = _state.value.books.indexOf(event.book)
if (indexOfBook == -1) {
return@launch
}
val editedList = _state.value.books.toMutableList()
editedList[indexOfBook] = editedList[indexOfBook].copy(
second = event.select ?: !editedList[indexOfBook].second
val editedList = _state.value.books.map {
if (it.first.id == event.book.first.id) {
it.copy(
second = event.select ?: !it.second
)
} else {
it
}
}
_state.update {
it.copy(
books = editedList.toList(),
books = editedList,
selectedItemsCount = editedList.filter { book -> book.second }.size,
hasSelectedItems = editedList.any { book -> book.second }
)
@ -234,6 +234,7 @@ class LibraryViewModel @Inject constructor(
updateBooks.execute(books)
getBooksFromDatabase()
event.refreshList()
onEvent(LibraryEvent.OnClearSelectedBooks)
onEvent(
@ -278,14 +279,16 @@ class LibraryViewModel @Inject constructor(
is LibraryEvent.OnUpdateBook -> {
viewModelScope.launch(Dispatchers.IO) {
val books = _state.value.books.toMutableList()
val index = books.indexOfFirst { it.first.id == event.book.id }
if (index == -1) {
return@launch
val books = _state.value.books.map {
if (it.first.id == event.book.id) {
it.copy(
first = event.book
)
} else {
it
}
}
books[index] = Pair(event.book, books[index].second)
_state.update {
it.copy(
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -54,6 +55,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.is_messages.IsError
import ua.acclorite.book_story.presentation.data.MainViewModel
@ -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.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(FlowPreview::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -86,8 +87,10 @@ fun ReaderScreen(
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
var loading by remember { mutableStateOf(true) }
val state by viewModel.state.collectAsState()
val listState = rememberLazyListState()
val state by viewModel.state.collectAsState()
val canScrollForward by remember { derivedStateOf { listState.canScrollForward } }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPostScroll(
@ -106,47 +109,45 @@ fun ReaderScreen(
}
}
val fontFamily = Constants.FONTS.find {
it.id == mainViewModel.fontFamily.collectAsState().value!!
val mainState by mainViewModel.state.collectAsState()
val fontFamily = remember(mainState.fontFamily) {
Constants.FONTS.find {
it.id == mainState.fontFamily
} ?: 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) {
viewModel.init(
navigator,
context,
listState,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnLoadList)
},
listState = listState,
onLoaded = {
loading = false
}
)
}
LaunchedEffect(canScrollForward) {
if (!canScrollForward && !state.showMenu && !loading) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
}
}
LaunchedEffect(listState) {
snapshotFlow {
listState.firstVisibleItemIndex
}.debounce(1000).collectLatest {
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(500).collectLatest { items ->
if (!loading) {
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (it > 0) {
val progress = if (items.first > 0) {
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
1f
} else {
(it.toFloat() / (state.book.text.lastIndex).toFloat())
(items.first.toFloat() / (state.book.text.lastIndex).toFloat())
}
} else {
0f
@ -154,14 +155,13 @@ fun ReaderScreen(
viewModel.onEvent(
ReaderEvent.OnChangeProgress(
progress,
navigator,
progress = progress,
navigator = navigator,
firstVisibleItemIndex = items.first,
firstVisibleItemOffset = items.second,
refreshList = { book ->
libraryViewModel.onEvent(
LibraryEvent.OnUpdateBook(
book
)
)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
}
)
)
@ -186,7 +186,10 @@ fun ReaderScreen(
) {
ReaderTopBar(
viewModel = viewModel,
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
navigator = navigator,
listState = listState,
containerColor = systemBarsColor
)
}
@ -200,8 +203,9 @@ fun ReaderScreen(
ReaderBottomBar(
viewModel = viewModel,
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
listState = listState,
navigator = navigator,
scrollState = listState,
systemBarsColor = systemBarsColor
)
}
@ -280,36 +284,44 @@ fun ReaderScreen(
itemsIndexed(
state.book.text, key = { _, key -> key.id }
) { index, line ->
val text =
remember(paragraphIndentation) { "${if (paragraphIndentation) " " else ""}${line.line}" }
val fontCol = remember(fontColor) { Color(fontColor.toULong()) }
val backgroundCol =
remember(backgroundColor) { Color(backgroundColor.toULong()) }
val lineHeightSp = remember(fontSize, lineHeight) { (fontSize + lineHeight).sp }
val text = remember(mainState.paragraphIndentation) {
"${if (mainState.paragraphIndentation!!) " " else ""}${line.line}"
}
val fontColor = remember(mainState.fontColor) {
Color(mainState.fontColor!!.toULong())
}
val backgroundColor = remember(mainState.backgroundColor) {
Color(mainState.backgroundColor!!.toULong())
}
val lineHeight = remember(mainState.fontSize, mainState.lineHeight) {
(mainState.fontSize!! + mainState.lineHeight!!).sp
}
Column(
Modifier
.background(backgroundCol)
.background(backgroundColor)
.fillMaxWidth()
.padding(
top = if (index == 0) 36.dp else 0.dp,
top = if (index == 0) 18.dp else 0.dp,
start = 18.dp,
end = 18.dp,
bottom = if (index == state.book.text.lastIndex) 36.dp
else (paragraphHeight * 3).dp
bottom = if (index == state.book.text.lastIndex) 18.dp
else (mainState.paragraphHeight!! * 3).dp
)
) {
Text(
text = text,
color = fontCol,
color = fontColor,
style = TextStyle(
lineBreak = LineBreak.Paragraph
),
fontFamily = fontFamily.font,
fontStyle = fontStyle,
fontSize = fontSize.sp,
lineHeight = lineHeightSp
fontStyle = when (mainState.isItalic!!) {
true -> FontStyle.Italic
false -> FontStyle.Normal
},
fontSize = mainState.fontSize!!.sp,
lineHeight = lineHeight
)
}
}
@ -319,7 +331,9 @@ fun ReaderScreen(
DisableSelection {
ReaderEndItem(
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
viewModel = viewModel,
listState = listState,
navigator = navigator
)
}
@ -343,10 +357,21 @@ fun ReaderScreen(
icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.go_back),
action = {
navigator.navigateBack()
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList)
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
viewModel.onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
it.navigateBack()
}
)
)
}
)
} else {
@ -369,7 +394,19 @@ fun ReaderScreen(
}
BackHandler {
navigator.navigateBack()
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
viewModel.onEvent(
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.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
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.presentation.data.Navigator
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.LibraryViewModel
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
fun ReaderEndItem(
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
viewModel: ReaderViewModel,
listState: LazyListState,
navigator: Navigator
) {
val state by viewModel.state.collectAsState()
@ -54,12 +59,11 @@ fun ReaderEndItem(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.windowInsetsPadding(WindowInsets.navigationBarsIgnoringVisibility)
.padding(
top = 24.dp,
start = 24.dp,
end = 24.dp,
bottom = 8.dp
horizontal = 36.dp,
vertical = 108.dp + WindowInsets.navigationBarsIgnoringVisibility
.asPaddingValues()
.calculateBottomPadding()
),
horizontalAlignment = Alignment.CenterHorizontally
) {
@ -72,7 +76,11 @@ fun ReaderEndItem(
)
Spacer(modifier = Modifier.height(4.dp))
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,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
@ -83,15 +91,15 @@ fun ReaderEndItem(
if (state.book.category != Category.ALREADY_READ) {
viewModel.onEvent(
ReaderEvent.OnMoveBookToAlreadyRead(
refreshList = {
libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
onUpdateCategories = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))
},
navigator = navigator,
listState = listState,
context = context
)
)
@ -103,11 +111,19 @@ fun ReaderEndItem(
).show()
} else {
viewModel.onEvent(
ReaderEvent.OnShowSystemBars(
context
ReaderEvent.OnGoBack(
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),

View file

@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image
import androidx.compose.material3.Icon
@ -34,7 +35,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R
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.
@ -49,7 +50,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility)
.padding(24.dp)
.padding(horizontal = 24.dp, vertical = 80.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
@ -59,7 +60,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
modifier = Modifier
.height(135.dp)
.width(90.dp)
.clip(MaterialTheme.shapes.medium)
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.elevation())
) {
if (state.book.coverImage != null) {
@ -68,7 +69,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
.clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop
)
} 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.removeDigits
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
@ -36,8 +38,9 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
fun ReaderBottomBar(
viewModel: ReaderViewModel,
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
listState: LazyListState,
navigator: Navigator,
scrollState: LazyListState,
systemBarsColor: Color
) {
val state by viewModel.state.collectAsState()
@ -74,20 +77,19 @@ fun ReaderBottomBar(
Slider(
value = state.book.progress,
onValueChange = {
viewModel.onEvent(ReaderEvent.OnScroll(listState, it))
viewModel.onEvent(
ReaderEvent.OnChangeProgress(
progress = it,
navigator = navigator,
firstVisibleItemIndex = listState.firstVisibleItemIndex,
firstVisibleItemOffset = 0,
refreshList = { book ->
libraryViewModel.onEvent(
LibraryEvent.OnUpdateBook(
book
)
)
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
}
)
)
viewModel.onEvent(ReaderEvent.OnScroll(scrollState, it))
},
colors = SliderDefaults.colors(
activeTrackColor = MaterialTheme.colorScheme.primary,

View file

@ -5,7 +5,9 @@ import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
@ -26,12 +28,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R
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.Screen
import ua.acclorite.book_story.presentation.data.removeDigits
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.ReaderViewModel
@ -40,7 +44,14 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
*/
@OptIn(ExperimentalMaterial3Api::class)
@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 state by viewModel.state.collectAsState()
@ -56,8 +67,25 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
TopAppBar(
navigationIcon = {
GoBackButton(navigator = navigator) {
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
CustomIconButton(
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 = {
@ -74,12 +102,21 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
interactionSource = null,
indication = null,
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,
true,
Argument(
"book",
state.book.id
true
)
}
)
)
}

View file

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

View file

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

View file

@ -2,17 +2,13 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.UIText
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.UIText
@Immutable
data class ReaderState(
val book: Book = Constants.EMPTY_BOOK,
val letters: Int = 0,
val words: Int = 0,
val errorMessage: UIText? = null,
val showMenu: Boolean = false,
val showSettingsBottomSheet: Boolean = false,
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.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.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.InsertHistory
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.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.util.UIText
import java.util.Date
import javax.inject.Inject
import kotlin.math.roundToInt
@ -40,9 +37,8 @@ import kotlin.math.roundToInt
@HiltViewModel
class ReaderViewModel @Inject constructor(
private val updateBooks: UpdateBooks,
private val insertHistory: InsertHistory,
private val getText: GetText,
private val getBooksById: GetBooksById
private val getLatestHistory: GetLatestHistory
) : ViewModel() {
private val _state = MutableStateFlow(ReaderState())
@ -67,7 +63,6 @@ class ReaderViewModel @Inject constructor(
event.onTextIsEmpty()
}
val time = Date().time
val letters = text
.replace("\n", "")
.length
@ -76,50 +71,46 @@ class ReaderViewModel @Inject constructor(
.split("\\s+".toRegex())
.size
val history = _state.value.book.id?.let {
getLatestHistory.execute(
it
)
}
_state.update {
it.copy(
book = it.book.copy(
text = text
.split("\n")
.map { line -> StringWithId(line.trim()) },
lastOpened = time
),
lastOpened = history?.time,
letters = letters,
words = words
)
)
}
updateBooks.execute(
listOf(_state.value.book)
)
insertHistory.execute(
listOf(
History(
null,
_state.value.book.id!!,
time
)
)
)
event.refreshList(_state.value.book)
event.navigator.putArgument(
Argument("book", _state.value.book.id)
)
viewModelScope.launch {
snapshotFlow {
event.scrollState.layoutInfo.totalItemsCount
event.listState.layoutInfo.totalItemsCount
}.collectLatest { itemsCount ->
val scrollTo =
(_state.value.book.text.size * _state.value.book.progress)
.toInt()
val index = _state.value.book.scrollIndex
val offset = _state.value.book.scrollOffset
if (itemsCount >= _state.value.book.text.size) {
if (scrollTo > 0) {
if (index > 0 || offset > 0) {
var loaded = false
for (i in 1..100) {
try {
event.scrollState.scrollToItem(scrollTo)
event.listState.scrollToItem(
index,
offset
)
loaded = true
break
} catch (e: Exception) {
@ -143,6 +134,7 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnShowHideMenu -> {
val shouldShow = event.show ?: !_state.value.showMenu
val insetsController = WindowCompat.getInsetsController(
event.context.window,
event.context.window.decorView
@ -166,20 +158,57 @@ class ReaderViewModel @Inject constructor(
}
}
is ReaderEvent.OnShowSystemBars -> {
is ReaderEvent.OnGoBack -> {
viewModelScope.launch {
val insetsController = WindowCompat.getInsetsController(
event.context.window,
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())
event.navigate(event.navigator)
}
}
is ReaderEvent.OnScroll -> {
viewModelScope.launch {
val scrollTo = (_state.value.book.text.size * event.progress).roundToInt()
event.scrollState.scrollToItem(
event.listState.scrollToItem(
scrollTo
)
}
@ -190,7 +219,9 @@ class ReaderViewModel @Inject constructor(
_state.update {
it.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)
)
event.navigator.putArgument(
Argument("book", _state.value.book.id)
Argument(
"book",
_state.value.book
)
)
event.refreshList(_state.value.book)
}
}
is ReaderEvent.OnShowHideSettingsBottomSheet -> {
viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0,
@ -213,6 +248,7 @@ class ReaderViewModel @Inject constructor(
)
}
}
}
is ReaderEvent.OnScrollToSettingsPage -> {
viewModelScope.launch {
@ -222,16 +258,32 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnMoveBookToAlreadyRead -> {
viewModelScope.launch {
onEvent(ReaderEvent.OnShowSystemBars(event.context))
val book = _state.value.book.copy(
category = Category.ALREADY_READ,
progress = 1f
onEvent(
ReaderEvent.OnGoBack(
event.context,
event.navigator,
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(
Category.entries.dropLastWhile {
it != Category.ALREADY_READ
@ -298,30 +350,30 @@ class ReaderViewModel @Inject constructor(
fun init(
navigator: Navigator,
context: ComponentActivity,
scrollState: LazyListState,
listState: LazyListState,
refreshList: (Book) -> Unit,
onLoaded: () -> Unit
) {
viewModelScope.launch {
val bookIndex = navigator.retrieveArgument("book") as? Int
viewModelScope.launch(Dispatchers.IO) {
val book = navigator.retrieveArgument("book") as? Book
if (bookIndex == null || bookIndex < 0) {
if (book == null) {
navigator.navigateBack()
return@launch
}
val book = getBooksById.execute(listOf(bookIndex)).first()
viewModelScope.launch {
onEvent(ReaderEvent.OnShowHideMenu(false, context))
}
_state.update {
ReaderState(book = book)
}
onEvent(ReaderEvent.OnShowHideMenu(false, context))
onEvent(
ReaderEvent.OnLoadText(
scrollState,
navigator,
refreshList = { refreshList(it) },
listState = listState,
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.Screen
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)
@Composable
@ -39,7 +39,7 @@ fun SettingsScreen(
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward
listState.canScrollForward || listState.canScrollBackward
}
)
@ -71,6 +71,10 @@ fun SettingsScreen(
.padding(top = paddingValues.calculateTopPadding()),
state = listState
) {
item {
Spacer(modifier = Modifier.height(16.dp))
}
item {
SettingsCategoryItem(
icon = Icons.Default.DisplaySettings,

View file

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

View file

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

View file

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

View file

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

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

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.os.Build
@ -22,11 +22,7 @@ enum class DarkTheme {
}
fun String.toDarkTheme(): DarkTheme {
return when (this) {
"OFF" -> DarkTheme.OFF
"ON" -> DarkTheme.ON
else -> DarkTheme.FOLLOW_SYSTEM
}
return DarkTheme.valueOf(this)
}
@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.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.ui.text.TextStyle

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