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:
parent
22ed828ee6
commit
4e948bc5d7
44 changed files with 608 additions and 470 deletions
|
|
@ -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<Book>
|
||||
?: 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<Book>
|
||||
?: 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()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package ua.acclorite.book_story.data.parser
|
||||
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import java.io.File
|
||||
|
||||
|
||||
interface FileParser {
|
||||
|
||||
suspend fun parse(file: File): Book?
|
||||
suspend fun parse(file: File): Pair<Book, CoverImage?>?
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package ua.acclorite.book_story.data.parser.epub
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.siegmann.epublib.epub.EpubReader
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import ua.acclorite.book_story.util.UIText
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
|
@ -15,7 +15,7 @@ import javax.inject.Inject
|
|||
|
||||
class EpubFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Book? {
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
if (!file.name.endsWith(".epub")) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
try {
|
||||
val epubReader = EpubReader()
|
||||
val book = withContext(Dispatchers.IO) {
|
||||
epubReader.readEpub(FileInputStream(file))
|
||||
FileInputStream(file).use {
|
||||
epubReader.readEpub(it)
|
||||
}
|
||||
}
|
||||
|
||||
val metadata = book.metadata
|
||||
|
|
@ -42,8 +44,6 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return Book(
|
||||
id = null,
|
||||
title = title,
|
||||
|
|
@ -55,8 +55,8 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = coverImage?.asImageBitmap()
|
||||
)
|
||||
coverImage = null
|
||||
) to coverImage
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ import ua.acclorite.book_story.data.parser.FileParser
|
|||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.domain.model.StringWithId
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import ua.acclorite.book_story.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class PdfFileParser @Inject constructor(private val application: Application) : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Book? {
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
if (!file.name.endsWith(".pdf")) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -46,7 +47,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
|||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
)
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import ua.acclorite.book_story.data.parser.FileParser
|
|||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.domain.model.StringWithId
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import ua.acclorite.book_story.util.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TxtFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): Book? {
|
||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||
if (!file.name.endsWith(".txt")) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
|||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
)
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Resource<List<Book>>> {
|
||||
return flow {
|
||||
|
|
@ -101,12 +98,46 @@ class BookRepositoryImpl @Inject constructor(
|
|||
return database.findBookById(id)
|
||||
}
|
||||
|
||||
override suspend fun insertBooks(books: List<Book>) {
|
||||
database.insertBooks(books.map { bookMapper.toBookEntity(it) })
|
||||
override suspend fun insertBooks(
|
||||
books: List<Pair<Book, CoverImage?>>
|
||||
) {
|
||||
val booksWithCover = books.map {
|
||||
var uri = ""
|
||||
|
||||
if (it.second != null) {
|
||||
try {
|
||||
uri = "${UUID.randomUUID()}.webp"
|
||||
application.openFileOutput(uri, 0x0000).use { stream ->
|
||||
if (
|
||||
!it.second!!.compress(
|
||||
Bitmap.CompressFormat.WEBP,
|
||||
30,
|
||||
stream
|
||||
)
|
||||
) {
|
||||
throw Exception("Couldn't save cover image")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
uri = ""
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
val book = it.first.copy(
|
||||
coverImage = if (uri.isNotBlank()) {
|
||||
Uri.fromFile(File("${application.filesDir}/$uri"))
|
||||
} else null
|
||||
)
|
||||
|
||||
bookMapper.toBookEntity(book)
|
||||
}
|
||||
|
||||
database.insertBooks(booksWithCover)
|
||||
}
|
||||
|
||||
override suspend fun updateBooks(books: List<Book>) {
|
||||
// without text
|
||||
// without text and cover image
|
||||
database.updateBooks(
|
||||
books.map {
|
||||
val book = database.findBookById(it.id ?: return)
|
||||
|
|
@ -114,7 +145,8 @@ class BookRepositoryImpl @Inject constructor(
|
|||
it.copy(
|
||||
text = book.text
|
||||
.split("\n")
|
||||
.map { line -> StringWithId(line.trim()) }
|
||||
.map { line -> StringWithId(line.trim()) },
|
||||
coverImage = if (book.image != null) Uri.parse(book.image) else null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -122,25 +154,99 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun updateBooksWithText(books: List<Book>) {
|
||||
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
|
||||
}
|
||||
|
||||
override suspend fun deleteBooks(books: List<Book>) {
|
||||
// without text
|
||||
database.deleteBooks(
|
||||
// without cover image
|
||||
database.updateBooks(
|
||||
books.map {
|
||||
val book = database.findBookById(it.id ?: return)
|
||||
bookMapper.toBookEntity(
|
||||
it.copy(
|
||||
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<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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun <T> retrieveDataFromDataStore(
|
||||
key: Preferences.Key<T>,
|
||||
defaultValue: T
|
||||
|
|
@ -230,55 +336,12 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>> {
|
||||
return flow {
|
||||
emit(Resource.Loading(true))
|
||||
|
||||
val text = if (file.name.endsWith(".txt")) {
|
||||
txtTextParser.parse(file)
|
||||
} else if (file.name.endsWith(".pdf")) {
|
||||
pdfTextParser.parse(file)
|
||||
} else if (file.name.endsWith(".epub")) {
|
||||
epubTextParser.parse(file)
|
||||
} else {
|
||||
emit(Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)))
|
||||
return@flow
|
||||
}
|
||||
|
||||
if (text is Resource.Success && text.data == null) {
|
||||
emit(
|
||||
Resource.Error(
|
||||
text.message
|
||||
?: UIText.StringResource(R.string.error_file_empty)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
when (text) {
|
||||
is Resource.Success -> emit(Resource.Success(text.data))
|
||||
is Resource.Error -> emit(
|
||||
Resource.Error(
|
||||
text.message
|
||||
?: UIText.StringResource(R.string.error_something_went_wrong)
|
||||
)
|
||||
)
|
||||
|
||||
is Resource.Loading -> Unit
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> {
|
||||
val books = mutableListOf<NullableBook>()
|
||||
|
||||
for (file in files) {
|
||||
val parsedBook = if (file.name.endsWith(".txt")) {
|
||||
txtFileParser.parse(file)
|
||||
} else if (file.name.endsWith(".pdf")) {
|
||||
pdfFileParser.parse(file)
|
||||
} else if (file.name.endsWith(".epub")) {
|
||||
epubFileParser.parse(file)
|
||||
val parsedBook = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
|
||||
fileParser.parse(file)
|
||||
} else {
|
||||
books.add(
|
||||
NullableBook.Null(
|
||||
|
|
@ -289,12 +352,8 @@ class BookRepositoryImpl @Inject constructor(
|
|||
continue
|
||||
}
|
||||
|
||||
val parsedText = if (file.name.endsWith(".txt")) {
|
||||
txtTextParser.parse(file)
|
||||
} else if (file.name.endsWith(".pdf")) {
|
||||
pdfTextParser.parse(file)
|
||||
} else if (file.name.endsWith(".epub")) {
|
||||
epubTextParser.parse(file)
|
||||
val parsedText = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
|
||||
textParser.parse(file)
|
||||
} else {
|
||||
books.add(
|
||||
NullableBook.Null(
|
||||
|
|
@ -338,8 +397,8 @@ class BookRepositoryImpl @Inject constructor(
|
|||
books.add(
|
||||
NullableBook.NotNull(
|
||||
Pair(
|
||||
parsedBook.copy(text = parsedText.data),
|
||||
true
|
||||
parsedBook.first.copy(text = parsedText.data),
|
||||
parsedBook.second
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
package ua.acclorite.book_story.domain.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import ua.acclorite.book_story.util.UIText
|
||||
|
||||
@Immutable
|
||||
sealed class NullableBook(
|
||||
val book: Pair<Book, Boolean>?,
|
||||
val book: Pair<Book, CoverImage?>?,
|
||||
val fileName: String?,
|
||||
val message: UIText?
|
||||
) {
|
||||
class NotNull(book: Pair<Book, Boolean>) : NullableBook(book, null, null)
|
||||
class NotNull(book: Pair<Book, CoverImage?>) : NullableBook(book, null, null)
|
||||
class Null(
|
||||
fileName: String,
|
||||
message: UIText?
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import ua.acclorite.book_story.data.local.dto.BookEntity
|
|||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.History
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.model.StringWithId
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import ua.acclorite.book_story.util.Resource
|
||||
import java.io.File
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ interface BookRepository {
|
|||
): BookEntity
|
||||
|
||||
suspend fun insertBooks(
|
||||
books: List<Book>
|
||||
books: List<Pair<Book, CoverImage?>>
|
||||
)
|
||||
|
||||
suspend fun updateBooks(
|
||||
|
|
@ -44,6 +44,11 @@ interface BookRepository {
|
|||
books: List<Book>
|
||||
)
|
||||
|
||||
suspend fun updateCoverImageOfBook(
|
||||
bookWithOldCover: Book,
|
||||
newCoverImage: CoverImage?
|
||||
)
|
||||
|
||||
suspend fun deleteBooks(
|
||||
books: List<Book>
|
||||
)
|
||||
|
|
@ -60,11 +65,8 @@ interface BookRepository {
|
|||
|
||||
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
||||
|
||||
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
|
||||
|
||||
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
||||
|
||||
|
||||
suspend fun insertHistory(history: List<History>)
|
||||
|
||||
suspend fun getHistory(): Flow<Resource<List<History>>>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ package ua.acclorite.book_story.domain.use_case
|
|||
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.util.CoverImage
|
||||
import javax.inject.Inject
|
||||
|
||||
class InsertBooks @Inject constructor(private val repository: BookRepository) {
|
||||
suspend fun execute(books: List<Book>) {
|
||||
suspend fun execute(books: List<Pair<Book, CoverImage?>>) {
|
||||
repository.insertBooks(books)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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, " "))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package ua.acclorite.book_story.presentation.screens.book_info
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
|
|
@ -52,9 +50,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
||||
|
|
@ -81,11 +77,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
|
|||
import ua.acclorite.book_story.ui.DefaultTransition
|
||||
import ua.acclorite.book_story.ui.Transitions
|
||||
import ua.acclorite.book_story.ui.elevation
|
||||
import ua.acclorite.book_story.util.Constants
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun BookInfoScreen(
|
||||
viewModel: BookInfoViewModel,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
browseViewModel: BrowseViewModel,
|
||||
historyViewModel: HistoryViewModel,
|
||||
|
|
@ -93,20 +89,6 @@ fun BookInfoScreen(
|
|||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel by (context as ComponentActivity).viewModels<BookInfoViewModel>(
|
||||
extrasProducer = {
|
||||
val book = navigator.retrieveArgument("book") as? Book
|
||||
if (book == null) {
|
||||
navigator.navigateBack()
|
||||
}
|
||||
|
||||
context.defaultViewModelCreationExtras
|
||||
.withCreationCallback<BookInfoViewModel.Factory> { factory ->
|
||||
factory.create(book ?: Constants.EMPTY_BOOK)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val listState = rememberLazyListState()
|
||||
|
|
@ -302,7 +284,7 @@ fun BookInfoScreen(
|
|||
onClick = {
|
||||
navigator.navigate(
|
||||
Screen.READER, false, Argument(
|
||||
"book", state.book
|
||||
"book", state.book.id
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.ui.DefaultTransition
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
|
@ -45,7 +46,7 @@ import java.util.Locale
|
|||
*/
|
||||
@Composable
|
||||
fun BrowseFileItem(
|
||||
file: Pair<File, Boolean>,
|
||||
file: Pair<File, Selected>,
|
||||
hasSelectedFiles: Boolean,
|
||||
modifier: Modifier, onClick: () -> Unit
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ fun BrowseAddingDialog(
|
|||
description = stringResource(id = R.string.add_books_description),
|
||||
actionText = stringResource(id = R.string.add),
|
||||
isActionEnabled = !state.isBooksLoading &&
|
||||
state.selectedBooks
|
||||
.filterIsInstance<NullableBook.NotNull>()
|
||||
.isNotEmpty(),
|
||||
state.selectedBooks.any { it.first is NullableBook.NotNull },
|
||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
||||
onAction = {
|
||||
viewModel.onEvent(
|
||||
|
|
@ -91,7 +89,7 @@ fun BrowseAddingDialog(
|
|||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
book.message?.asString(context),
|
||||
book.first.message?.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,14 @@ import androidx.compose.ui.unit.dp
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
|
||||
/**
|
||||
* Adding dialog item.
|
||||
*/
|
||||
@Composable
|
||||
fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
||||
if (result is NullableBook.NotNull) {
|
||||
fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boolean) -> Unit) {
|
||||
if (result.first is NullableBook.NotNull) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -44,14 +45,14 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
|||
Modifier.weight(0.85f)
|
||||
) {
|
||||
Text(
|
||||
text = result.book!!.first.title,
|
||||
text = result.first.book!!.first.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = result.book.first.author.asString(),
|
||||
text = result.first.book!!.first.author.asString(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
|
|
@ -59,12 +60,12 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
|||
)
|
||||
}
|
||||
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
|
||||
CustomCheckbox(selected = result.book!!.second)
|
||||
CustomCheckbox(selected = result.second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val icon =
|
||||
if (result.message?.asString() == stringResource(R.string.error_file_encrypted)) {
|
||||
if (result.first.message?.asString() == stringResource(R.string.error_file_encrypted)) {
|
||||
Icons.Default.Lock
|
||||
} else {
|
||||
Icons.Default.Error
|
||||
|
|
@ -87,7 +88,7 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
|||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = result.fileName!!,
|
||||
text = result.first.fileName!!,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
maxLines = 2,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.presentation.data.Navigator
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
import java.io.File
|
||||
|
||||
sealed class BrowseEvent {
|
||||
|
|
@ -29,8 +30,8 @@ sealed class BrowseEvent {
|
|||
val hideErrorMessage: () -> Unit
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnSelectFile(val file: Pair<File, Boolean>) : BrowseEvent()
|
||||
data class OnSelectBook(val book: NullableBook) : BrowseEvent()
|
||||
data class OnSelectFile(val file: Pair<File, Selected>) : BrowseEvent()
|
||||
data class OnSelectBook(val book: Pair<NullableBook, Selected>) : BrowseEvent()
|
||||
data object OnSearchShowHide : BrowseEvent()
|
||||
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
|
||||
data object OnClearSelectedFiles : BrowseEvent()
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ package ua.acclorite.book_story.presentation.screens.browse.data
|
|||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
import java.io.File
|
||||
|
||||
@Immutable
|
||||
data class BrowseState(
|
||||
val selectableFiles: List<Pair<File, Boolean>> = emptyList(),
|
||||
val selectableFiles: List<Pair<File, Selected>> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(0, 0),
|
||||
|
||||
val isLoading: Boolean = true,
|
||||
|
|
@ -25,6 +26,6 @@ data class BrowseState(
|
|||
val hasFocused: Boolean = false,
|
||||
|
||||
val showAddingDialog: Boolean = false,
|
||||
val selectedBooks: List<NullableBook> = emptyList(),
|
||||
val selectedBooks: List<Pair<NullableBook, Selected>> = emptyList(),
|
||||
val isBooksLoading: Boolean = false
|
||||
)
|
||||
|
|
@ -199,13 +199,12 @@ class BrowseViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
val editedList = _state.value.selectedBooks.toMutableList()
|
||||
editedList[indexOfFile] = NullableBook.NotNull(
|
||||
editedList[indexOfFile].book!!.copy(
|
||||
second = !editedList[indexOfFile].book!!.second
|
||||
)
|
||||
)
|
||||
editedList[indexOfFile] = editedList[indexOfFile].first to
|
||||
!editedList[indexOfFile].second
|
||||
|
||||
if (!editedList.any { it.book?.second == true }) {
|
||||
if (!editedList.filter { it.first is NullableBook.NotNull }
|
||||
.any { it.second }
|
||||
) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +307,7 @@ class BrowseViewModel @Inject constructor(
|
|||
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedBooks = books,
|
||||
selectedBooks = books.map { book -> book to true },
|
||||
isBooksLoading = false
|
||||
)
|
||||
}
|
||||
|
|
@ -318,9 +317,9 @@ class BrowseViewModel @Inject constructor(
|
|||
is BrowseEvent.OnAddBooks -> {
|
||||
viewModelScope.launch {
|
||||
val booksToInsert = _state.value.selectedBooks
|
||||
.filterIsInstance<NullableBook.NotNull>()
|
||||
.filter { it.book!!.second }
|
||||
.map { it.book!!.first }
|
||||
.filter { it.first is NullableBook.NotNull }
|
||||
.filter { it.second }
|
||||
.map { it.first.book!! }
|
||||
|
||||
if (booksToInsert.isEmpty()) {
|
||||
return@launch
|
||||
|
|
@ -330,19 +329,21 @@ class BrowseViewModel @Inject constructor(
|
|||
val books = fastGetBooks.execute("")
|
||||
|
||||
event.resetScroll()
|
||||
_state.update {
|
||||
it.copy(
|
||||
showAddingDialog = false
|
||||
)
|
||||
}
|
||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||
onEvent(BrowseEvent.OnLoadList)
|
||||
|
||||
event.navigator.navigate(
|
||||
Screen.LIBRARY,
|
||||
false,
|
||||
Argument("added_books", books)
|
||||
)
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
showAddingDialog = false,
|
||||
selectableFiles = emptyList(),
|
||||
listState = LazyListState(0, 0)
|
||||
)
|
||||
}
|
||||
onEvent(BrowseEvent.OnLoadList)
|
||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
|
|||
import ua.acclorite.book_story.ui.DefaultTransition
|
||||
import ua.acclorite.book_story.ui.Transitions
|
||||
import ua.acclorite.book_story.ui.elevation
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(
|
||||
|
|
@ -304,14 +305,14 @@ fun LibraryScreen(
|
|||
) {
|
||||
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
|
||||
var categoryIsLoading by remember { mutableStateOf(true) }
|
||||
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Boolean>>() }
|
||||
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Selected>>() }
|
||||
val category = remember { Category.entries[index] }
|
||||
|
||||
LaunchedEffect(state.books) {
|
||||
categorizedBooks.clear()
|
||||
categorizedBooks.addAll(state.books.filter { it.first.category == category }
|
||||
.sortedWith(
|
||||
compareByDescending<Pair<Book, Boolean>> { it.first.lastOpened }
|
||||
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
|
||||
.thenBy { it.first.title }
|
||||
)
|
||||
)
|
||||
|
|
@ -340,7 +341,7 @@ fun LibraryScreen(
|
|||
navigator.navigate(
|
||||
Screen.BOOK_INFO,
|
||||
false,
|
||||
Argument("book", it.first)
|
||||
Argument("book", it.first.id)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -353,7 +354,7 @@ fun LibraryScreen(
|
|||
navigator.navigate(
|
||||
Screen.READER,
|
||||
false,
|
||||
Argument("book", it.first)
|
||||
Argument("book", it.first.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.screens.library.components
|
|||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -35,11 +34,13 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.presentation.data.removeDigits
|
||||
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||
import ua.acclorite.book_story.ui.elevation
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
|
||||
/**
|
||||
* Library list element item.
|
||||
|
|
@ -47,7 +48,7 @@ import ua.acclorite.book_story.ui.elevation
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LibraryBookItem(
|
||||
book: Pair<Book, Boolean>,
|
||||
book: Pair<Book, Selected>,
|
||||
modifier: Modifier = Modifier,
|
||||
onCoverImageClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
|
|
@ -103,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(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.pager.PagerState
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
|
||||
sealed class LibraryEvent {
|
||||
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
|
||||
|
|
@ -17,7 +18,7 @@ sealed class LibraryEvent {
|
|||
data object OnSearchShowHide : LibraryEvent()
|
||||
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
|
||||
data class OnSearchQueryChange(val query: String) : LibraryEvent()
|
||||
data class OnSelectBook(val book: Pair<Book, Boolean>, val select: Boolean? = null) :
|
||||
data class OnSelectBook(val book: Pair<Book, Selected>, val select: Boolean? = null) :
|
||||
LibraryEvent()
|
||||
|
||||
data object OnClearSelectedBooks : LibraryEvent()
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ package ua.acclorite.book_story.presentation.screens.library.data
|
|||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.util.Selected
|
||||
|
||||
@Immutable
|
||||
data class LibraryState(
|
||||
val books: List<Pair<Book, Boolean>> = emptyList(),
|
||||
val books: List<Pair<Book, Selected>> = emptyList(),
|
||||
val isLoading: Boolean = true,
|
||||
val isRefreshing: Boolean = false,
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Build
|
|||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
|
|
@ -51,12 +50,10 @@ import androidx.compose.ui.text.style.LineBreak
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
|
|
@ -79,6 +76,7 @@ import ua.acclorite.book_story.util.Constants
|
|||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun ReaderScreen(
|
||||
viewModel: ReaderViewModel,
|
||||
mainViewModel: MainViewModel,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
historyViewModel: HistoryViewModel,
|
||||
|
|
@ -87,20 +85,6 @@ fun ReaderScreen(
|
|||
val context = LocalContext.current as ComponentActivity
|
||||
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
|
||||
|
||||
val viewModel by context.viewModels<ReaderViewModel>(
|
||||
extrasProducer = {
|
||||
val book = navigator.retrieveArgument("book") as? Book
|
||||
if (book == null) {
|
||||
navigator.navigateBack()
|
||||
}
|
||||
|
||||
context.defaultViewModelCreationExtras
|
||||
.withCreationCallback<ReaderViewModel.Factory> { factory ->
|
||||
factory.create(book ?: Constants.EMPTY_BOOK)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
val state by viewModel.state.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
|
|
@ -296,13 +280,16 @@ fun ReaderScreen(
|
|||
itemsIndexed(
|
||||
state.book.text, key = { _, key -> key.id }
|
||||
) { index, line ->
|
||||
val text = remember { "${if (paragraphIndentation) " " else ""}${line.line}" }
|
||||
val color = remember { Color(fontColor.toULong()) }
|
||||
val lineHeightSp = remember { (fontSize + lineHeight).sp }
|
||||
val text =
|
||||
remember(paragraphIndentation) { "${if (paragraphIndentation) " " else ""}${line.line}" }
|
||||
val fontCol = remember(fontColor) { Color(fontColor.toULong()) }
|
||||
val backgroundCol =
|
||||
remember(backgroundColor) { Color(backgroundColor.toULong()) }
|
||||
val lineHeightSp = remember(fontSize, lineHeight) { (fontSize + lineHeight).sp }
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.background(Color(backgroundColor.toULong()))
|
||||
.background(backgroundCol)
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = if (index == 0) 36.dp else 0.dp,
|
||||
|
|
@ -314,7 +301,7 @@ fun ReaderScreen(
|
|||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
color = fontCol,
|
||||
|
||||
style = TextStyle(
|
||||
lineBreak = LineBreak.Paragraph
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
|||
true,
|
||||
Argument(
|
||||
"book",
|
||||
state.book
|
||||
state.book.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ object Constants {
|
|||
)
|
||||
|
||||
val EMPTY_BOOK = Book(
|
||||
0,
|
||||
-1,
|
||||
"",
|
||||
UIText.StringValue(""),
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package ua.acclorite.book_story.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
typealias CoverImage = Bitmap
|
||||
typealias Selected = Boolean
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
<string name="add_books_description">
|
||||
Ви можете вибрати книги, які хочете додати.
|
||||
Ви зможете відредагувати Назву та Обкладинку книги пізніше.
|
||||
Процес завантаження може зайняти декілька хвилин.
|
||||
</string>
|
||||
<string name="move_books_description">
|
||||
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<resources>
|
||||
<!-- Basic Strings -->
|
||||
<string name="app_version" translatable="false">0.9.2</string>
|
||||
<string name="app_version" translatable="false">0.9.3</string>
|
||||
<string name="app_name">Book\'s Story</string>
|
||||
|
||||
<!-- Screens -->
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
<string name="add_books_description">
|
||||
You can choose which books you want to add.
|
||||
You can edit Title and Cover image of the book later.
|
||||
The loading process can take up to several minutes.
|
||||
</string>
|
||||
<string name="move_books_description">
|
||||
Choose the category to which you want to move all the selected books(%1$s).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue