0.9.3 - New CoverImage storage system(saving to app's internal storage) + Bug fixes + Various optimisations(such as initializing all ViewModels in Activity.kt).

This commit is contained in:
acclorite 2024-03-16 17:21:44 +02:00
parent 22ed828ee6
commit 4e948bc5d7
44 changed files with 608 additions and 470 deletions

View file

@ -15,7 +15,7 @@ android {
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.9.2"
versionName = "0.9.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View file

@ -30,6 +30,7 @@ import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.NavigationHost
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen
@ -37,6 +38,7 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode
import ua.acclorite.book_story.presentation.screens.library.LibraryScreen
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreen
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
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
@ -57,7 +59,8 @@ class Activity : AppCompatActivity() {
private val libraryViewModel: LibraryViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels()
private val browseViewModel: BrowseViewModel by viewModels()
private val bookInfoViewModel: BookInfoViewModel by viewModels()
private val readerViewModel: ReaderViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -87,12 +90,14 @@ class Activity : AppCompatActivity() {
setContent {
val windowClass = calculateWindowSizeClass(activity = this)
val isLoaded by mainViewModel.isReady.collectAsState()
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
val darkTheme =
mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM
val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact
if (isLoaded) {
BooksHistoryResurrectionTheme(
theme = theme,
isDark = darkTheme.isDark()
@ -165,6 +170,7 @@ class Activity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
BookInfoScreen(
viewModel = bookInfoViewModel,
libraryViewModel = libraryViewModel,
browseViewModel = browseViewModel,
historyViewModel = historyViewModel,
@ -177,6 +183,7 @@ class Activity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
ReaderScreen(
viewModel = readerViewModel,
mainViewModel = mainViewModel,
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
@ -236,3 +243,4 @@ class Activity : AppCompatActivity() {
}
}
}
}

View file

@ -10,6 +10,10 @@ import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.FileParserImpl
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.parser.TextParserImpl
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Singleton
@ -40,4 +44,16 @@ abstract class RepositoryModule {
abstract fun bindHistoryMapper(
historyMapperImpl: HistoryMapperImpl
): HistoryMapper
@Binds
@Singleton
abstract fun bindFileParser(
fileParserImpl: FileParserImpl
): FileParser
@Binds
@Singleton
abstract fun bindTextParser(
textParserImpl: TextParserImpl
): TextParser
}

View file

@ -13,19 +13,6 @@ data class BookEntity(
val text: String,
val filePath: String,
val progress: Float,
val image: ByteArray = byteArrayOf(),
val image: String? = null,
val category: Category
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BookEntity
return image.contentEquals(other.image)
}
override fun hashCode(): Int {
return image.contentHashCode()
}
}
)

View file

@ -1,31 +1,15 @@
package ua.acclorite.book_story.data.mapper.book
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import 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 java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
class BookMapperImpl @Inject constructor() : BookMapper {
override suspend fun toBookEntity(book: Book): BookEntity {
val legacyAPI = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val stream = ByteArrayOutputStream()
book.coverImage?.asAndroidBitmap()?.compress(
if (legacyAPI) Bitmap.CompressFormat.WEBP
else Bitmap.CompressFormat.WEBP_LOSSY,
0,
stream
)
val textAsString = book.text.joinToString("\n") {
it.line.trim()
}
@ -38,21 +22,13 @@ class BookMapperImpl @Inject constructor() : BookMapper {
author = book.author.string,
text = textAsString,
description = book.description,
image = stream.toByteArray(),
image = if (book.coverImage != null) book.coverImage.toString() else null,
category = book.category
)
}
override suspend fun toBook(bookEntity: BookEntity): Book {
val file = File(bookEntity.filePath)
var image: ImageBitmap? = null
if (bookEntity.image.isNotEmpty()) {
image = BitmapFactory
.decodeByteArray(bookEntity.image, 0, bookEntity.image.size)
.asImageBitmap()
image.prepareToDraw()
}
return Book(
id = bookEntity.id,
@ -67,7 +43,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
filePath = bookEntity.filePath,
lastOpened = null,
category = bookEntity.category,
coverImage = image
coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null
)
}
}

View file

@ -1,9 +1,11 @@
package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.CoverImage
import java.io.File
interface FileParser {
suspend fun parse(file: File): Book?
suspend fun parse(file: File): Pair<Book, CoverImage?>?
}

View file

@ -0,0 +1,41 @@
package ua.acclorite.book_story.data.parser
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 java.io.File
import javax.inject.Inject
class FileParserImpl @Inject constructor(
private val txtFileParser: TxtFileParser,
private val pdfFileParser: PdfFileParser,
private val epubFileParser: EpubFileParser
) : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
val fileFormat = ".${file.name.substringAfterLast(".")}"
if (fileFormat == ".pdf") {
return pdfFileParser.parse(file)
}
if (fileFormat == ".epub") {
return epubFileParser.parse(file)
}
if (fileFormat == ".txt") {
return txtFileParser.parse(file)
}
return null
}
}

View file

@ -0,0 +1,43 @@
package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.R
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 java.io.File
import javax.inject.Inject
class TextParserImpl @Inject constructor(
private val txtTextParser: TxtTextParser,
private val pdfTextParser: PdfTextParser,
private val epubTextParser: EpubTextParser,
) : TextParser {
override suspend fun parse(file: File): Resource<List<StringWithId>> {
val fileFormat = ".${file.name.substringAfterLast(".")}"
if (fileFormat == ".pdf") {
return pdfTextParser.parse(file)
}
if (fileFormat == ".epub") {
return epubTextParser.parse(file)
}
if (fileFormat == ".txt") {
return txtTextParser.parse(file)
}
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
}
}

View file

@ -1,13 +1,13 @@
package ua.acclorite.book_story.data.parser.epub
import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.asImageBitmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText
import java.io.File
import java.io.FileInputStream
@ -15,7 +15,7 @@ import javax.inject.Inject
class EpubFileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Book? {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
if (!file.name.endsWith(".epub")) {
return null
}
@ -23,7 +23,9 @@ class EpubFileParser @Inject constructor() : FileParser {
try {
val epubReader = EpubReader()
val book = withContext(Dispatchers.IO) {
epubReader.readEpub(FileInputStream(file))
FileInputStream(file).use {
epubReader.readEpub(it)
}
}
val metadata = book.metadata
@ -42,8 +44,6 @@ class EpubFileParser @Inject constructor() : FileParser {
}
}
return Book(
id = null,
title = title,
@ -55,8 +55,8 @@ class EpubFileParser @Inject constructor() : FileParser {
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = coverImage?.asImageBitmap()
)
coverImage = null
) to coverImage
} catch (e: Exception) {
return null
}

View file

@ -8,13 +8,14 @@ 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 java.io.File
import javax.inject.Inject
class PdfFileParser @Inject constructor(private val application: Application) : FileParser {
override suspend fun parse(file: File): Book? {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
if (!file.name.endsWith(".pdf")) {
return null
}
@ -46,7 +47,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
lastOpened = null,
category = Category.entries[0],
coverImage = null
)
) to null
} catch (e: Exception) {
return null
}

View file

@ -5,13 +5,14 @@ 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 java.io.File
import javax.inject.Inject
class TxtFileParser @Inject constructor() : FileParser {
override suspend fun parse(file: File): Book? {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
if (!file.name.endsWith(".txt")) {
return null
}
@ -33,7 +34,7 @@ class TxtFileParser @Inject constructor() : FileParser {
lastOpened = null,
category = Category.entries[0],
coverImage = null
)
) to null
} catch (e: Exception) {
return null
}

View file

@ -1,5 +1,8 @@
package ua.acclorite.book_story.data.repository
import android.app.Application
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow
@ -10,12 +13,8 @@ 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
import ua.acclorite.book_story.data.parser.epub.EpubFileParser
import ua.acclorite.book_story.data.parser.epub.EpubTextParser
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
@ -23,27 +22,25 @@ import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.presentation.data.calculateFamiliarity
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.Resource
import ua.acclorite.book_story.util.UIText
import java.io.File
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BookRepositoryImpl @Inject constructor(
private val application: Application,
private val database: BookDao,
private val dataStore: DataStore,
private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper,
private val txtFileParser: TxtFileParser,
private val pdfFileParser: PdfFileParser,
private val epubFileParser: EpubFileParser,
private val txtTextParser: TxtTextParser,
private val pdfTextParser: PdfTextParser,
private val epubTextParser: EpubTextParser,
private val fileParser: FileParser,
private val textParser: TextParser
) : BookRepository {
@ -101,12 +98,46 @@ class BookRepositoryImpl @Inject constructor(
return database.findBookById(id)
}
override suspend fun insertBooks(books: List<Book>) {
database.insertBooks(books.map { bookMapper.toBookEntity(it) })
override suspend fun insertBooks(
books: List<Pair<Book, CoverImage?>>
) {
val booksWithCover = books.map {
var uri = ""
if (it.second != null) {
try {
uri = "${UUID.randomUUID()}.webp"
application.openFileOutput(uri, 0x0000).use { stream ->
if (
!it.second!!.compress(
Bitmap.CompressFormat.WEBP,
30,
stream
)
) {
throw Exception("Couldn't save cover image")
}
}
} catch (e: Exception) {
uri = ""
e.printStackTrace()
}
}
val book = it.first.copy(
coverImage = if (uri.isNotBlank()) {
Uri.fromFile(File("${application.filesDir}/$uri"))
} else null
)
bookMapper.toBookEntity(book)
}
database.insertBooks(booksWithCover)
}
override suspend fun updateBooks(books: List<Book>) {
// without text
// without text and cover image
database.updateBooks(
books.map {
val book = database.findBookById(it.id ?: return)
@ -114,7 +145,8 @@ class BookRepositoryImpl @Inject constructor(
it.copy(
text = book.text
.split("\n")
.map { line -> StringWithId(line.trim()) }
.map { line -> StringWithId(line.trim()) },
coverImage = if (book.image != null) Uri.parse(book.image) else null
)
)
}
@ -122,21 +154,95 @@ class BookRepositoryImpl @Inject constructor(
}
override suspend fun updateBooksWithText(books: List<Book>) {
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
}
override suspend fun deleteBooks(books: List<Book>) {
// without text
database.deleteBooks(
// without cover image
database.updateBooks(
books.map {
val book = database.findBookById(it.id ?: return)
bookMapper.toBookEntity(
it.copy(
coverImage = if (book.image != null) Uri.parse(book.image) else null
)
)
}
)
}
override suspend fun updateCoverImageOfBook(
bookWithOldCover: Book,
newCoverImage: CoverImage?
) {
// without text
val book = database.findBookById(bookWithOldCover.id ?: return)
var uri: String? = null
if (newCoverImage != null) {
try {
uri = "${UUID.randomUUID()}.webp"
application.openFileOutput(uri, 0x0000).use { stream ->
if (
!newCoverImage.compress(
Bitmap.CompressFormat.WEBP,
30,
stream
)
) {
throw Exception("Couldn't save cover image")
}
}
} catch (e: Exception) {
e.printStackTrace()
return
}
}
if (book.image != null) {
try {
application.deleteFile(
book.image.substringAfterLast("/")
)
} catch (e: Exception) {
e.printStackTrace()
}
}
val newCoverImageUri = if (uri != null) {
Uri.fromFile(File("${application.filesDir}/$uri"))
} else {
null
}
val bookWithNewCover = bookWithOldCover.copy(
text = book.text
.split("\n")
.map { line -> StringWithId(line.trim()) }
.map { line -> StringWithId(line.trim()) },
coverImage = newCoverImageUri
)
database.updateBooks(
listOf(
bookMapper.toBookEntity(
bookWithNewCover
)
)
)
}
override suspend fun deleteBooks(books: List<Book>) {
database.deleteBooks(
books.map {
val book = database.findBookById(it.id ?: return)
if (book.image != null) {
try {
application.deleteFile(
book.image.substringAfterLast("/")
)
} catch (e: Exception) {
e.printStackTrace()
}
}
book
}
)
}
@ -230,55 +336,12 @@ class BookRepositoryImpl @Inject constructor(
}
}
override suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>> {
return flow {
emit(Resource.Loading(true))
val text = if (file.name.endsWith(".txt")) {
txtTextParser.parse(file)
} else if (file.name.endsWith(".pdf")) {
pdfTextParser.parse(file)
} else if (file.name.endsWith(".epub")) {
epubTextParser.parse(file)
} else {
emit(Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)))
return@flow
}
if (text is Resource.Success && text.data == null) {
emit(
Resource.Error(
text.message
?: UIText.StringResource(R.string.error_file_empty)
)
)
}
when (text) {
is Resource.Success -> emit(Resource.Success(text.data))
is Resource.Error -> emit(
Resource.Error(
text.message
?: UIText.StringResource(R.string.error_something_went_wrong)
)
)
is Resource.Loading -> Unit
}
}
}
override suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> {
val books = mutableListOf<NullableBook>()
for (file in files) {
val parsedBook = if (file.name.endsWith(".txt")) {
txtFileParser.parse(file)
} else if (file.name.endsWith(".pdf")) {
pdfFileParser.parse(file)
} else if (file.name.endsWith(".epub")) {
epubFileParser.parse(file)
val parsedBook = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
fileParser.parse(file)
} else {
books.add(
NullableBook.Null(
@ -289,12 +352,8 @@ class BookRepositoryImpl @Inject constructor(
continue
}
val parsedText = if (file.name.endsWith(".txt")) {
txtTextParser.parse(file)
} else if (file.name.endsWith(".pdf")) {
pdfTextParser.parse(file)
} else if (file.name.endsWith(".epub")) {
epubTextParser.parse(file)
val parsedText = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
textParser.parse(file)
} else {
books.add(
NullableBook.Null(
@ -338,8 +397,8 @@ class BookRepositoryImpl @Inject constructor(
books.add(
NullableBook.NotNull(
Pair(
parsedBook.copy(text = parsedText.data),
true
parsedBook.first.copy(text = parsedText.data),
parsedBook.second
)
)
)

View file

@ -1,7 +1,7 @@
package ua.acclorite.book_story.domain.model
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.ImageBitmap
import ua.acclorite.book_story.util.UIText
import java.io.File
@ -17,5 +17,5 @@ data class Book(
val filePath: String,
val lastOpened: Long?,
val category: Category,
val coverImage: ImageBitmap?
val coverImage: Uri?
)

View file

@ -1,15 +1,16 @@
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
@Immutable
sealed class NullableBook(
val book: Pair<Book, Boolean>?,
val book: Pair<Book, CoverImage?>?,
val fileName: String?,
val message: UIText?
) {
class NotNull(book: Pair<Book, Boolean>) : NullableBook(book, null, null)
class NotNull(book: Pair<Book, CoverImage?>) : NullableBook(book, null, null)
class Null(
fileName: String,
message: UIText?

View file

@ -6,7 +6,7 @@ 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.domain.model.StringWithId
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.Resource
import java.io.File
@ -33,7 +33,7 @@ interface BookRepository {
): BookEntity
suspend fun insertBooks(
books: List<Book>
books: List<Pair<Book, CoverImage?>>
)
suspend fun updateBooks(
@ -44,6 +44,11 @@ interface BookRepository {
books: List<Book>
)
suspend fun updateCoverImageOfBook(
bookWithOldCover: Book,
newCoverImage: CoverImage?
)
suspend fun deleteBooks(
books: List<Book>
)
@ -60,11 +65,8 @@ interface BookRepository {
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
suspend fun insertHistory(history: List<History>)
suspend fun getHistory(): Flow<Resource<List<History>>>

View file

@ -2,10 +2,11 @@ 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 javax.inject.Inject
class InsertBooks @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Book>) {
suspend fun execute(books: List<Pair<Book, CoverImage?>>) {
repository.insertBooks(books)
}
}

View file

@ -0,0 +1,13 @@
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 javax.inject.Inject
class UpdateCoverImageOfBook @Inject constructor(private val repository: BookRepository) {
suspend fun execute(bookWithOldCover: Book, newCoverImage: CoverImage?) {
repository.updateCoverImageOfBook(bookWithOldCover, newCoverImage)
}
}

View file

@ -1,5 +1,6 @@
package ua.acclorite.book_story.presentation.components
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.view.ActionMode
@ -151,6 +152,8 @@ private class CustomSelectionToolbar(
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onDictionaryRequested = {
@ -164,6 +167,8 @@ private class CustomSelectionToolbar(
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}

View file

@ -1,8 +1,6 @@
package ua.acclorite.book_story.presentation.screens.book_info
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
@ -52,9 +50,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dagger.hilt.android.lifecycle.withCreationCallback
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSnackbar
@ -81,11 +77,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
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.Constants
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun BookInfoScreen(
viewModel: BookInfoViewModel,
libraryViewModel: LibraryViewModel,
browseViewModel: BrowseViewModel,
historyViewModel: HistoryViewModel,
@ -93,20 +89,6 @@ fun BookInfoScreen(
) {
val context = LocalContext.current
val viewModel by (context as ComponentActivity).viewModels<BookInfoViewModel>(
extrasProducer = {
val book = navigator.retrieveArgument("book") as? Book
if (book == null) {
navigator.navigateBack()
}
context.defaultViewModelCreationExtras
.withCreationCallback<BookInfoViewModel.Factory> { factory ->
factory.create(book ?: Constants.EMPTY_BOOK)
}
}
)
val state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
@ -302,7 +284,7 @@ fun BookInfoScreen(
onClick = {
navigator.navigate(
Screen.READER, false, Argument(
"book", state.book
"book", state.book.id
)
)
},

View file

@ -1,6 +1,6 @@
package ua.acclorite.book_story.presentation.screens.book_info.components
import androidx.compose.foundation.Image
import android.net.Uri
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
@ -10,20 +10,20 @@ import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
/**
* Background of the BookInfoScreen.
*/
@Composable
fun BookInfoBackground(height: Dp, image: ImageBitmap) {
fun BookInfoBackground(height: Dp, image: Uri) {
val background = MaterialTheme.colorScheme.background
Image(
bitmap = image,
AsyncImage(
model = image,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
@ -38,7 +38,7 @@ fun BookInfoBackground(height: Dp, image: ImageBitmap) {
)
}
.blur(3.dp),
alpha = 0.3f,
alpha = 0.4f,
contentScale = ContentScale.Crop
)
}

View file

@ -1,7 +1,6 @@
package ua.acclorite.book_story.presentation.screens.book_info.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -41,6 +40,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
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
@ -76,6 +76,16 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
}
)
) {
if (book.coverImage != null) {
AsyncImage(
model = book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large),
contentScale = ContentScale.Crop
)
} else {
Icon(
imageVector = Icons.Default.Image,
contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc),
@ -85,16 +95,6 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
.aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp)
)
if (book.coverImage != null) {
Image(
bitmap = book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large),
contentScale = ContentScale.Crop
)
}
}

View file

@ -3,10 +3,11 @@ 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
@Immutable
data class BookInfoState(
val book: Book,
val book: Book = Constants.EMPTY_BOOK,
val isRefreshing: Boolean = false,
val editTitle: Boolean = false,

View file

@ -6,12 +6,8 @@ import android.content.Context.CLIPBOARD_SERVICE
import android.graphics.BitmapFactory
import android.os.Build
import androidx.compose.material3.SnackbarResult
import androidx.compose.ui.graphics.asImageBitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -21,27 +17,30 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.use_case.DeleteBooks
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
import ua.acclorite.book_story.domain.use_case.GetBooksById
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.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import javax.inject.Inject
@HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class)
class BookInfoViewModel @AssistedInject constructor(
@Assisted book: Book,
@HiltViewModel
class BookInfoViewModel @Inject constructor(
private val updateBooks: UpdateBooks,
private val updateBooksWithText: UpdateBooksWithText,
private val updateCoverImageOfBook: UpdateCoverImageOfBook,
private val deleteBooks: DeleteBooks,
private val getBookFromFile: GetBookFromFile
private val getBookFromFile: GetBookFromFile,
private val getBookById: GetBooksById
) : ViewModel() {
private val _state = MutableStateFlow(BookInfoState(book))
private val _state = MutableStateFlow(BookInfoState())
val state = _state.asStateFlow()
private var job: Job? = null
@ -59,22 +58,24 @@ class BookInfoViewModel @AssistedInject constructor(
is BookInfoEvent.OnChangeCover -> {
viewModelScope.launch {
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
BitmapFactory.decodeStream(it).asImageBitmap()
BitmapFactory.decodeStream(it)
} ?: return@launch
image.prepareToDraw()
updateBooks.execute(
listOf(
_state.value.book.copy(coverImage = image)
)
updateCoverImageOfBook.execute(
_state.value.book,
image
)
event.refreshList()
val newCoverImage = getBookById.execute(
listOf(
_state.value.book.id ?: return@launch
)
).first().coverImage
_state.update {
it.copy(
book = it.book.copy(
coverImage = image
coverImage = newCoverImage
),
showChangeCoverBottomSheet = false
)
@ -88,10 +89,9 @@ class BookInfoViewModel @AssistedInject constructor(
return@launch
}
updateBooks.execute(
listOf(
_state.value.book.copy(coverImage = null)
)
updateCoverImageOfBook.execute(
bookWithOldCover = _state.value.book,
newCoverImage = null
)
event.refreshList()
@ -251,7 +251,7 @@ class BookInfoViewModel @AssistedInject constructor(
val clipboardManager =
event.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(ClipData.newPlainText("", event.text))
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, event.text))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
event.success()
@ -399,21 +399,18 @@ class BookInfoViewModel @AssistedInject constructor(
fun init(navigator: Navigator) {
viewModelScope.launch {
val book = navigator.retrieveArgument("book") as? Book
val bookIndex = navigator.retrieveArgument("book") as? Int
if (book == null) {
if (bookIndex == null || bookIndex < 0) {
navigator.navigateBack()
return@launch
}
val book = getBookById.execute(listOf(bookIndex)).first()
_state.update {
BookInfoState(book = book)
}
}
}
@AssistedFactory
interface Factory {
fun create(book: Book): BookInfoViewModel
}
}

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomCheckbox
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.util.Selected
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
@ -45,7 +46,7 @@ import java.util.Locale
*/
@Composable
fun BrowseFileItem(
file: Pair<File, Boolean>,
file: Pair<File, Selected>,
hasSelectedFiles: Boolean,
modifier: Modifier, onClick: () -> Unit
) {

View file

@ -46,9 +46,7 @@ fun BrowseAddingDialog(
description = stringResource(id = R.string.add_books_description),
actionText = stringResource(id = R.string.add),
isActionEnabled = !state.isBooksLoading &&
state.selectedBooks
.filterIsInstance<NullableBook.NotNull>()
.isNotEmpty(),
state.selectedBooks.any { it.first is NullableBook.NotNull },
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
onAction = {
viewModel.onEvent(
@ -91,7 +89,7 @@ fun BrowseAddingDialog(
} else {
Toast.makeText(
context,
book.message?.asString(context),
book.first.message?.asString(context),
Toast.LENGTH_LONG
).show()
}

View file

@ -24,13 +24,14 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.presentation.components.CustomCheckbox
import ua.acclorite.book_story.util.Selected
/**
* Adding dialog item.
*/
@Composable
fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
if (result is NullableBook.NotNull) {
fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boolean) -> Unit) {
if (result.first is NullableBook.NotNull) {
Row(
Modifier
.fillMaxWidth()
@ -44,14 +45,14 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
Modifier.weight(0.85f)
) {
Text(
text = result.book!!.first.title,
text = result.first.book!!.first.title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = result.book.first.author.asString(),
text = result.first.book!!.first.author.asString(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
@ -59,12 +60,12 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
)
}
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
CustomCheckbox(selected = result.book!!.second)
CustomCheckbox(selected = result.second)
}
}
} else {
val icon =
if (result.message?.asString() == stringResource(R.string.error_file_encrypted)) {
if (result.first.message?.asString() == stringResource(R.string.error_file_encrypted)) {
Icons.Default.Lock
} else {
Icons.Default.Error
@ -87,7 +88,7 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = result.fileName!!,
text = result.first.fileName!!,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error,
maxLines = 2,

View file

@ -8,6 +8,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.util.Selected
import java.io.File
sealed class BrowseEvent {
@ -29,8 +30,8 @@ sealed class BrowseEvent {
val hideErrorMessage: () -> Unit
) : BrowseEvent()
data class OnSelectFile(val file: Pair<File, Boolean>) : BrowseEvent()
data class OnSelectBook(val book: NullableBook) : BrowseEvent()
data class OnSelectFile(val file: Pair<File, Selected>) : BrowseEvent()
data class OnSelectBook(val book: Pair<NullableBook, Selected>) : BrowseEvent()
data object OnSearchShowHide : BrowseEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
data object OnClearSelectedFiles : BrowseEvent()

View file

@ -3,11 +3,12 @@ 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 java.io.File
@Immutable
data class BrowseState(
val selectableFiles: List<Pair<File, Boolean>> = emptyList(),
val selectableFiles: List<Pair<File, Selected>> = emptyList(),
val listState: LazyListState = LazyListState(0, 0),
val isLoading: Boolean = true,
@ -25,6 +26,6 @@ data class BrowseState(
val hasFocused: Boolean = false,
val showAddingDialog: Boolean = false,
val selectedBooks: List<NullableBook> = emptyList(),
val selectedBooks: List<Pair<NullableBook, Selected>> = emptyList(),
val isBooksLoading: Boolean = false
)

View file

@ -199,13 +199,12 @@ class BrowseViewModel @Inject constructor(
}
val editedList = _state.value.selectedBooks.toMutableList()
editedList[indexOfFile] = NullableBook.NotNull(
editedList[indexOfFile].book!!.copy(
second = !editedList[indexOfFile].book!!.second
)
)
editedList[indexOfFile] = editedList[indexOfFile].first to
!editedList[indexOfFile].second
if (!editedList.any { it.book?.second == true }) {
if (!editedList.filter { it.first is NullableBook.NotNull }
.any { it.second }
) {
return@launch
}
@ -308,7 +307,7 @@ class BrowseViewModel @Inject constructor(
_state.update {
it.copy(
selectedBooks = books,
selectedBooks = books.map { book -> book to true },
isBooksLoading = false
)
}
@ -318,9 +317,9 @@ class BrowseViewModel @Inject constructor(
is BrowseEvent.OnAddBooks -> {
viewModelScope.launch {
val booksToInsert = _state.value.selectedBooks
.filterIsInstance<NullableBook.NotNull>()
.filter { it.book!!.second }
.map { it.book!!.first }
.filter { it.first is NullableBook.NotNull }
.filter { it.second }
.map { it.first.book!! }
if (booksToInsert.isEmpty()) {
return@launch
@ -330,19 +329,21 @@ class BrowseViewModel @Inject constructor(
val books = fastGetBooks.execute("")
event.resetScroll()
_state.update {
it.copy(
showAddingDialog = false
)
}
onEvent(BrowseEvent.OnClearSelectedFiles)
onEvent(BrowseEvent.OnLoadList)
event.navigator.navigate(
Screen.LIBRARY,
false,
Argument("added_books", books)
)
_state.update {
it.copy(
showAddingDialog = false,
selectableFiles = emptyList(),
listState = LazyListState(0, 0)
)
}
onEvent(BrowseEvent.OnLoadList)
onEvent(BrowseEvent.OnClearSelectedFiles)
}
}

View file

@ -248,14 +248,14 @@ fun HistoryScreen(
navigator.navigate(
Screen.BOOK_INFO,
false,
Argument("book", book)
Argument("book", book.id)
)
},
onTitleClick = {
navigator.navigate(
Screen.READER,
false,
Argument("book", book)
Argument("book", book.id)
)
},
isDeleteEnabled = !state.isRefreshing,

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.history.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -31,6 +30,7 @@ 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 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
@ -77,6 +77,16 @@ fun HistoryItem(
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation())
) {
if (book.coverImage != null) {
AsyncImage(
model = book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
} else {
Icon(
imageVector = Icons.Default.Image,
contentDescription = stringResource(
@ -88,16 +98,6 @@ fun HistoryItem(
.aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp)
)
if (book.coverImage != null) {
Image(
bitmap = book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
}
}

View file

@ -86,6 +86,7 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
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 java.util.UUID
@OptIn(
@ -304,14 +305,14 @@ fun LibraryScreen(
) {
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
var categoryIsLoading by remember { mutableStateOf(true) }
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Boolean>>() }
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Selected>>() }
val category = remember { Category.entries[index] }
LaunchedEffect(state.books) {
categorizedBooks.clear()
categorizedBooks.addAll(state.books.filter { it.first.category == category }
.sortedWith(
compareByDescending<Pair<Book, Boolean>> { it.first.lastOpened }
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
.thenBy { it.first.title }
)
)
@ -340,7 +341,7 @@ fun LibraryScreen(
navigator.navigate(
Screen.BOOK_INFO,
false,
Argument("book", it.first)
Argument("book", it.first.id)
)
}
},
@ -353,7 +354,7 @@ fun LibraryScreen(
navigator.navigate(
Screen.READER,
false,
Argument("book", it.first)
Argument("book", it.first.id)
)
}
)

View file

@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.screens.library.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
@ -35,11 +34,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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.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
/**
* Library list element item.
@ -47,7 +48,7 @@ import ua.acclorite.book_story.ui.elevation
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryBookItem(
book: Pair<Book, Boolean>,
book: Pair<Book, Selected>,
modifier: Modifier = Modifier,
onCoverImageClick: () -> Unit,
onLongClick: () -> Unit,
@ -103,6 +104,16 @@ fun LibraryBookItem(
}
)
) {
if (book.first.coverImage != null) {
AsyncImage(
model = book.first.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large),
contentScale = ContentScale.Crop
)
} else {
Icon(
imageVector = Icons.Default.Image,
contentDescription = stringResource(
@ -114,16 +125,6 @@ fun LibraryBookItem(
.aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp)
)
if (book.first.coverImage != null) {
Image(
bitmap = book.first.coverImage!!,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.large),
contentScale = ContentScale.Crop
)
}
Text(

View file

@ -7,6 +7,7 @@ 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
sealed class LibraryEvent {
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
@ -17,7 +18,7 @@ sealed class LibraryEvent {
data object OnSearchShowHide : LibraryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
data class OnSearchQueryChange(val query: String) : LibraryEvent()
data class OnSelectBook(val book: Pair<Book, Boolean>, val select: Boolean? = null) :
data class OnSelectBook(val book: Pair<Book, Selected>, val select: Boolean? = null) :
LibraryEvent()
data object OnClearSelectedBooks : LibraryEvent()

View file

@ -3,10 +3,11 @@ 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
@Immutable
data class LibraryState(
val books: List<Pair<Book, Boolean>> = emptyList(),
val books: List<Pair<Book, Selected>> = emptyList(),
val isLoading: Boolean = true,
val isRefreshing: Boolean = false,

View file

@ -5,7 +5,6 @@ import android.os.Build
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
@ -51,12 +50,10 @@ import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.is_messages.IsError
import ua.acclorite.book_story.presentation.data.MainViewModel
@ -79,6 +76,7 @@ import ua.acclorite.book_story.util.Constants
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun ReaderScreen(
viewModel: ReaderViewModel,
mainViewModel: MainViewModel,
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
@ -87,20 +85,6 @@ fun ReaderScreen(
val context = LocalContext.current as ComponentActivity
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
val viewModel by context.viewModels<ReaderViewModel>(
extrasProducer = {
val book = navigator.retrieveArgument("book") as? Book
if (book == null) {
navigator.navigateBack()
}
context.defaultViewModelCreationExtras
.withCreationCallback<ReaderViewModel.Factory> { factory ->
factory.create(book ?: Constants.EMPTY_BOOK)
}
}
)
var loading by remember { mutableStateOf(true) }
val state by viewModel.state.collectAsState()
val listState = rememberLazyListState()
@ -296,13 +280,16 @@ fun ReaderScreen(
itemsIndexed(
state.book.text, key = { _, key -> key.id }
) { index, line ->
val text = remember { "${if (paragraphIndentation) " " else ""}${line.line}" }
val color = remember { Color(fontColor.toULong()) }
val lineHeightSp = remember { (fontSize + lineHeight).sp }
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 }
Column(
Modifier
.background(Color(backgroundColor.toULong()))
.background(backgroundCol)
.fillMaxWidth()
.padding(
top = if (index == 0) 36.dp else 0.dp,
@ -314,7 +301,7 @@ fun ReaderScreen(
) {
Text(
text = text,
color = color,
color = fontCol,
style = TextStyle(
lineBreak = LineBreak.Paragraph

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.reader.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -32,6 +31,7 @@ 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 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
@ -62,6 +62,16 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation())
) {
if (state.book.coverImage != null) {
AsyncImage(
model = state.book.coverImage!!,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
} else {
Icon(
imageVector = Icons.Default.Image,
contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc),
@ -71,16 +81,6 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp)
)
if (state.book.coverImage != null) {
Image(
bitmap = state.book.coverImage!!,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
}
}

View file

@ -79,7 +79,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
true,
Argument(
"book",
state.book
state.book.id
)
)
}

View file

@ -2,11 +2,12 @@ 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
@Immutable
data class ReaderState(
val book: Book,
val book: Book = Constants.EMPTY_BOOK,
val letters: Int = 0,
val words: Int = 0,

View file

@ -11,9 +11,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -27,6 +24,7 @@ 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.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks
@ -35,18 +33,19 @@ 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
@OptIn(ExperimentalFoundationApi::class)
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class)
class ReaderViewModel @AssistedInject constructor(
@Assisted book: Book,
@HiltViewModel
class ReaderViewModel @Inject constructor(
private val updateBooks: UpdateBooks,
private val insertHistory: InsertHistory,
private val getText: GetText
private val getText: GetText,
private val getBooksById: GetBooksById
) : ViewModel() {
private val _state = MutableStateFlow(ReaderState(book))
private val _state = MutableStateFlow(ReaderState())
val state = _state.asStateFlow()
fun onEvent(event: ReaderEvent) {
@ -104,14 +103,17 @@ class ReaderViewModel @AssistedInject constructor(
)
event.refreshList(_state.value.book)
event.navigator.putArgument(
Argument("book", _state.value.book)
Argument("book", _state.value.book.id)
)
viewModelScope.launch {
snapshotFlow {
event.scrollState.layoutInfo.totalItemsCount
}.collectLatest { itemsCount ->
val scrollTo = (itemsCount * _state.value.book.progress).roundToInt()
val scrollTo =
(_state.value.book.text.size * _state.value.book.progress)
.toInt()
if (itemsCount >= _state.value.book.text.size) {
if (scrollTo > 0) {
var loaded = false
@ -194,12 +196,12 @@ class ReaderViewModel @AssistedInject constructor(
}
updateBooks.execute(
listOf(_state.value.book.copy(progress = event.progress))
listOf(_state.value.book)
)
event.navigator.putArgument(
Argument("book", _state.value.book.copy(progress = event.progress))
Argument("book", _state.value.book.id)
)
event.refreshList(_state.value.book.copy(progress = event.progress))
event.refreshList(_state.value.book)
}
}
@ -301,13 +303,15 @@ class ReaderViewModel @AssistedInject constructor(
onLoaded: () -> Unit
) {
viewModelScope.launch {
val book = navigator.retrieveArgument("book") as? Book
val bookIndex = navigator.retrieveArgument("book") as? Int
if (book == null) {
if (bookIndex == null || bookIndex < 0) {
navigator.navigateBack()
return@launch
}
val book = getBooksById.execute(listOf(bookIndex)).first()
_state.update {
ReaderState(book = book)
}
@ -328,11 +332,6 @@ class ReaderViewModel @AssistedInject constructor(
)
}
}
@AssistedFactory
interface Factory {
fun create(book: Book): ReaderViewModel
}
}

View file

@ -177,7 +177,7 @@ object Constants {
)
val EMPTY_BOOK = Book(
0,
-1,
"",
UIText.StringValue(""),
null,

View file

@ -0,0 +1,6 @@
package ua.acclorite.book_story.util
import android.graphics.Bitmap
typealias CoverImage = Bitmap
typealias Selected = Boolean

View file

@ -27,6 +27,7 @@
<string name="add_books_description">
Ви можете вибрати книги, які хочете додати.
Ви зможете відредагувати Назву та Обкладинку книги пізніше.
Процес завантаження може зайняти декілька хвилин.
</string>
<string name="move_books_description">
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).

View file

@ -1,6 +1,6 @@
<resources>
<!-- Basic Strings -->
<string name="app_version" translatable="false">0.9.2</string>
<string name="app_version" translatable="false">0.9.3</string>
<string name="app_name">Book\'s Story</string>
<!-- Screens -->
@ -28,6 +28,7 @@
<string name="add_books_description">
You can choose which books you want to add.
You can edit Title and Cover image of the book later.
The loading process can take up to several minutes.
</string>
<string name="move_books_description">
Choose the category to which you want to move all the selected books(%1$s).