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
|
|
@ -15,7 +15,7 @@ android {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.9.2"
|
versionName = "0.9.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||||
import ua.acclorite.book_story.presentation.data.NavigationHost
|
import ua.acclorite.book_story.presentation.data.NavigationHost
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
|
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
|
||||||
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
|
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen
|
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen
|
||||||
|
|
@ -37,6 +38,7 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode
|
||||||
import ua.acclorite.book_story.presentation.screens.library.LibraryScreen
|
import ua.acclorite.book_story.presentation.screens.library.LibraryScreen
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreen
|
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreen
|
||||||
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen
|
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings
|
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
|
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
|
||||||
|
|
@ -57,7 +59,8 @@ class Activity : AppCompatActivity() {
|
||||||
private val libraryViewModel: LibraryViewModel by viewModels()
|
private val libraryViewModel: LibraryViewModel by viewModels()
|
||||||
private val historyViewModel: HistoryViewModel by viewModels()
|
private val historyViewModel: HistoryViewModel by viewModels()
|
||||||
private val browseViewModel: BrowseViewModel by viewModels()
|
private val browseViewModel: BrowseViewModel by viewModels()
|
||||||
|
private val bookInfoViewModel: BookInfoViewModel by viewModels()
|
||||||
|
private val readerViewModel: ReaderViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -87,150 +90,155 @@ class Activity : AppCompatActivity() {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val windowClass = calculateWindowSizeClass(activity = this)
|
val windowClass = calculateWindowSizeClass(activity = this)
|
||||||
|
val isLoaded by mainViewModel.isReady.collectAsState()
|
||||||
|
|
||||||
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
|
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
|
||||||
val darkTheme =
|
val darkTheme =
|
||||||
mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM
|
mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM
|
||||||
val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact
|
val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact
|
||||||
|
|
||||||
BooksHistoryResurrectionTheme(
|
if (isLoaded) {
|
||||||
theme = theme,
|
BooksHistoryResurrectionTheme(
|
||||||
isDark = darkTheme.isDark()
|
theme = theme,
|
||||||
) {
|
isDark = darkTheme.isDark()
|
||||||
NavigationHost(startScreen = Screen.LIBRARY) {
|
) {
|
||||||
val currentScreen by this.getCurrentScreen().collectAsState()
|
NavigationHost(startScreen = Screen.LIBRARY) {
|
||||||
|
val currentScreen by this.getCurrentScreen().collectAsState()
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = currentScreen == Screen.LIBRARY ||
|
visible = currentScreen == Screen.LIBRARY ||
|
||||||
currentScreen == Screen.HISTORY ||
|
currentScreen == Screen.HISTORY ||
|
||||||
currentScreen == Screen.BROWSE,
|
currentScreen == Screen.BROWSE,
|
||||||
enter = Transitions.BackSlidingTransitionIn,
|
enter = Transitions.BackSlidingTransitionIn,
|
||||||
exit = Transitions.SlidingTransitionOut
|
exit = Transitions.SlidingTransitionOut
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
bottomBar = {
|
|
||||||
if (!tabletUI) {
|
|
||||||
BottomNavigationBar(navigator = this@NavigationHost)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Scaffold(
|
||||||
modifier = Modifier
|
bottomBar = {
|
||||||
.fillMaxSize()
|
if (!tabletUI) {
|
||||||
.padding(
|
BottomNavigationBar(navigator = this@NavigationHost)
|
||||||
start = if (tabletUI) 80.dp else 0.dp,
|
}
|
||||||
bottom = it.calculateBottomPadding()
|
},
|
||||||
)
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
composable(screen = Screen.LIBRARY) {
|
Box(
|
||||||
@Suppress("UNCHECKED_CAST")
|
modifier = Modifier
|
||||||
LibraryScreen(
|
.fillMaxSize()
|
||||||
viewModel = libraryViewModel,
|
.padding(
|
||||||
historyViewModel = historyViewModel,
|
start = if (tabletUI) 80.dp else 0.dp,
|
||||||
browseViewModel = browseViewModel,
|
bottom = it.calculateBottomPadding()
|
||||||
navigator = this@NavigationHost,
|
)
|
||||||
addedBooks = retrieveArgument("added_books") as? List<Book>
|
) {
|
||||||
?: emptyList()
|
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) {
|
if (tabletUI) {
|
||||||
HistoryScreen(
|
CustomNavigationRail(navigator = this@NavigationHost)
|
||||||
viewModel = historyViewModel,
|
|
||||||
libraryViewModel = libraryViewModel,
|
|
||||||
navigator = this@NavigationHost
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(screen = Screen.BROWSE) {
|
|
||||||
BrowseScreen(
|
|
||||||
viewModel = browseViewModel,
|
|
||||||
libraryViewModel = libraryViewModel,
|
|
||||||
navigator = this@NavigationHost
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabletUI) {
|
|
||||||
CustomNavigationRail(navigator = this@NavigationHost)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Book Info
|
// Book Info
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.BOOK_INFO,
|
screen = Screen.BOOK_INFO,
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
BookInfoScreen(
|
BookInfoScreen(
|
||||||
libraryViewModel = libraryViewModel,
|
viewModel = bookInfoViewModel,
|
||||||
browseViewModel = browseViewModel,
|
libraryViewModel = libraryViewModel,
|
||||||
historyViewModel = historyViewModel,
|
browseViewModel = browseViewModel,
|
||||||
navigator = this@NavigationHost
|
historyViewModel = historyViewModel,
|
||||||
)
|
navigator = this@NavigationHost
|
||||||
}
|
)
|
||||||
composable(
|
}
|
||||||
screen = Screen.READER,
|
composable(
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
screen = Screen.READER,
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
) {
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
ReaderScreen(
|
) {
|
||||||
mainViewModel = mainViewModel,
|
ReaderScreen(
|
||||||
libraryViewModel = libraryViewModel,
|
viewModel = readerViewModel,
|
||||||
historyViewModel = historyViewModel,
|
mainViewModel = mainViewModel,
|
||||||
navigator = this@NavigationHost
|
libraryViewModel = libraryViewModel,
|
||||||
)
|
historyViewModel = historyViewModel,
|
||||||
}
|
navigator = this@NavigationHost
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.SETTINGS,
|
screen = Screen.SETTINGS,
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested categories
|
// Nested categories
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.GENERAL_SETTINGS,
|
screen = Screen.GENERAL_SETTINGS,
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
GeneralSettings(
|
GeneralSettings(
|
||||||
mainViewModel = mainViewModel,
|
mainViewModel = mainViewModel,
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.APPEARANCE_SETTINGS,
|
screen = Screen.APPEARANCE_SETTINGS,
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
AppearanceSettings(
|
AppearanceSettings(
|
||||||
mainViewModel = mainViewModel,
|
mainViewModel = mainViewModel,
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.READER_SETTINGS,
|
screen = Screen.READER_SETTINGS,
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
ReaderSettings(
|
ReaderSettings(
|
||||||
mainViewModel = mainViewModel,
|
mainViewModel = mainViewModel,
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start screen (later)
|
// Start screen (later)
|
||||||
// composable(screen = Screen.START) {
|
// composable(screen = Screen.START) {
|
||||||
// StartScreen()
|
// 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.book.BookMapperImpl
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
|
||||||
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
|
import ua.acclorite.book_story.data.parser.FileParserImpl
|
||||||
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
|
import ua.acclorite.book_story.data.parser.TextParserImpl
|
||||||
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
|
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -40,4 +44,16 @@ abstract class RepositoryModule {
|
||||||
abstract fun bindHistoryMapper(
|
abstract fun bindHistoryMapper(
|
||||||
historyMapperImpl: HistoryMapperImpl
|
historyMapperImpl: HistoryMapperImpl
|
||||||
): HistoryMapper
|
): HistoryMapper
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindFileParser(
|
||||||
|
fileParserImpl: FileParserImpl
|
||||||
|
): FileParser
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindTextParser(
|
||||||
|
textParserImpl: TextParserImpl
|
||||||
|
): TextParser
|
||||||
}
|
}
|
||||||
|
|
@ -13,19 +13,6 @@ data class BookEntity(
|
||||||
val text: String,
|
val text: String,
|
||||||
val filePath: String,
|
val filePath: String,
|
||||||
val progress: Float,
|
val progress: Float,
|
||||||
val image: ByteArray = byteArrayOf(),
|
val image: String? = null,
|
||||||
val category: Category
|
val category: Category
|
||||||
) {
|
)
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as BookEntity
|
|
||||||
|
|
||||||
return image.contentEquals(other.image)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return image.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,15 @@
|
||||||
package ua.acclorite.book_story.data.mapper.book
|
package ua.acclorite.book_story.data.mapper.book
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.net.Uri
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class BookMapperImpl @Inject constructor() : BookMapper {
|
class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
override suspend fun toBookEntity(book: Book): BookEntity {
|
override suspend fun toBookEntity(book: Book): BookEntity {
|
||||||
val legacyAPI = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
|
||||||
|
|
||||||
val stream = ByteArrayOutputStream()
|
|
||||||
book.coverImage?.asAndroidBitmap()?.compress(
|
|
||||||
if (legacyAPI) Bitmap.CompressFormat.WEBP
|
|
||||||
else Bitmap.CompressFormat.WEBP_LOSSY,
|
|
||||||
0,
|
|
||||||
stream
|
|
||||||
)
|
|
||||||
|
|
||||||
val textAsString = book.text.joinToString("\n") {
|
val textAsString = book.text.joinToString("\n") {
|
||||||
it.line.trim()
|
it.line.trim()
|
||||||
}
|
}
|
||||||
|
|
@ -38,21 +22,13 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
author = book.author.string,
|
author = book.author.string,
|
||||||
text = textAsString,
|
text = textAsString,
|
||||||
description = book.description,
|
description = book.description,
|
||||||
image = stream.toByteArray(),
|
image = if (book.coverImage != null) book.coverImage.toString() else null,
|
||||||
category = book.category
|
category = book.category
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toBook(bookEntity: BookEntity): Book {
|
override suspend fun toBook(bookEntity: BookEntity): Book {
|
||||||
val file = File(bookEntity.filePath)
|
val file = File(bookEntity.filePath)
|
||||||
var image: ImageBitmap? = null
|
|
||||||
|
|
||||||
if (bookEntity.image.isNotEmpty()) {
|
|
||||||
image = BitmapFactory
|
|
||||||
.decodeByteArray(bookEntity.image, 0, bookEntity.image.size)
|
|
||||||
.asImageBitmap()
|
|
||||||
image.prepareToDraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Book(
|
return Book(
|
||||||
id = bookEntity.id,
|
id = bookEntity.id,
|
||||||
|
|
@ -67,7 +43,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
filePath = bookEntity.filePath,
|
filePath = bookEntity.filePath,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = bookEntity.category,
|
category = bookEntity.category,
|
||||||
coverImage = image
|
coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
interface FileParser {
|
interface FileParser {
|
||||||
|
|
||||||
suspend fun parse(file: File): Book?
|
suspend fun parse(file: File): Pair<Book, CoverImage?>?
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
package ua.acclorite.book_story.data.parser.epub
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.siegmann.epublib.epub.EpubReader
|
import nl.siegmann.epublib.epub.EpubReader
|
||||||
import ua.acclorite.book_story.data.parser.FileParser
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
@ -15,7 +15,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class EpubFileParser @Inject constructor() : FileParser {
|
class EpubFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): Book? {
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
if (!file.name.endsWith(".epub")) {
|
if (!file.name.endsWith(".epub")) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,9 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
try {
|
try {
|
||||||
val epubReader = EpubReader()
|
val epubReader = EpubReader()
|
||||||
val book = withContext(Dispatchers.IO) {
|
val book = withContext(Dispatchers.IO) {
|
||||||
epubReader.readEpub(FileInputStream(file))
|
FileInputStream(file).use {
|
||||||
|
epubReader.readEpub(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadata = book.metadata
|
val metadata = book.metadata
|
||||||
|
|
@ -42,8 +44,6 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Book(
|
return Book(
|
||||||
id = null,
|
id = null,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -55,8 +55,8 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
filePath = file.path,
|
filePath = file.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = coverImage?.asImageBitmap()
|
coverImage = null
|
||||||
)
|
) to coverImage
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ import ua.acclorite.book_story.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PdfFileParser @Inject constructor(private val application: Application) : FileParser {
|
class PdfFileParser @Inject constructor(private val application: Application) : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): Book? {
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
if (!file.name.endsWith(".pdf")) {
|
if (!file.name.endsWith(".pdf")) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +47,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = null
|
coverImage = null
|
||||||
)
|
) to null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ import ua.acclorite.book_story.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TxtFileParser @Inject constructor() : FileParser {
|
class TxtFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): Book? {
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
if (!file.name.endsWith(".txt")) {
|
if (!file.name.endsWith(".txt")) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +34,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = null
|
coverImage = null
|
||||||
)
|
) to null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package ua.acclorite.book_story.data.repository
|
package ua.acclorite.book_story.data.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -10,12 +13,8 @@ import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||||
import ua.acclorite.book_story.data.local.room.BookDao
|
import ua.acclorite.book_story.data.local.room.BookDao
|
||||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
import ua.acclorite.book_story.data.parser.epub.EpubFileParser
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.data.parser.epub.EpubTextParser
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
|
|
||||||
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
|
||||||
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
|
|
||||||
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
|
|
@ -23,29 +22,27 @@ import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import ua.acclorite.book_story.presentation.data.calculateFamiliarity
|
import ua.acclorite.book_story.presentation.data.calculateFamiliarity
|
||||||
import ua.acclorite.book_story.util.Constants
|
import ua.acclorite.book_story.util.Constants
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import ua.acclorite.book_story.util.Resource
|
import ua.acclorite.book_story.util.Resource
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BookRepositoryImpl @Inject constructor(
|
class BookRepositoryImpl @Inject constructor(
|
||||||
|
private val application: Application,
|
||||||
private val database: BookDao,
|
private val database: BookDao,
|
||||||
private val dataStore: DataStore,
|
private val dataStore: DataStore,
|
||||||
|
|
||||||
private val bookMapper: BookMapper,
|
private val bookMapper: BookMapper,
|
||||||
private val historyMapper: HistoryMapper,
|
private val historyMapper: HistoryMapper,
|
||||||
|
|
||||||
private val txtFileParser: TxtFileParser,
|
private val fileParser: FileParser,
|
||||||
private val pdfFileParser: PdfFileParser,
|
private val textParser: TextParser
|
||||||
private val epubFileParser: EpubFileParser,
|
|
||||||
|
|
||||||
private val txtTextParser: TxtTextParser,
|
) : BookRepository {
|
||||||
private val pdfTextParser: PdfTextParser,
|
|
||||||
private val epubTextParser: EpubTextParser,
|
|
||||||
|
|
||||||
) : BookRepository {
|
|
||||||
|
|
||||||
override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> {
|
override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> {
|
||||||
return flow {
|
return flow {
|
||||||
|
|
@ -101,12 +98,46 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
return database.findBookById(id)
|
return database.findBookById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertBooks(books: List<Book>) {
|
override suspend fun insertBooks(
|
||||||
database.insertBooks(books.map { bookMapper.toBookEntity(it) })
|
books: List<Pair<Book, CoverImage?>>
|
||||||
|
) {
|
||||||
|
val booksWithCover = books.map {
|
||||||
|
var uri = ""
|
||||||
|
|
||||||
|
if (it.second != null) {
|
||||||
|
try {
|
||||||
|
uri = "${UUID.randomUUID()}.webp"
|
||||||
|
application.openFileOutput(uri, 0x0000).use { stream ->
|
||||||
|
if (
|
||||||
|
!it.second!!.compress(
|
||||||
|
Bitmap.CompressFormat.WEBP,
|
||||||
|
30,
|
||||||
|
stream
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw Exception("Couldn't save cover image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uri = ""
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val book = it.first.copy(
|
||||||
|
coverImage = if (uri.isNotBlank()) {
|
||||||
|
Uri.fromFile(File("${application.filesDir}/$uri"))
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
|
||||||
|
bookMapper.toBookEntity(book)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.insertBooks(booksWithCover)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateBooks(books: List<Book>) {
|
override suspend fun updateBooks(books: List<Book>) {
|
||||||
// without text
|
// without text and cover image
|
||||||
database.updateBooks(
|
database.updateBooks(
|
||||||
books.map {
|
books.map {
|
||||||
val book = database.findBookById(it.id ?: return)
|
val book = database.findBookById(it.id ?: return)
|
||||||
|
|
@ -114,7 +145,8 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
it.copy(
|
it.copy(
|
||||||
text = book.text
|
text = book.text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map { line -> StringWithId(line.trim()) }
|
.map { line -> StringWithId(line.trim()) },
|
||||||
|
coverImage = if (book.image != null) Uri.parse(book.image) else null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -122,25 +154,99 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateBooksWithText(books: List<Book>) {
|
override suspend fun updateBooksWithText(books: List<Book>) {
|
||||||
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
|
// without cover image
|
||||||
}
|
database.updateBooks(
|
||||||
|
|
||||||
override suspend fun deleteBooks(books: List<Book>) {
|
|
||||||
// without text
|
|
||||||
database.deleteBooks(
|
|
||||||
books.map {
|
books.map {
|
||||||
val book = database.findBookById(it.id ?: return)
|
val book = database.findBookById(it.id ?: return)
|
||||||
bookMapper.toBookEntity(
|
bookMapper.toBookEntity(
|
||||||
it.copy(
|
it.copy(
|
||||||
text = book.text
|
coverImage = if (book.image != null) Uri.parse(book.image) else null
|
||||||
.split("\n")
|
|
||||||
.map { line -> StringWithId(line.trim()) }
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
override suspend fun <T> retrieveDataFromDataStore(
|
||||||
key: Preferences.Key<T>,
|
key: Preferences.Key<T>,
|
||||||
defaultValue: 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> {
|
override suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> {
|
||||||
val books = mutableListOf<NullableBook>()
|
val books = mutableListOf<NullableBook>()
|
||||||
|
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
val parsedBook = if (file.name.endsWith(".txt")) {
|
val parsedBook = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
|
||||||
txtFileParser.parse(file)
|
fileParser.parse(file)
|
||||||
} else if (file.name.endsWith(".pdf")) {
|
|
||||||
pdfFileParser.parse(file)
|
|
||||||
} else if (file.name.endsWith(".epub")) {
|
|
||||||
epubFileParser.parse(file)
|
|
||||||
} else {
|
} else {
|
||||||
books.add(
|
books.add(
|
||||||
NullableBook.Null(
|
NullableBook.Null(
|
||||||
|
|
@ -289,12 +352,8 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val parsedText = if (file.name.endsWith(".txt")) {
|
val parsedText = if (Constants.EXTENSIONS.any { file.name.endsWith(it) }) {
|
||||||
txtTextParser.parse(file)
|
textParser.parse(file)
|
||||||
} else if (file.name.endsWith(".pdf")) {
|
|
||||||
pdfTextParser.parse(file)
|
|
||||||
} else if (file.name.endsWith(".epub")) {
|
|
||||||
epubTextParser.parse(file)
|
|
||||||
} else {
|
} else {
|
||||||
books.add(
|
books.add(
|
||||||
NullableBook.Null(
|
NullableBook.Null(
|
||||||
|
|
@ -338,8 +397,8 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
books.add(
|
books.add(
|
||||||
NullableBook.NotNull(
|
NullableBook.NotNull(
|
||||||
Pair(
|
Pair(
|
||||||
parsedBook.copy(text = parsedText.data),
|
parsedBook.first.copy(text = parsedText.data),
|
||||||
true
|
parsedBook.second
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package ua.acclorite.book_story.domain.model
|
package ua.acclorite.book_story.domain.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
@ -17,5 +17,5 @@ data class Book(
|
||||||
val filePath: String,
|
val filePath: String,
|
||||||
val lastOpened: Long?,
|
val lastOpened: Long?,
|
||||||
val category: Category,
|
val category: Category,
|
||||||
val coverImage: ImageBitmap?
|
val coverImage: Uri?
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
package ua.acclorite.book_story.domain.model
|
package ua.acclorite.book_story.domain.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class NullableBook(
|
sealed class NullableBook(
|
||||||
val book: Pair<Book, Boolean>?,
|
val book: Pair<Book, CoverImage?>?,
|
||||||
val fileName: String?,
|
val fileName: String?,
|
||||||
val message: UIText?
|
val message: UIText?
|
||||||
) {
|
) {
|
||||||
class NotNull(book: Pair<Book, Boolean>) : NullableBook(book, null, null)
|
class NotNull(book: Pair<Book, CoverImage?>) : NullableBook(book, null, null)
|
||||||
class Null(
|
class Null(
|
||||||
fileName: String,
|
fileName: String,
|
||||||
message: UIText?
|
message: UIText?
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import ua.acclorite.book_story.util.Resource
|
import ua.acclorite.book_story.util.Resource
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ interface BookRepository {
|
||||||
): BookEntity
|
): BookEntity
|
||||||
|
|
||||||
suspend fun insertBooks(
|
suspend fun insertBooks(
|
||||||
books: List<Book>
|
books: List<Pair<Book, CoverImage?>>
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun updateBooks(
|
suspend fun updateBooks(
|
||||||
|
|
@ -44,6 +44,11 @@ interface BookRepository {
|
||||||
books: List<Book>
|
books: List<Book>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
suspend fun updateCoverImageOfBook(
|
||||||
|
bookWithOldCover: Book,
|
||||||
|
newCoverImage: CoverImage?
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun deleteBooks(
|
suspend fun deleteBooks(
|
||||||
books: List<Book>
|
books: List<Book>
|
||||||
)
|
)
|
||||||
|
|
@ -60,11 +65,8 @@ interface BookRepository {
|
||||||
|
|
||||||
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
||||||
|
|
||||||
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
|
|
||||||
|
|
||||||
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
||||||
|
|
||||||
|
|
||||||
suspend fun insertHistory(history: List<History>)
|
suspend fun insertHistory(history: List<History>)
|
||||||
|
|
||||||
suspend fun getHistory(): Flow<Resource<List<History>>>
|
suspend fun getHistory(): Flow<Resource<List<History>>>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
|
import ua.acclorite.book_story.util.CoverImage
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class InsertBooks @Inject constructor(private val repository: BookRepository) {
|
class InsertBooks @Inject constructor(private val repository: BookRepository) {
|
||||||
suspend fun execute(books: List<Book>) {
|
suspend fun execute(books: List<Pair<Book, CoverImage?>>) {
|
||||||
repository.insertBooks(books)
|
repository.insertBooks(books)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
|
|
@ -151,6 +152,8 @@ private class CustomSelectionToolbar(
|
||||||
clipboardManager.setPrimaryClip(
|
clipboardManager.setPrimaryClip(
|
||||||
previousClipboard
|
previousClipboard
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback.onDictionaryRequested = {
|
callback.onDictionaryRequested = {
|
||||||
|
|
@ -164,6 +167,8 @@ private class CustomSelectionToolbar(
|
||||||
clipboardManager.setPrimaryClip(
|
clipboardManager.setPrimaryClip(
|
||||||
previousClipboard
|
previousClipboard
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.book_info
|
package ua.acclorite.book_story.presentation.screens.book_info
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
|
@ -52,9 +50,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
|
||||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||||
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
||||||
|
|
@ -81,11 +77,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
|
||||||
import ua.acclorite.book_story.ui.DefaultTransition
|
import ua.acclorite.book_story.ui.DefaultTransition
|
||||||
import ua.acclorite.book_story.ui.Transitions
|
import ua.acclorite.book_story.ui.Transitions
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
import ua.acclorite.book_story.util.Constants
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoScreen(
|
fun BookInfoScreen(
|
||||||
|
viewModel: BookInfoViewModel,
|
||||||
libraryViewModel: LibraryViewModel,
|
libraryViewModel: LibraryViewModel,
|
||||||
browseViewModel: BrowseViewModel,
|
browseViewModel: BrowseViewModel,
|
||||||
historyViewModel: HistoryViewModel,
|
historyViewModel: HistoryViewModel,
|
||||||
|
|
@ -93,20 +89,6 @@ fun BookInfoScreen(
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val viewModel by (context as ComponentActivity).viewModels<BookInfoViewModel>(
|
|
||||||
extrasProducer = {
|
|
||||||
val book = navigator.retrieveArgument("book") as? Book
|
|
||||||
if (book == null) {
|
|
||||||
navigator.navigateBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
context.defaultViewModelCreationExtras
|
|
||||||
.withCreationCallback<BookInfoViewModel.Factory> { factory ->
|
|
||||||
factory.create(book ?: Constants.EMPTY_BOOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
@ -302,7 +284,7 @@ fun BookInfoScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.READER, false, Argument(
|
Screen.READER, false, Argument(
|
||||||
"book", state.book
|
"book", state.book.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.book_info.components
|
package ua.acclorite.book_story.presentation.screens.book_info.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -10,20 +10,20 @@ import androidx.compose.ui.draw.blur
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Background of the BookInfoScreen.
|
* Background of the BookInfoScreen.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoBackground(height: Dp, image: ImageBitmap) {
|
fun BookInfoBackground(height: Dp, image: Uri) {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
|
|
||||||
Image(
|
AsyncImage(
|
||||||
bitmap = image,
|
model = image,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -38,7 +38,7 @@ fun BookInfoBackground(height: Dp, image: ImageBitmap) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.blur(3.dp),
|
.blur(3.dp),
|
||||||
alpha = 0.3f,
|
alpha = 0.4f,
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.book_info.components
|
package ua.acclorite.book_story.presentation.screens.book_info.components
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
|
@ -41,6 +40,7 @@ import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
||||||
|
|
@ -76,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) {
|
if (book.coverImage != null) {
|
||||||
Image(
|
AsyncImage(
|
||||||
bitmap = book.coverImage,
|
model = book.coverImage,
|
||||||
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.large),
|
.clip(MaterialTheme.shapes.large),
|
||||||
contentScale = ContentScale.Crop
|
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 androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.util.Constants
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class BookInfoState(
|
data class BookInfoState(
|
||||||
val book: Book,
|
val book: Book = Constants.EMPTY_BOOK,
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
|
|
||||||
val editTitle: Boolean = false,
|
val editTitle: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,8 @@ import android.content.Context.CLIPBOARD_SERVICE
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -21,27 +17,30 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.use_case.DeleteBooks
|
import ua.acclorite.book_story.domain.use_case.DeleteBooks
|
||||||
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
|
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
|
||||||
|
import ua.acclorite.book_story.domain.use_case.GetBooksById
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
|
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
|
||||||
|
import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class)
|
@HiltViewModel
|
||||||
class BookInfoViewModel @AssistedInject constructor(
|
class BookInfoViewModel @Inject constructor(
|
||||||
@Assisted book: Book,
|
|
||||||
private val updateBooks: UpdateBooks,
|
private val updateBooks: UpdateBooks,
|
||||||
private val updateBooksWithText: UpdateBooksWithText,
|
private val updateBooksWithText: UpdateBooksWithText,
|
||||||
|
private val updateCoverImageOfBook: UpdateCoverImageOfBook,
|
||||||
private val deleteBooks: DeleteBooks,
|
private val deleteBooks: DeleteBooks,
|
||||||
private val getBookFromFile: GetBookFromFile
|
private val getBookFromFile: GetBookFromFile,
|
||||||
|
private val getBookById: GetBooksById
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(BookInfoState(book))
|
private val _state = MutableStateFlow(BookInfoState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
@ -59,22 +58,24 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
is BookInfoEvent.OnChangeCover -> {
|
is BookInfoEvent.OnChangeCover -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
|
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
|
||||||
BitmapFactory.decodeStream(it).asImageBitmap()
|
BitmapFactory.decodeStream(it)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|
||||||
image.prepareToDraw()
|
updateCoverImageOfBook.execute(
|
||||||
|
_state.value.book,
|
||||||
updateBooks.execute(
|
image
|
||||||
listOf(
|
|
||||||
_state.value.book.copy(coverImage = image)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
event.refreshList()
|
event.refreshList()
|
||||||
|
val newCoverImage = getBookById.execute(
|
||||||
|
listOf(
|
||||||
|
_state.value.book.id ?: return@launch
|
||||||
|
)
|
||||||
|
).first().coverImage
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
book = it.book.copy(
|
book = it.book.copy(
|
||||||
coverImage = image
|
coverImage = newCoverImage
|
||||||
),
|
),
|
||||||
showChangeCoverBottomSheet = false
|
showChangeCoverBottomSheet = false
|
||||||
)
|
)
|
||||||
|
|
@ -88,10 +89,9 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBooks.execute(
|
updateCoverImageOfBook.execute(
|
||||||
listOf(
|
bookWithOldCover = _state.value.book,
|
||||||
_state.value.book.copy(coverImage = null)
|
newCoverImage = null
|
||||||
)
|
|
||||||
)
|
)
|
||||||
event.refreshList()
|
event.refreshList()
|
||||||
|
|
||||||
|
|
@ -251,7 +251,7 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
event.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
event.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("", event.text))
|
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, event.text))
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
event.success()
|
event.success()
|
||||||
|
|
@ -399,21 +399,18 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
fun init(navigator: Navigator) {
|
fun init(navigator: Navigator) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val book = navigator.retrieveArgument("book") as? Book
|
val bookIndex = navigator.retrieveArgument("book") as? Int
|
||||||
|
|
||||||
if (book == null) {
|
if (bookIndex == null || bookIndex < 0) {
|
||||||
navigator.navigateBack()
|
navigator.navigateBack()
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val book = getBookById.execute(listOf(bookIndex)).first()
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
BookInfoState(book = book)
|
BookInfoState(book = book)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
fun create(book: Book): BookInfoViewModel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||||
import ua.acclorite.book_story.ui.DefaultTransition
|
import ua.acclorite.book_story.ui.DefaultTransition
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
@ -45,7 +46,7 @@ import java.util.Locale
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseFileItem(
|
fun BrowseFileItem(
|
||||||
file: Pair<File, Boolean>,
|
file: Pair<File, Selected>,
|
||||||
hasSelectedFiles: Boolean,
|
hasSelectedFiles: Boolean,
|
||||||
modifier: Modifier, onClick: () -> Unit
|
modifier: Modifier, onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ fun BrowseAddingDialog(
|
||||||
description = stringResource(id = R.string.add_books_description),
|
description = stringResource(id = R.string.add_books_description),
|
||||||
actionText = stringResource(id = R.string.add),
|
actionText = stringResource(id = R.string.add),
|
||||||
isActionEnabled = !state.isBooksLoading &&
|
isActionEnabled = !state.isBooksLoading &&
|
||||||
state.selectedBooks
|
state.selectedBooks.any { it.first is NullableBook.NotNull },
|
||||||
.filterIsInstance<NullableBook.NotNull>()
|
|
||||||
.isNotEmpty(),
|
|
||||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
||||||
onAction = {
|
onAction = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
|
|
@ -91,7 +89,7 @@ fun BrowseAddingDialog(
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
book.message?.asString(context),
|
book.first.message?.asString(context),
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,14 @@ import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adding dialog item.
|
* Adding dialog item.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boolean) -> Unit) {
|
||||||
if (result is NullableBook.NotNull) {
|
if (result.first is NullableBook.NotNull) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -44,14 +45,14 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
||||||
Modifier.weight(0.85f)
|
Modifier.weight(0.85f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = result.book!!.first.title,
|
text = result.first.book!!.first.title,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = result.book.first.author.asString(),
|
text = result.first.book!!.first.author.asString(),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
@ -59,12 +60,12 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
|
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
|
||||||
CustomCheckbox(selected = result.book!!.second)
|
CustomCheckbox(selected = result.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val icon =
|
val icon =
|
||||||
if (result.message?.asString() == stringResource(R.string.error_file_encrypted)) {
|
if (result.first.message?.asString() == stringResource(R.string.error_file_encrypted)) {
|
||||||
Icons.Default.Lock
|
Icons.Default.Lock
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.Error
|
Icons.Default.Error
|
||||||
|
|
@ -87,7 +88,7 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = result.fileName!!,
|
text = result.first.fileName!!,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionState
|
import com.google.accompanist.permissions.PermissionState
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
sealed class BrowseEvent {
|
sealed class BrowseEvent {
|
||||||
|
|
@ -29,8 +30,8 @@ sealed class BrowseEvent {
|
||||||
val hideErrorMessage: () -> Unit
|
val hideErrorMessage: () -> Unit
|
||||||
) : BrowseEvent()
|
) : BrowseEvent()
|
||||||
|
|
||||||
data class OnSelectFile(val file: Pair<File, Boolean>) : BrowseEvent()
|
data class OnSelectFile(val file: Pair<File, Selected>) : BrowseEvent()
|
||||||
data class OnSelectBook(val book: NullableBook) : BrowseEvent()
|
data class OnSelectBook(val book: Pair<NullableBook, Selected>) : BrowseEvent()
|
||||||
data object OnSearchShowHide : BrowseEvent()
|
data object OnSearchShowHide : BrowseEvent()
|
||||||
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
|
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
|
||||||
data object OnClearSelectedFiles : BrowseEvent()
|
data object OnClearSelectedFiles : BrowseEvent()
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ package ua.acclorite.book_story.presentation.screens.browse.data
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class BrowseState(
|
data class BrowseState(
|
||||||
val selectableFiles: List<Pair<File, Boolean>> = emptyList(),
|
val selectableFiles: List<Pair<File, Selected>> = emptyList(),
|
||||||
val listState: LazyListState = LazyListState(0, 0),
|
val listState: LazyListState = LazyListState(0, 0),
|
||||||
|
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
|
|
@ -25,6 +26,6 @@ data class BrowseState(
|
||||||
val hasFocused: Boolean = false,
|
val hasFocused: Boolean = false,
|
||||||
|
|
||||||
val showAddingDialog: Boolean = false,
|
val showAddingDialog: Boolean = false,
|
||||||
val selectedBooks: List<NullableBook> = emptyList(),
|
val selectedBooks: List<Pair<NullableBook, Selected>> = emptyList(),
|
||||||
val isBooksLoading: Boolean = false
|
val isBooksLoading: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
@ -199,13 +199,12 @@ class BrowseViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val editedList = _state.value.selectedBooks.toMutableList()
|
val editedList = _state.value.selectedBooks.toMutableList()
|
||||||
editedList[indexOfFile] = NullableBook.NotNull(
|
editedList[indexOfFile] = editedList[indexOfFile].first to
|
||||||
editedList[indexOfFile].book!!.copy(
|
!editedList[indexOfFile].second
|
||||||
second = !editedList[indexOfFile].book!!.second
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!editedList.any { it.book?.second == true }) {
|
if (!editedList.filter { it.first is NullableBook.NotNull }
|
||||||
|
.any { it.second }
|
||||||
|
) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +307,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedBooks = books,
|
selectedBooks = books.map { book -> book to true },
|
||||||
isBooksLoading = false
|
isBooksLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -318,9 +317,9 @@ class BrowseViewModel @Inject constructor(
|
||||||
is BrowseEvent.OnAddBooks -> {
|
is BrowseEvent.OnAddBooks -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val booksToInsert = _state.value.selectedBooks
|
val booksToInsert = _state.value.selectedBooks
|
||||||
.filterIsInstance<NullableBook.NotNull>()
|
.filter { it.first is NullableBook.NotNull }
|
||||||
.filter { it.book!!.second }
|
.filter { it.second }
|
||||||
.map { it.book!!.first }
|
.map { it.first.book!! }
|
||||||
|
|
||||||
if (booksToInsert.isEmpty()) {
|
if (booksToInsert.isEmpty()) {
|
||||||
return@launch
|
return@launch
|
||||||
|
|
@ -330,19 +329,21 @@ class BrowseViewModel @Inject constructor(
|
||||||
val books = fastGetBooks.execute("")
|
val books = fastGetBooks.execute("")
|
||||||
|
|
||||||
event.resetScroll()
|
event.resetScroll()
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
showAddingDialog = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
|
||||||
onEvent(BrowseEvent.OnLoadList)
|
|
||||||
|
|
||||||
event.navigator.navigate(
|
event.navigator.navigate(
|
||||||
Screen.LIBRARY,
|
Screen.LIBRARY,
|
||||||
false,
|
false,
|
||||||
Argument("added_books", books)
|
Argument("added_books", books)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
showAddingDialog = false,
|
||||||
|
selectableFiles = emptyList(),
|
||||||
|
listState = LazyListState(0, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onEvent(BrowseEvent.OnLoadList)
|
||||||
|
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,14 +248,14 @@ fun HistoryScreen(
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.BOOK_INFO,
|
Screen.BOOK_INFO,
|
||||||
false,
|
false,
|
||||||
Argument("book", book)
|
Argument("book", book.id)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTitleClick = {
|
onTitleClick = {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.READER,
|
Screen.READER,
|
||||||
false,
|
false,
|
||||||
Argument("book", book)
|
Argument("book", book.id)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isDeleteEnabled = !state.isRefreshing,
|
isDeleteEnabled = !state.isRefreshing,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.history.components
|
package ua.acclorite.book_story.presentation.screens.history.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -31,6 +30,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
|
|
@ -77,27 +77,27 @@ fun HistoryItem(
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.elevation())
|
.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) {
|
if (book.coverImage != null) {
|
||||||
Image(
|
AsyncImage(
|
||||||
bitmap = book.coverImage,
|
model = book.coverImage,
|
||||||
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.medium),
|
.clip(MaterialTheme.shapes.medium),
|
||||||
contentScale = ContentScale.Crop
|
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.DefaultTransition
|
||||||
import ua.acclorite.book_story.ui.Transitions
|
import ua.acclorite.book_story.ui.Transitions
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
|
|
@ -304,14 +305,14 @@ fun LibraryScreen(
|
||||||
) {
|
) {
|
||||||
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
|
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
|
||||||
var categoryIsLoading by remember { mutableStateOf(true) }
|
var categoryIsLoading by remember { mutableStateOf(true) }
|
||||||
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Boolean>>() }
|
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Selected>>() }
|
||||||
val category = remember { Category.entries[index] }
|
val category = remember { Category.entries[index] }
|
||||||
|
|
||||||
LaunchedEffect(state.books) {
|
LaunchedEffect(state.books) {
|
||||||
categorizedBooks.clear()
|
categorizedBooks.clear()
|
||||||
categorizedBooks.addAll(state.books.filter { it.first.category == category }
|
categorizedBooks.addAll(state.books.filter { it.first.category == category }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareByDescending<Pair<Book, Boolean>> { it.first.lastOpened }
|
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
|
||||||
.thenBy { it.first.title }
|
.thenBy { it.first.title }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -340,7 +341,7 @@ fun LibraryScreen(
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.BOOK_INFO,
|
Screen.BOOK_INFO,
|
||||||
false,
|
false,
|
||||||
Argument("book", it.first)
|
Argument("book", it.first.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -353,7 +354,7 @@ fun LibraryScreen(
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.READER,
|
Screen.READER,
|
||||||
false,
|
false,
|
||||||
Argument("book", it.first)
|
Argument("book", it.first.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.screens.library.components
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -35,11 +34,13 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.data.removeDigits
|
import ua.acclorite.book_story.presentation.data.removeDigits
|
||||||
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library list element item.
|
* Library list element item.
|
||||||
|
|
@ -47,7 +48,7 @@ import ua.acclorite.book_story.ui.elevation
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryBookItem(
|
fun LibraryBookItem(
|
||||||
book: Pair<Book, Boolean>,
|
book: Pair<Book, Selected>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onCoverImageClick: () -> Unit,
|
onCoverImageClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
|
@ -103,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) {
|
if (book.first.coverImage != null) {
|
||||||
Image(
|
AsyncImage(
|
||||||
bitmap = book.first.coverImage!!,
|
model = book.first.coverImage,
|
||||||
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.large),
|
.clip(MaterialTheme.shapes.large),
|
||||||
contentScale = ContentScale.Crop
|
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(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
|
|
||||||
sealed class LibraryEvent {
|
sealed class LibraryEvent {
|
||||||
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
|
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
|
||||||
|
|
@ -17,7 +18,7 @@ sealed class LibraryEvent {
|
||||||
data object OnSearchShowHide : LibraryEvent()
|
data object OnSearchShowHide : LibraryEvent()
|
||||||
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
|
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
|
||||||
data class OnSearchQueryChange(val query: String) : LibraryEvent()
|
data class OnSearchQueryChange(val query: String) : LibraryEvent()
|
||||||
data class OnSelectBook(val book: Pair<Book, Boolean>, val select: Boolean? = null) :
|
data class OnSelectBook(val book: Pair<Book, Selected>, val select: Boolean? = null) :
|
||||||
LibraryEvent()
|
LibraryEvent()
|
||||||
|
|
||||||
data object OnClearSelectedBooks : LibraryEvent()
|
data object OnClearSelectedBooks : LibraryEvent()
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ package ua.acclorite.book_story.presentation.screens.library.data
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.util.Selected
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class LibraryState(
|
data class LibraryState(
|
||||||
val books: List<Pair<Book, Boolean>> = emptyList(),
|
val books: List<Pair<Book, Selected>> = emptyList(),
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
|
@ -51,12 +50,10 @@ import androidx.compose.ui.text.style.LineBreak
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
|
||||||
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
|
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
|
||||||
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
||||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||||
|
|
@ -79,6 +76,7 @@ import ua.acclorite.book_story.util.Constants
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderScreen(
|
fun ReaderScreen(
|
||||||
|
viewModel: ReaderViewModel,
|
||||||
mainViewModel: MainViewModel,
|
mainViewModel: MainViewModel,
|
||||||
libraryViewModel: LibraryViewModel,
|
libraryViewModel: LibraryViewModel,
|
||||||
historyViewModel: HistoryViewModel,
|
historyViewModel: HistoryViewModel,
|
||||||
|
|
@ -87,20 +85,6 @@ fun ReaderScreen(
|
||||||
val context = LocalContext.current as ComponentActivity
|
val context = LocalContext.current as ComponentActivity
|
||||||
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
|
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
|
||||||
|
|
||||||
val viewModel by context.viewModels<ReaderViewModel>(
|
|
||||||
extrasProducer = {
|
|
||||||
val book = navigator.retrieveArgument("book") as? Book
|
|
||||||
if (book == null) {
|
|
||||||
navigator.navigateBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
context.defaultViewModelCreationExtras
|
|
||||||
.withCreationCallback<ReaderViewModel.Factory> { factory ->
|
|
||||||
factory.create(book ?: Constants.EMPTY_BOOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
@ -296,13 +280,16 @@ fun ReaderScreen(
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
state.book.text, key = { _, key -> key.id }
|
state.book.text, key = { _, key -> key.id }
|
||||||
) { index, line ->
|
) { index, line ->
|
||||||
val text = remember { "${if (paragraphIndentation) " " else ""}${line.line}" }
|
val text =
|
||||||
val color = remember { Color(fontColor.toULong()) }
|
remember(paragraphIndentation) { "${if (paragraphIndentation) " " else ""}${line.line}" }
|
||||||
val lineHeightSp = remember { (fontSize + lineHeight).sp }
|
val fontCol = remember(fontColor) { Color(fontColor.toULong()) }
|
||||||
|
val backgroundCol =
|
||||||
|
remember(backgroundColor) { Color(backgroundColor.toULong()) }
|
||||||
|
val lineHeightSp = remember(fontSize, lineHeight) { (fontSize + lineHeight).sp }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.background(Color(backgroundColor.toULong()))
|
.background(backgroundCol)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
top = if (index == 0) 36.dp else 0.dp,
|
top = if (index == 0) 36.dp else 0.dp,
|
||||||
|
|
@ -314,7 +301,7 @@ fun ReaderScreen(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
color = color,
|
color = fontCol,
|
||||||
|
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
lineBreak = LineBreak.Paragraph
|
lineBreak = LineBreak.Paragraph
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.reader.components
|
package ua.acclorite.book_story.presentation.screens.reader.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -32,6 +31,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
@ -62,25 +62,25 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.elevation())
|
.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) {
|
if (state.book.coverImage != null) {
|
||||||
Image(
|
AsyncImage(
|
||||||
bitmap = state.book.coverImage!!,
|
model = state.book.coverImage!!,
|
||||||
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.medium),
|
.clip(MaterialTheme.shapes.medium),
|
||||||
contentScale = ContentScale.Crop
|
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,
|
true,
|
||||||
Argument(
|
Argument(
|
||||||
"book",
|
"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 androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
import ua.acclorite.book_story.util.Constants
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ReaderState(
|
data class ReaderState(
|
||||||
val book: Book,
|
val book: Book = Constants.EMPTY_BOOK,
|
||||||
val letters: Int = 0,
|
val letters: Int = 0,
|
||||||
val words: Int = 0,
|
val words: Int = 0,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@ import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -27,6 +24,7 @@ import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.domain.use_case.GetBooksById
|
||||||
import ua.acclorite.book_story.domain.use_case.GetText
|
import ua.acclorite.book_story.domain.use_case.GetText
|
||||||
import ua.acclorite.book_story.domain.use_case.InsertHistory
|
import ua.acclorite.book_story.domain.use_case.InsertHistory
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||||
|
|
@ -35,18 +33,19 @@ import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class)
|
@HiltViewModel
|
||||||
class ReaderViewModel @AssistedInject constructor(
|
class ReaderViewModel @Inject constructor(
|
||||||
@Assisted book: Book,
|
|
||||||
private val updateBooks: UpdateBooks,
|
private val updateBooks: UpdateBooks,
|
||||||
private val insertHistory: InsertHistory,
|
private val insertHistory: InsertHistory,
|
||||||
private val getText: GetText
|
private val getText: GetText,
|
||||||
|
private val getBooksById: GetBooksById
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ReaderState(book))
|
private val _state = MutableStateFlow(ReaderState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
fun onEvent(event: ReaderEvent) {
|
fun onEvent(event: ReaderEvent) {
|
||||||
|
|
@ -104,14 +103,17 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
event.refreshList(_state.value.book)
|
event.refreshList(_state.value.book)
|
||||||
event.navigator.putArgument(
|
event.navigator.putArgument(
|
||||||
Argument("book", _state.value.book)
|
Argument("book", _state.value.book.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
event.scrollState.layoutInfo.totalItemsCount
|
event.scrollState.layoutInfo.totalItemsCount
|
||||||
}.collectLatest { itemsCount ->
|
}.collectLatest { itemsCount ->
|
||||||
val scrollTo = (itemsCount * _state.value.book.progress).roundToInt()
|
val scrollTo =
|
||||||
|
(_state.value.book.text.size * _state.value.book.progress)
|
||||||
|
.toInt()
|
||||||
|
|
||||||
if (itemsCount >= _state.value.book.text.size) {
|
if (itemsCount >= _state.value.book.text.size) {
|
||||||
if (scrollTo > 0) {
|
if (scrollTo > 0) {
|
||||||
var loaded = false
|
var loaded = false
|
||||||
|
|
@ -194,12 +196,12 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBooks.execute(
|
updateBooks.execute(
|
||||||
listOf(_state.value.book.copy(progress = event.progress))
|
listOf(_state.value.book)
|
||||||
)
|
)
|
||||||
event.navigator.putArgument(
|
event.navigator.putArgument(
|
||||||
Argument("book", _state.value.book.copy(progress = event.progress))
|
Argument("book", _state.value.book.id)
|
||||||
)
|
)
|
||||||
event.refreshList(_state.value.book.copy(progress = event.progress))
|
event.refreshList(_state.value.book)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,13 +303,15 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
onLoaded: () -> Unit
|
onLoaded: () -> Unit
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val book = navigator.retrieveArgument("book") as? Book
|
val bookIndex = navigator.retrieveArgument("book") as? Int
|
||||||
|
|
||||||
if (book == null) {
|
if (bookIndex == null || bookIndex < 0) {
|
||||||
navigator.navigateBack()
|
navigator.navigateBack()
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val book = getBooksById.execute(listOf(bookIndex)).first()
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
ReaderState(book = book)
|
ReaderState(book = book)
|
||||||
}
|
}
|
||||||
|
|
@ -328,11 +332,6 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
fun create(book: Book): ReaderViewModel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ object Constants {
|
||||||
)
|
)
|
||||||
|
|
||||||
val EMPTY_BOOK = Book(
|
val EMPTY_BOOK = Book(
|
||||||
0,
|
-1,
|
||||||
"",
|
"",
|
||||||
UIText.StringValue(""),
|
UIText.StringValue(""),
|
||||||
null,
|
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 name="add_books_description">
|
||||||
Ви можете вибрати книги, які хочете додати.
|
Ви можете вибрати книги, які хочете додати.
|
||||||
Ви зможете відредагувати Назву та Обкладинку книги пізніше.
|
Ви зможете відредагувати Назву та Обкладинку книги пізніше.
|
||||||
|
Процес завантаження може зайняти декілька хвилин.
|
||||||
</string>
|
</string>
|
||||||
<string name="move_books_description">
|
<string name="move_books_description">
|
||||||
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
|
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Basic Strings -->
|
<!-- Basic Strings -->
|
||||||
<string name="app_version" translatable="false">0.9.2</string>
|
<string name="app_version" translatable="false">0.9.3</string>
|
||||||
<string name="app_name">Book\'s Story</string>
|
<string name="app_name">Book\'s Story</string>
|
||||||
|
|
||||||
<!-- Screens -->
|
<!-- Screens -->
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
<string name="add_books_description">
|
<string name="add_books_description">
|
||||||
You can choose which books you want to add.
|
You can choose which books you want to add.
|
||||||
You can edit Title and Cover image of the book later.
|
You can edit Title and Cover image of the book later.
|
||||||
|
The loading process can take up to several minutes.
|
||||||
</string>
|
</string>
|
||||||
<string name="move_books_description">
|
<string name="move_books_description">
|
||||||
Choose the category to which you want to move all the selected books(%1$s).
|
Choose the category to which you want to move all the selected books(%1$s).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue