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 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.9.2" versionName = "0.9.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { 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.NavigationHost
import ua.acclorite.book_story.presentation.data.Screen 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.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.BrowseScreen
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen 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.LibraryScreen
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreen 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.SettingsScreen
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
@ -57,7 +59,8 @@ class Activity : AppCompatActivity() {
private val libraryViewModel: LibraryViewModel by viewModels() private val libraryViewModel: LibraryViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels()
private val browseViewModel: BrowseViewModel 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -87,12 +90,14 @@ class Activity : AppCompatActivity() {
setContent { setContent {
val windowClass = calculateWindowSizeClass(activity = this) val windowClass = calculateWindowSizeClass(activity = this)
val isLoaded by mainViewModel.isReady.collectAsState()
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
val darkTheme = val darkTheme =
mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM
val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact
if (isLoaded) {
BooksHistoryResurrectionTheme( BooksHistoryResurrectionTheme(
theme = theme, theme = theme,
isDark = darkTheme.isDark() isDark = darkTheme.isDark()
@ -165,6 +170,7 @@ class Activity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut exitAnim = Transitions.SlidingTransitionOut
) { ) {
BookInfoScreen( BookInfoScreen(
viewModel = bookInfoViewModel,
libraryViewModel = libraryViewModel, libraryViewModel = libraryViewModel,
browseViewModel = browseViewModel, browseViewModel = browseViewModel,
historyViewModel = historyViewModel, historyViewModel = historyViewModel,
@ -177,6 +183,7 @@ class Activity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut exitAnim = Transitions.SlidingTransitionOut
) { ) {
ReaderScreen( ReaderScreen(
viewModel = readerViewModel,
mainViewModel = mainViewModel, mainViewModel = mainViewModel,
libraryViewModel = libraryViewModel, libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel, 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.book.BookMapperImpl
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl 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.data.repository.BookRepositoryImpl
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Singleton import javax.inject.Singleton
@ -40,4 +44,16 @@ abstract class RepositoryModule {
abstract fun bindHistoryMapper( abstract fun bindHistoryMapper(
historyMapperImpl: HistoryMapperImpl historyMapperImpl: HistoryMapperImpl
): HistoryMapper ): 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 text: String,
val filePath: String, val filePath: String,
val progress: Float, val progress: Float,
val image: ByteArray = byteArrayOf(), val image: String? = null,
val category: Category 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 package ua.acclorite.book_story.data.mapper.book
import android.graphics.Bitmap import android.net.Uri
import android.graphics.BitmapFactory
import android.os.Build
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class BookMapperImpl @Inject constructor() : BookMapper { class BookMapperImpl @Inject constructor() : BookMapper {
override suspend fun toBookEntity(book: Book): BookEntity { 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") { val textAsString = book.text.joinToString("\n") {
it.line.trim() it.line.trim()
} }
@ -38,21 +22,13 @@ class BookMapperImpl @Inject constructor() : BookMapper {
author = book.author.string, author = book.author.string,
text = textAsString, text = textAsString,
description = book.description, description = book.description,
image = stream.toByteArray(), image = if (book.coverImage != null) book.coverImage.toString() else null,
category = book.category category = book.category
) )
} }
override suspend fun toBook(bookEntity: BookEntity): Book { override suspend fun toBook(bookEntity: BookEntity): Book {
val file = File(bookEntity.filePath) 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( return Book(
id = bookEntity.id, id = bookEntity.id,
@ -67,7 +43,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
filePath = bookEntity.filePath, filePath = bookEntity.filePath,
lastOpened = null, lastOpened = null,
category = bookEntity.category, 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 package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.CoverImage
import java.io.File import java.io.File
interface FileParser { 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 package ua.acclorite.book_story.data.parser.epub
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.asImageBitmap
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader import nl.siegmann.epublib.epub.EpubReader
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -15,7 +15,7 @@ import javax.inject.Inject
class EpubFileParser @Inject constructor() : FileParser { 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")) { if (!file.name.endsWith(".epub")) {
return null return null
} }
@ -23,7 +23,9 @@ class EpubFileParser @Inject constructor() : FileParser {
try { try {
val epubReader = EpubReader() val epubReader = EpubReader()
val book = withContext(Dispatchers.IO) { val book = withContext(Dispatchers.IO) {
epubReader.readEpub(FileInputStream(file)) FileInputStream(file).use {
epubReader.readEpub(it)
}
} }
val metadata = book.metadata val metadata = book.metadata
@ -42,8 +44,6 @@ class EpubFileParser @Inject constructor() : FileParser {
} }
} }
return Book( return Book(
id = null, id = null,
title = title, title = title,
@ -55,8 +55,8 @@ class EpubFileParser @Inject constructor() : FileParser {
filePath = file.path, filePath = file.path,
lastOpened = null, lastOpened = null,
category = Category.entries[0], category = Category.entries[0],
coverImage = coverImage?.asImageBitmap() coverImage = null
) ) to coverImage
} catch (e: Exception) { } catch (e: Exception) {
return null 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.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class PdfFileParser @Inject constructor(private val application: Application) : FileParser { 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")) { if (!file.name.endsWith(".pdf")) {
return null return null
} }
@ -46,7 +47,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
lastOpened = null, lastOpened = null,
category = Category.entries[0], category = Category.entries[0],
coverImage = null coverImage = null
) ) to null
} catch (e: Exception) { } catch (e: Exception) {
return null 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.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class TxtFileParser @Inject constructor() : FileParser { 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")) { if (!file.name.endsWith(".txt")) {
return null return null
} }
@ -33,7 +34,7 @@ class TxtFileParser @Inject constructor() : FileParser {
lastOpened = null, lastOpened = null,
category = Category.entries[0], category = Category.entries[0],
coverImage = null coverImage = null
) ) to null
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }

View file

@ -1,5 +1,8 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import android.app.Application
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow 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.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.epub.EpubFileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.epub.EpubTextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
@ -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.domain.repository.BookRepository
import ua.acclorite.book_story.presentation.data.calculateFamiliarity import ua.acclorite.book_story.presentation.data.calculateFamiliarity
import ua.acclorite.book_story.util.Constants import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.util.Resource
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
import java.io.File import java.io.File
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class BookRepositoryImpl @Inject constructor( class BookRepositoryImpl @Inject constructor(
private val application: Application,
private val database: BookDao, private val database: BookDao,
private val dataStore: DataStore, private val dataStore: DataStore,
private val bookMapper: BookMapper, private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper, private val historyMapper: HistoryMapper,
private val txtFileParser: TxtFileParser, private val fileParser: FileParser,
private val pdfFileParser: PdfFileParser, private val textParser: TextParser
private val epubFileParser: EpubFileParser,
private val txtTextParser: TxtTextParser,
private val pdfTextParser: PdfTextParser,
private val epubTextParser: EpubTextParser,
) : BookRepository { ) : BookRepository {
@ -101,12 +98,46 @@ class BookRepositoryImpl @Inject constructor(
return database.findBookById(id) return database.findBookById(id)
} }
override suspend fun insertBooks(books: List<Book>) { override suspend fun insertBooks(
database.insertBooks(books.map { bookMapper.toBookEntity(it) }) 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>) { override suspend fun updateBooks(books: List<Book>) {
// without text // without text and cover image
database.updateBooks( database.updateBooks(
books.map { books.map {
val book = database.findBookById(it.id ?: return) val book = database.findBookById(it.id ?: return)
@ -114,7 +145,8 @@ class BookRepositoryImpl @Inject constructor(
it.copy( it.copy(
text = book.text text = book.text
.split("\n") .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>) { override suspend fun updateBooksWithText(books: List<Book>) {
database.updateBooks(books.map { bookMapper.toBookEntity(it) }) // without cover image
} database.updateBooks(
override suspend fun deleteBooks(books: List<Book>) {
// without text
database.deleteBooks(
books.map { books.map {
val book = database.findBookById(it.id ?: return) val book = database.findBookById(it.id ?: return)
bookMapper.toBookEntity( bookMapper.toBookEntity(
it.copy( 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 text = book.text
.split("\n") .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> { override suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> {
val books = mutableListOf<NullableBook>() val books = mutableListOf<NullableBook>()
for (file in files) { for (file in files) {
val parsedBook = if (file.name.endsWith(".txt")) { val parsedBook = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
txtFileParser.parse(file) fileParser.parse(file)
} else if (file.name.endsWith(".pdf")) {
pdfFileParser.parse(file)
} else if (file.name.endsWith(".epub")) {
epubFileParser.parse(file)
} else { } else {
books.add( books.add(
NullableBook.Null( NullableBook.Null(
@ -289,12 +352,8 @@ class BookRepositoryImpl @Inject constructor(
continue continue
} }
val parsedText = if (file.name.endsWith(".txt")) { val parsedText = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
txtTextParser.parse(file) textParser.parse(file)
} else if (file.name.endsWith(".pdf")) {
pdfTextParser.parse(file)
} else if (file.name.endsWith(".epub")) {
epubTextParser.parse(file)
} else { } else {
books.add( books.add(
NullableBook.Null( NullableBook.Null(
@ -338,8 +397,8 @@ class BookRepositoryImpl @Inject constructor(
books.add( books.add(
NullableBook.NotNull( NullableBook.NotNull(
Pair( Pair(
parsedBook.copy(text = parsedText.data), parsedBook.first.copy(text = parsedText.data),
true parsedBook.second
) )
) )
) )

View file

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

View file

@ -1,15 +1,16 @@
package ua.acclorite.book_story.domain.model package ua.acclorite.book_story.domain.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
@Immutable @Immutable
sealed class NullableBook( sealed class NullableBook(
val book: Pair<Book, Boolean>?, val book: Pair<Book, CoverImage?>?,
val fileName: String?, val fileName: String?,
val message: UIText? 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( class Null(
fileName: String, fileName: String,
message: UIText? 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.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.util.CoverImage
import ua.acclorite.book_story.util.Resource import ua.acclorite.book_story.util.Resource
import java.io.File import java.io.File
@ -33,7 +33,7 @@ interface BookRepository {
): BookEntity ): BookEntity
suspend fun insertBooks( suspend fun insertBooks(
books: List<Book> books: List<Pair<Book, CoverImage?>>
) )
suspend fun updateBooks( suspend fun updateBooks(
@ -44,6 +44,11 @@ interface BookRepository {
books: List<Book> books: List<Book>
) )
suspend fun updateCoverImageOfBook(
bookWithOldCover: Book,
newCoverImage: CoverImage?
)
suspend fun deleteBooks( suspend fun deleteBooks(
books: List<Book> books: List<Book>
) )
@ -60,11 +65,8 @@ interface BookRepository {
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>> 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 getBooksFromFiles(files: List<File>): List<NullableBook>
suspend fun insertHistory(history: List<History>) suspend fun insertHistory(history: List<History>)
suspend fun getHistory(): Flow<Resource<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.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.util.CoverImage
import javax.inject.Inject import javax.inject.Inject
class InsertBooks @Inject constructor(private val repository: BookRepository) { 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) 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 package ua.acclorite.book_story.presentation.components
import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.view.ActionMode import android.view.ActionMode
@ -151,6 +152,8 @@ private class CustomSelectionToolbar(
clipboardManager.setPrimaryClip( clipboardManager.setPrimaryClip(
previousClipboard previousClipboard
) )
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
} }
} }
callback.onDictionaryRequested = { callback.onDictionaryRequested = {
@ -164,6 +167,8 @@ private class CustomSelectionToolbar(
clipboardManager.setPrimaryClip( clipboardManager.setPrimaryClip(
previousClipboard previousClipboard
) )
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
} }
} }

View file

@ -1,8 +1,6 @@
package ua.acclorite.book_story.presentation.screens.book_info package ua.acclorite.book_story.presentation.screens.book_info
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -52,9 +50,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dagger.hilt.android.lifecycle.withCreationCallback
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSnackbar 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.DefaultTransition
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Constants
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
fun BookInfoScreen( fun BookInfoScreen(
viewModel: BookInfoViewModel,
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel,
browseViewModel: BrowseViewModel, browseViewModel: BrowseViewModel,
historyViewModel: HistoryViewModel, historyViewModel: HistoryViewModel,
@ -93,20 +89,6 @@ fun BookInfoScreen(
) { ) {
val context = LocalContext.current 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 state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState() val listState = rememberLazyListState()
@ -302,7 +284,7 @@ fun BookInfoScreen(
onClick = { onClick = {
navigator.navigate( navigator.navigate(
Screen.READER, false, Argument( 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 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme 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.draw.drawWithContent
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
/** /**
* Background of the BookInfoScreen. * Background of the BookInfoScreen.
*/ */
@Composable @Composable
fun BookInfoBackground(height: Dp, image: ImageBitmap) { fun BookInfoBackground(height: Dp, image: Uri) {
val background = MaterialTheme.colorScheme.background val background = MaterialTheme.colorScheme.background
Image( AsyncImage(
bitmap = image, model = image,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -38,7 +38,7 @@ fun BookInfoBackground(height: Dp, image: ImageBitmap) {
) )
} }
.blur(3.dp), .blur(3.dp),
alpha = 0.3f, alpha = 0.4f,
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }

View file

@ -1,7 +1,6 @@
package ua.acclorite.book_story.presentation.screens.book_info.components package ua.acclorite.book_story.presentation.screens.book_info.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource 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.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent 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( Icon(
imageVector = Icons.Default.Image, imageVector = Icons.Default.Image,
contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc),
@ -85,16 +95,6 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) 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 androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Constants
@Immutable @Immutable
data class BookInfoState( data class BookInfoState(
val book: Book, val book: Book = Constants.EMPTY_BOOK,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val editTitle: Boolean = false, val editTitle: Boolean = false,

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.util.Selected
import java.io.File import java.io.File
sealed class BrowseEvent { sealed class BrowseEvent {
@ -29,8 +30,8 @@ sealed class BrowseEvent {
val hideErrorMessage: () -> Unit val hideErrorMessage: () -> Unit
) : BrowseEvent() ) : BrowseEvent()
data class OnSelectFile(val file: Pair<File, Boolean>) : BrowseEvent() data class OnSelectFile(val file: Pair<File, Selected>) : BrowseEvent()
data class OnSelectBook(val book: NullableBook) : BrowseEvent() data class OnSelectBook(val book: Pair<NullableBook, Selected>) : BrowseEvent()
data object OnSearchShowHide : BrowseEvent() data object OnSearchShowHide : BrowseEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
data object OnClearSelectedFiles : 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.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.util.Selected
import java.io.File import java.io.File
@Immutable @Immutable
data class BrowseState( data class BrowseState(
val selectableFiles: List<Pair<File, Boolean>> = emptyList(), val selectableFiles: List<Pair<File, Selected>> = emptyList(),
val listState: LazyListState = LazyListState(0, 0), val listState: LazyListState = LazyListState(0, 0),
val isLoading: Boolean = true, val isLoading: Boolean = true,
@ -25,6 +26,6 @@ data class BrowseState(
val hasFocused: Boolean = false, val hasFocused: Boolean = false,
val showAddingDialog: Boolean = false, val showAddingDialog: Boolean = false,
val selectedBooks: List<NullableBook> = emptyList(), val selectedBooks: List<Pair<NullableBook, Selected>> = emptyList(),
val isBooksLoading: Boolean = false val isBooksLoading: Boolean = false
) )

View file

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

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.history.components package ua.acclorite.book_story.presentation.screens.history.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement 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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
@ -77,6 +77,16 @@ fun HistoryItem(
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation()) .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( Icon(
imageVector = Icons.Default.Image, imageVector = Icons.Default.Image,
contentDescription = stringResource( contentDescription = stringResource(
@ -88,16 +98,6 @@ fun HistoryItem(
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) 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.DefaultTransition
import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Selected
import java.util.UUID import java.util.UUID
@OptIn( @OptIn(
@ -304,14 +305,14 @@ fun LibraryScreen(
) { ) {
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index -> HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
var categoryIsLoading by remember { mutableStateOf(true) } 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] } val category = remember { Category.entries[index] }
LaunchedEffect(state.books) { LaunchedEffect(state.books) {
categorizedBooks.clear() categorizedBooks.clear()
categorizedBooks.addAll(state.books.filter { it.first.category == category } categorizedBooks.addAll(state.books.filter { it.first.category == category }
.sortedWith( .sortedWith(
compareByDescending<Pair<Book, Boolean>> { it.first.lastOpened } compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
.thenBy { it.first.title } .thenBy { it.first.title }
) )
) )
@ -340,7 +341,7 @@ fun LibraryScreen(
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
false, false,
Argument("book", it.first) Argument("book", it.first.id)
) )
} }
}, },
@ -353,7 +354,7 @@ fun LibraryScreen(
navigator.navigate( navigator.navigate(
Screen.READER, Screen.READER,
false, 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.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Selected
/** /**
* Library list element item. * Library list element item.
@ -47,7 +48,7 @@ import ua.acclorite.book_story.ui.elevation
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LibraryBookItem( fun LibraryBookItem(
book: Pair<Book, Boolean>, book: Pair<Book, Selected>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onCoverImageClick: () -> Unit, onCoverImageClick: () -> Unit,
onLongClick: () -> 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( Icon(
imageVector = Icons.Default.Image, imageVector = Icons.Default.Image,
contentDescription = stringResource( contentDescription = stringResource(
@ -114,16 +125,6 @@ fun LibraryBookItem(
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) 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( Text(

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Selected
sealed class LibraryEvent { sealed class LibraryEvent {
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent() data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
@ -17,7 +18,7 @@ sealed class LibraryEvent {
data object OnSearchShowHide : LibraryEvent() data object OnSearchShowHide : LibraryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
data class OnSearchQueryChange(val query: String) : 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() LibraryEvent()
data object OnClearSelectedBooks : 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 androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.Selected
@Immutable @Immutable
data class LibraryState( data class LibraryState(
val books: List<Pair<Book, Boolean>> = emptyList(), val books: List<Pair<Book, Selected>> = emptyList(),
val isLoading: Boolean = true, val isLoading: Boolean = true,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,

View file

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

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.reader.components package ua.acclorite.book_story.presentation.screens.reader.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.ui.elevation import ua.acclorite.book_story.ui.elevation
@ -62,6 +62,16 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation()) .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( Icon(
imageVector = Icons.Default.Image, imageVector = Icons.Default.Image,
contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc),
@ -71,16 +81,6 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) 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, true,
Argument( Argument(
"book", "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 androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
@Immutable @Immutable
data class ReaderState( data class ReaderState(
val book: Book, val book: Book = Constants.EMPTY_BOOK,
val letters: Int = 0, val letters: Int = 0,
val words: Int = 0, val words: Int = 0,

View file

@ -11,9 +11,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay 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.Category
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetText import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks 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.presentation.data.Screen
import ua.acclorite.book_story.util.UIText import ua.acclorite.book_story.util.UIText
import java.util.Date import java.util.Date
import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class) @HiltViewModel
class ReaderViewModel @AssistedInject constructor( class ReaderViewModel @Inject constructor(
@Assisted book: Book,
private val updateBooks: UpdateBooks, private val updateBooks: UpdateBooks,
private val insertHistory: InsertHistory, private val insertHistory: InsertHistory,
private val getText: GetText private val getText: GetText,
private val getBooksById: GetBooksById
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(ReaderState(book)) private val _state = MutableStateFlow(ReaderState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
fun onEvent(event: ReaderEvent) { fun onEvent(event: ReaderEvent) {
@ -104,14 +103,17 @@ class ReaderViewModel @AssistedInject constructor(
) )
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
event.navigator.putArgument( event.navigator.putArgument(
Argument("book", _state.value.book) Argument("book", _state.value.book.id)
) )
viewModelScope.launch { viewModelScope.launch {
snapshotFlow { snapshotFlow {
event.scrollState.layoutInfo.totalItemsCount event.scrollState.layoutInfo.totalItemsCount
}.collectLatest { itemsCount -> }.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 (itemsCount >= _state.value.book.text.size) {
if (scrollTo > 0) { if (scrollTo > 0) {
var loaded = false var loaded = false
@ -194,12 +196,12 @@ class ReaderViewModel @AssistedInject constructor(
} }
updateBooks.execute( updateBooks.execute(
listOf(_state.value.book.copy(progress = event.progress)) listOf(_state.value.book)
) )
event.navigator.putArgument( 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 onLoaded: () -> Unit
) { ) {
viewModelScope.launch { 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() navigator.navigateBack()
return@launch return@launch
} }
val book = getBooksById.execute(listOf(bookIndex)).first()
_state.update { _state.update {
ReaderState(book = book) 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( val EMPTY_BOOK = Book(
0, -1,
"", "",
UIText.StringValue(""), UIText.StringValue(""),
null, 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 name="add_books_description">
Ви можете вибрати книги, які хочете додати. Ви можете вибрати книги, які хочете додати.
Ви зможете відредагувати Назву та Обкладинку книги пізніше. Ви зможете відредагувати Назву та Обкладинку книги пізніше.
Процес завантаження може зайняти декілька хвилин.
</string> </string>
<string name="move_books_description"> <string name="move_books_description">
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s). Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).

View file

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