diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 52eefaa9..d00a6ede 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/ua/acclorite/book_story/Activity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt index a44d066a..c687bce6 100644 --- a/app/src/main/java/ua/acclorite/book_story/Activity.kt +++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt @@ -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,150 +90,155 @@ 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 - BooksHistoryResurrectionTheme( - theme = theme, - isDark = darkTheme.isDark() - ) { - NavigationHost(startScreen = Screen.LIBRARY) { - val currentScreen by this.getCurrentScreen().collectAsState() + if (isLoaded) { + BooksHistoryResurrectionTheme( + theme = theme, + isDark = darkTheme.isDark() + ) { + NavigationHost(startScreen = Screen.LIBRARY) { + val currentScreen by this.getCurrentScreen().collectAsState() - AnimatedVisibility( - visible = currentScreen == Screen.LIBRARY || - currentScreen == Screen.HISTORY || - currentScreen == Screen.BROWSE, - enter = Transitions.BackSlidingTransitionIn, - exit = Transitions.SlidingTransitionOut - ) { - Scaffold( - bottomBar = { - if (!tabletUI) { - BottomNavigationBar(navigator = this@NavigationHost) - } - }, - containerColor = MaterialTheme.colorScheme.surface + AnimatedVisibility( + visible = currentScreen == Screen.LIBRARY || + currentScreen == Screen.HISTORY || + currentScreen == Screen.BROWSE, + enter = Transitions.BackSlidingTransitionIn, + exit = Transitions.SlidingTransitionOut ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding( - start = if (tabletUI) 80.dp else 0.dp, - bottom = it.calculateBottomPadding() - ) + Scaffold( + bottomBar = { + if (!tabletUI) { + BottomNavigationBar(navigator = this@NavigationHost) + } + }, + containerColor = MaterialTheme.colorScheme.surface ) { - composable(screen = Screen.LIBRARY) { - @Suppress("UNCHECKED_CAST") - LibraryScreen( - viewModel = libraryViewModel, - historyViewModel = historyViewModel, - browseViewModel = browseViewModel, - navigator = this@NavigationHost, - addedBooks = retrieveArgument("added_books") as? List - ?: emptyList() - ) + Box( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (tabletUI) 80.dp else 0.dp, + bottom = it.calculateBottomPadding() + ) + ) { + composable(screen = Screen.LIBRARY) { + @Suppress("UNCHECKED_CAST") + LibraryScreen( + viewModel = libraryViewModel, + historyViewModel = historyViewModel, + browseViewModel = browseViewModel, + navigator = this@NavigationHost, + addedBooks = retrieveArgument("added_books") as? List + ?: emptyList() + ) + } + + composable(screen = Screen.HISTORY) { + HistoryScreen( + viewModel = historyViewModel, + libraryViewModel = libraryViewModel, + navigator = this@NavigationHost + ) + } + + composable(screen = Screen.BROWSE) { + BrowseScreen( + viewModel = browseViewModel, + libraryViewModel = libraryViewModel, + navigator = this@NavigationHost + ) + } } - composable(screen = Screen.HISTORY) { - HistoryScreen( - viewModel = historyViewModel, - libraryViewModel = libraryViewModel, - navigator = this@NavigationHost - ) + if (tabletUI) { + CustomNavigationRail(navigator = this@NavigationHost) } - - composable(screen = Screen.BROWSE) { - BrowseScreen( - viewModel = browseViewModel, - libraryViewModel = libraryViewModel, - navigator = this@NavigationHost - ) - } - } - - if (tabletUI) { - CustomNavigationRail(navigator = this@NavigationHost) } } - } - // Book Info - composable( - screen = Screen.BOOK_INFO, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - BookInfoScreen( - libraryViewModel = libraryViewModel, - browseViewModel = browseViewModel, - historyViewModel = historyViewModel, - navigator = this@NavigationHost - ) - } - composable( - screen = Screen.READER, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - ReaderScreen( - mainViewModel = mainViewModel, - libraryViewModel = libraryViewModel, - historyViewModel = historyViewModel, - navigator = this@NavigationHost - ) - } + // Book Info + composable( + screen = Screen.BOOK_INFO, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + BookInfoScreen( + viewModel = bookInfoViewModel, + libraryViewModel = libraryViewModel, + browseViewModel = browseViewModel, + historyViewModel = historyViewModel, + navigator = this@NavigationHost + ) + } + composable( + screen = Screen.READER, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + ReaderScreen( + viewModel = readerViewModel, + mainViewModel = mainViewModel, + libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, + navigator = this@NavigationHost + ) + } - // Settings - composable( - screen = Screen.SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - SettingsScreen( - navigator = this@NavigationHost - ) - } + // Settings + composable( + screen = Screen.SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + SettingsScreen( + navigator = this@NavigationHost + ) + } - // Nested categories - composable( - screen = Screen.GENERAL_SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - GeneralSettings( - mainViewModel = mainViewModel, - navigator = this@NavigationHost - ) - } - composable( - screen = Screen.APPEARANCE_SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - AppearanceSettings( - mainViewModel = mainViewModel, - navigator = this@NavigationHost - ) - } - composable( - screen = Screen.READER_SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - ReaderSettings( - mainViewModel = mainViewModel, - navigator = this@NavigationHost - ) - } + // Nested categories + composable( + screen = Screen.GENERAL_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + GeneralSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } + composable( + screen = Screen.APPEARANCE_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + AppearanceSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } + composable( + screen = Screen.READER_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + ReaderSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } // Start screen (later) // composable(screen = Screen.START) { // StartScreen() // } + } } } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt index bf178666..0235feff 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt index 719c5dc2..85810b09 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt @@ -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() - } -} +) diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt index 5d54ec6f..95b1c9e0 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt @@ -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 ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt index 270f9846..41974c7c 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt @@ -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? } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt new file mode 100644 index 00000000..27b4f864 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt @@ -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? { + 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 + } +} + + + + + + + + diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt new file mode 100644 index 00000000..e98b60e5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt @@ -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> { + 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)) + } +} + + + + + + + + diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index 1d2493ee..eea796a2 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -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? { 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 } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index 5b894e1e..eb458ebb 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -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? { 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 } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index f2b9485d..92cfcfd6 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -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? { 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 } diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index faa10015..d8d6e28a 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -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,29 +22,27 @@ 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 fileParser: FileParser, + private val textParser: TextParser - private val txtTextParser: TxtTextParser, - private val pdfTextParser: PdfTextParser, - private val epubTextParser: EpubTextParser, - - ) : BookRepository { +) : BookRepository { override suspend fun getBooks(query: String): Flow>> { return flow { @@ -101,12 +98,46 @@ class BookRepositoryImpl @Inject constructor( return database.findBookById(id) } - override suspend fun insertBooks(books: List) { - database.insertBooks(books.map { bookMapper.toBookEntity(it) }) + override suspend fun insertBooks( + books: List> + ) { + 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) { - // 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,25 +154,99 @@ class BookRepositoryImpl @Inject constructor( } override suspend fun updateBooksWithText(books: List) { - database.updateBooks(books.map { bookMapper.toBookEntity(it) }) - } - - override suspend fun deleteBooks(books: List) { - // without text - database.deleteBooks( + // without cover image + database.updateBooks( books.map { val book = database.findBookById(it.id ?: return) bookMapper.toBookEntity( it.copy( - text = book.text - .split("\n") - .map { line -> StringWithId(line.trim()) } + 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()) }, + coverImage = newCoverImageUri + ) + + database.updateBooks( + listOf( + bookMapper.toBookEntity( + bookWithNewCover + ) + ) + ) + } + + override suspend fun deleteBooks(books: List) { + 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 + } + ) + } + override suspend fun retrieveDataFromDataStore( key: Preferences.Key, defaultValue: T @@ -230,55 +336,12 @@ class BookRepositoryImpl @Inject constructor( } } - override suspend fun getBookTextFromFile(file: File): Flow>> { - 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): List { val books = mutableListOf() for (file in files) { - val parsedBook = if (file.name.endsWith(".txt")) { - txtFileParser.parse(file) - } else if (file.name.endsWith(".pdf")) { - pdfFileParser.parse(file) - } else if (file.name.endsWith(".epub")) { - epubFileParser.parse(file) + 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 ) ) ) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt index 0d72a8b0..124612bc 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.domain.model +import android.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? ) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt index 7af9a63c..88ee5acb 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt @@ -1,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?, + val book: Pair?, val fileName: String?, val message: UIText? ) { - class NotNull(book: Pair) : NullableBook(book, null, null) + class NotNull(book: Pair) : NullableBook(book, null, null) class Null( fileName: String, message: UIText? diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index 30b09407..a7d8853e 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -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 + books: List> ) suspend fun updateBooks( @@ -44,6 +44,11 @@ interface BookRepository { books: List ) + suspend fun updateCoverImageOfBook( + bookWithOldCover: Book, + newCoverImage: CoverImage? + ) + suspend fun deleteBooks( books: List ) @@ -60,11 +65,8 @@ interface BookRepository { suspend fun getFilesFromDevice(query: String = ""): Flow>> - suspend fun getBookTextFromFile(file: File): Flow>> - suspend fun getBooksFromFiles(files: List): List - suspend fun insertHistory(history: List) suspend fun getHistory(): Flow>> diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt index 9edadf50..5caeabcb 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt @@ -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) { + suspend fun execute(books: List>) { repository.insertBooks(books) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt new file mode 100644 index 00000000..4e574d7d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt index 39577ccc..afc1c2e7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt @@ -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, " ")) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt index 43c31bea..6b23e85f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt @@ -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( - extrasProducer = { - val book = navigator.retrieveArgument("book") as? Book - if (book == null) { - navigator.navigateBack() - } - - context.defaultViewModelCreationExtras - .withCreationCallback { 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 ) ) }, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt index 722e60ed..867c5c6a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt @@ -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 ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt index 6f81b150..89075bc0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt @@ -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,25 +76,25 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { } ) ) { - Icon( - imageVector = Icons.Default.Image, - contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(0.7f) - .aspectRatio(1f), - tint = MaterialTheme.elevation(12.dp) - ) - if (book.coverImage != null) { - Image( - bitmap = book.coverImage, + 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), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(0.7f) + .aspectRatio(1f), + tint = MaterialTheme.elevation(12.dp) + ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt index 9e6f8a28..650e4522 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt index 9d3b08a2..f6533883 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt @@ -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 - } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt index d81c78a3..4931fa47 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt @@ -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: Pair, hasSelectedFiles: Boolean, modifier: Modifier, onClick: () -> Unit ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt index 6e50c3c1..6e822334 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt @@ -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() - .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() } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt index 307eb37f..1e23efb3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt @@ -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, 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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt index 17e6b1bb..d641538b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt @@ -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) : BrowseEvent() - data class OnSelectBook(val book: NullableBook) : BrowseEvent() + data class OnSelectFile(val file: Pair) : BrowseEvent() + data class OnSelectBook(val book: Pair) : BrowseEvent() data object OnSearchShowHide : BrowseEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent() data object OnClearSelectedFiles : BrowseEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt index e6a412f4..fb453989 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt @@ -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> = emptyList(), + val selectableFiles: List> = 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 = emptyList(), + val selectedBooks: List> = emptyList(), val isBooksLoading: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt index f72c146c..c48e0fd1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt @@ -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() - .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) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt index 4d269075..2390b880 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt index efc4efd0..105b1562 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt @@ -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,27 +77,27 @@ fun HistoryItem( .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation()) ) { - Icon( - imageVector = Icons.Default.Image, - contentDescription = stringResource( - id = R.string.cover_image_not_found_content_desc - ), - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(0.7f) - .aspectRatio(1f), - tint = MaterialTheme.elevation(12.dp) - ) - if (book.coverImage != null) { - Image( - bitmap = book.coverImage, + 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( + id = R.string.cover_image_not_found_content_desc + ), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(0.7f) + .aspectRatio(1f), + tint = MaterialTheme.elevation(12.dp) + ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt index 088ed1ea..e38e38bc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt @@ -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>() } + val categorizedBooks = remember { mutableStateListOf>() } val category = remember { Category.entries[index] } LaunchedEffect(state.books) { categorizedBooks.clear() categorizedBooks.addAll(state.books.filter { it.first.category == category } .sortedWith( - compareByDescending> { it.first.lastOpened } + compareByDescending> { 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) ) } ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt index 69db8aaf..10b956a2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt @@ -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: Pair, modifier: Modifier = Modifier, onCoverImageClick: () -> Unit, onLongClick: () -> Unit, @@ -103,27 +104,27 @@ fun LibraryBookItem( } ) ) { - Icon( - imageVector = Icons.Default.Image, - contentDescription = stringResource( - id = R.string.cover_image_not_found_content_desc - ), - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(0.7f) - .aspectRatio(1f), - tint = MaterialTheme.elevation(12.dp) - ) - if (book.first.coverImage != null) { - Image( - bitmap = book.first.coverImage!!, + 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( + id = R.string.cover_image_not_found_content_desc + ), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(0.7f) + .aspectRatio(1f), + tint = MaterialTheme.elevation(12.dp) + ) } Text( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt index a5967bdc..ed83a084 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt @@ -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) : 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, val select: Boolean? = null) : + data class OnSelectBook(val book: Pair, val select: Boolean? = null) : LibraryEvent() data object OnClearSelectedBooks : LibraryEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt index 22794824..deca028e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt @@ -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> = emptyList(), + val books: List> = emptyList(), val isLoading: Boolean = true, val isRefreshing: Boolean = false, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt index aed7ebdb..d963d002 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt @@ -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( - extrasProducer = { - val book = navigator.retrieveArgument("book") as? Book - if (book == null) { - navigator.navigateBack() - } - - context.defaultViewModelCreationExtras - .withCreationCallback { 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 diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt index e99104be..952b183b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt @@ -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,25 +62,25 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation()) ) { - Icon( - imageVector = Icons.Default.Image, - contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(0.7f) - .aspectRatio(1f), - tint = MaterialTheme.elevation(12.dp) - ) - if (state.book.coverImage != null) { - Image( - bitmap = state.book.coverImage!!, + 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), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(0.7f) + .aspectRatio(1f), + tint = MaterialTheme.elevation(12.dp) + ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt index 80b91f51..d747c949 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt @@ -79,7 +79,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo true, Argument( "book", - state.book + state.book.id ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt index 318d066b..62a91d8a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt index 042913e7..c77f51f1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt @@ -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 - } } diff --git a/app/src/main/java/ua/acclorite/book_story/util/Constants.kt b/app/src/main/java/ua/acclorite/book_story/util/Constants.kt index 7103c7dc..fd69ea8e 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/Constants.kt +++ b/app/src/main/java/ua/acclorite/book_story/util/Constants.kt @@ -177,7 +177,7 @@ object Constants { ) val EMPTY_BOOK = Book( - 0, + -1, "", UIText.StringValue(""), null, diff --git a/app/src/main/java/ua/acclorite/book_story/util/Typealias.kt b/app/src/main/java/ua/acclorite/book_story/util/Typealias.kt new file mode 100644 index 00000000..21734d6f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/util/Typealias.kt @@ -0,0 +1,6 @@ +package ua.acclorite.book_story.util + +import android.graphics.Bitmap + +typealias CoverImage = Bitmap +typealias Selected = Boolean \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a60cebae..71eca021 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -27,6 +27,7 @@ Ви можете вибрати книги, які хочете додати. Ви зможете відредагувати Назву та Обкладинку книги пізніше. + Процес завантаження може зайняти декілька хвилин. Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s). diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 84ccbde7..85fe7ae7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - 0.9.2 + 0.9.3 Book\'s Story @@ -28,6 +28,7 @@ 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. Choose the category to which you want to move all the selected books(%1$s).