feat: add Kavita document fallback and live text settings
Build & Test / Build & Test App (push) Has been cancelled
Build & Test / Build & Test App (push) Has been cancelled
This commit is contained in:
@@ -9,7 +9,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
@@ -33,15 +32,11 @@ fun ReaderSettingsDialog(
|
|||||||
stringResource(MR.strings.pref_category_text_reader),
|
stringResource(MR.strings.pref_category_text_reader),
|
||||||
)
|
)
|
||||||
val pagerState = rememberPagerState { tabTitles.size }
|
val pagerState = rememberPagerState { tabTitles.size }
|
||||||
val initialTextStyle = remember { viewModel.preferences.textReaderStyle() }
|
|
||||||
|
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
TabbedDialog(
|
TabbedDialog(
|
||||||
modifier = Modifier.heightIn(max = maxHeight * 0.75f),
|
modifier = Modifier.heightIn(max = maxHeight * 0.75f),
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
if (viewModel.preferences.textReaderStyle() != initialTextStyle) {
|
viewModel.flushTextSettingsReload()
|
||||||
viewModel.onTextSettingsChanged()
|
|
||||||
}
|
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
onShowMenus()
|
onShowMenus()
|
||||||
},
|
},
|
||||||
@@ -51,7 +46,7 @@ fun ReaderSettingsDialog(
|
|||||||
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
|
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
|
||||||
|
|
||||||
LaunchedEffect(pagerState.currentPage) {
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
if (pagerState.currentPage == 2) {
|
if (pagerState.currentPage == 2 || pagerState.currentPage == 3) {
|
||||||
window?.setDimAmount(0f)
|
window?.setDimAmount(0f)
|
||||||
onHideMenus()
|
onHideMenus()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ internal fun ColumnScope.TextSettingsPage(viewModel: ReaderSettingsViewModel) {
|
|||||||
valueRange = 32..72,
|
valueRange = 32..72,
|
||||||
steps = 19,
|
steps = 19,
|
||||||
valueString = fontSize.toString(),
|
valueString = fontSize.toString(),
|
||||||
onChange = fontSizePref::set,
|
onChange = { value ->
|
||||||
|
fontSizePref.set(value)
|
||||||
|
viewModel.requestTextSettingsReload()
|
||||||
|
},
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +49,10 @@ internal fun ColumnScope.TextSettingsPage(viewModel: ReaderSettingsViewModel) {
|
|||||||
valueRange = 90..180,
|
valueRange = 90..180,
|
||||||
steps = 17,
|
steps = 17,
|
||||||
valueString = "$lineSpacing%",
|
valueString = "$lineSpacing%",
|
||||||
onChange = lineSpacingPref::set,
|
onChange = { value ->
|
||||||
|
lineSpacingPref.set(value)
|
||||||
|
viewModel.requestTextSettingsReload()
|
||||||
|
},
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,7 +64,10 @@ internal fun ColumnScope.TextSettingsPage(viewModel: ReaderSettingsViewModel) {
|
|||||||
valueRange = 0..48,
|
valueRange = 0..48,
|
||||||
steps = 11,
|
steps = 11,
|
||||||
valueString = paragraphSpacing.toString(),
|
valueString = paragraphSpacing.toString(),
|
||||||
onChange = paragraphSpacingPref::set,
|
onChange = { value ->
|
||||||
|
paragraphSpacingPref.set(value)
|
||||||
|
viewModel.requestTextSettingsReload()
|
||||||
|
},
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,7 +77,10 @@ internal fun ColumnScope.TextSettingsPage(viewModel: ReaderSettingsViewModel) {
|
|||||||
fontFamilies.forEach { (label, value) ->
|
fontFamilies.forEach { (label, value) ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = fontFamily == value,
|
selected = fontFamily == value,
|
||||||
onClick = { fontFamilyPref.set(value) },
|
onClick = {
|
||||||
|
fontFamilyPref.set(value)
|
||||||
|
viewModel.requestTextSettingsReload()
|
||||||
|
},
|
||||||
label = { Text(stringResource(label)) },
|
label = { Text(stringResource(label)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.kavita
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
|
private const val KAVITA_PACKAGE_PREFIX = "eu.kanade.tachiyomi.extension.all.kavita."
|
||||||
|
|
||||||
|
internal fun Source.isKavitaSource(): Boolean =
|
||||||
|
this::class.java.name.startsWith(KAVITA_PACKAGE_PREFIX)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kavita extension chapter URLs are normally /Chapter/{id}. Keep compatibility
|
||||||
|
* with older/alternate forms used by reading lists and previous releases.
|
||||||
|
*/
|
||||||
|
internal fun String.toKavitaChapterId(): Int? {
|
||||||
|
Regex("""(?:^|/)Chapter/(\d+)""", RegexOption.IGNORE_CASE)
|
||||||
|
.find(this)
|
||||||
|
?.groupValues
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.let { return it }
|
||||||
|
|
||||||
|
Regex("""(?:^|/)chapter_(\d+)""", RegexOption.IGNORE_CASE)
|
||||||
|
.find(this)
|
||||||
|
?.groupValues
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.let { return it }
|
||||||
|
|
||||||
|
Regex("""[?&]chapterId=(\d+)""", RegexOption.IGNORE_CASE)
|
||||||
|
.find(this)
|
||||||
|
?.groupValues
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.let { return it }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -90,6 +90,7 @@ import tachiyomi.domain.source.service.SourceManager
|
|||||||
import tachiyomi.source.local.image.LocalCoverManager
|
import tachiyomi.source.local.image.LocalCoverManager
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
@@ -823,10 +824,19 @@ class ReaderViewModel(
|
|||||||
mutableState.update { it.copy(dialog = null) }
|
mutableState.update { it.copy(dialog = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val textReaderStyleReloading = AtomicBoolean(false)
|
||||||
|
private val textReaderStyleReloadPending = AtomicBoolean(false)
|
||||||
|
|
||||||
fun reloadTextReaderStyle() {
|
fun reloadTextReaderStyle() {
|
||||||
val chapter = state.value.currentChapter ?: return
|
val chapter = state.value.currentChapter ?: return
|
||||||
if (chapter.pageLoader?.usesTextReaderStyle != true) return
|
if (chapter.pageLoader?.usesTextReaderStyle != true) return
|
||||||
val chapterLoader = loader ?: return
|
val chapterLoader = loader ?: return
|
||||||
|
|
||||||
|
if (!textReaderStyleReloading.compareAndSet(false, true)) {
|
||||||
|
textReaderStyleReloadPending.set(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0)
|
val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0)
|
||||||
val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
|
val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
|
||||||
val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex
|
val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex
|
||||||
@@ -844,6 +854,11 @@ class ReaderViewModel(
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to reload text reader style" }
|
logcat(LogPriority.ERROR, e) { "Failed to reload text reader style" }
|
||||||
|
} finally {
|
||||||
|
textReaderStyleReloading.set(false)
|
||||||
|
if (textReaderStyleReloadPending.getAndSet(false)) {
|
||||||
|
reloadTextReaderStyle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.bookoasis.isBookOasisSource
|
import eu.kanade.tachiyomi.source.bookoasis.isBookOasisSource
|
||||||
import eu.kanade.tachiyomi.source.bookoasis.toBookOasisChapterRef
|
import eu.kanade.tachiyomi.source.bookoasis.toBookOasisChapterRef
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.isKavitaSource
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.toKavitaChapterId
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
@@ -91,6 +93,7 @@ class ChapterLoader(
|
|||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
val bookOasisRef = dbChapter.url.toBookOasisChapterRef()
|
val bookOasisRef = dbChapter.url.toBookOasisChapterRef()
|
||||||
|
val kavitaChapterId = dbChapter.url.toKavitaChapterId()
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
isDownloaded -> DownloadPageLoader(
|
isDownloaded -> DownloadPageLoader(
|
||||||
@@ -112,6 +115,16 @@ class ChapterLoader(
|
|||||||
source is HttpSource && source.isBookOasisSource() && bookOasisRef != null -> {
|
source is HttpSource && source.isBookOasisSource() && bookOasisRef != null -> {
|
||||||
BookOasisFilePageLoader(context, source, bookOasisRef, readerPreferences.textReaderStyle())
|
BookOasisFilePageLoader(context, source, bookOasisRef, readerPreferences.textReaderStyle())
|
||||||
}
|
}
|
||||||
|
source is HttpSource && source.isKavitaSource() && kavitaChapterId != null -> {
|
||||||
|
KavitaCompatPageLoader(
|
||||||
|
context = context,
|
||||||
|
chapter = chapter,
|
||||||
|
source = source,
|
||||||
|
chapterCache = chapterCache,
|
||||||
|
chapterId = kavitaChapterId,
|
||||||
|
textStyle = readerPreferences.textReaderStyle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
source is HttpSource -> HttpPageLoader(chapter, source, chapterCache)
|
source is HttpSource -> HttpPageLoader(chapter, source, chapterCache)
|
||||||
source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString()))
|
source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString()))
|
||||||
else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
|
else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
||||||
|
import mihon.core.archive.epubReader
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility loader for Kavita document chapters.
|
||||||
|
*
|
||||||
|
* The Kavita extension renders every chapter through /Reader/image. That works
|
||||||
|
* for image/archive content, but EPUB/TXT can fail while Kavita is preparing an
|
||||||
|
* image cache. For document files, download the original chapter and use
|
||||||
|
* Mihon's native document reader instead. Other formats keep the extension's
|
||||||
|
* normal HttpPageLoader behavior.
|
||||||
|
*/
|
||||||
|
internal class KavitaCompatPageLoader(
|
||||||
|
private val context: Context,
|
||||||
|
private val chapter: ReaderChapter,
|
||||||
|
private val source: HttpSource,
|
||||||
|
private val chapterCache: ChapterCache,
|
||||||
|
private val chapterId: Int,
|
||||||
|
private val textStyle: TextReaderStyle,
|
||||||
|
) : PageLoader() {
|
||||||
|
|
||||||
|
override var isLocal: Boolean = false
|
||||||
|
override val usesTextReaderStyle: Boolean
|
||||||
|
get() = delegate?.usesTextReaderStyle == true
|
||||||
|
|
||||||
|
private var delegate: PageLoader? = null
|
||||||
|
|
||||||
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
|
check(!isRecycled)
|
||||||
|
|
||||||
|
val format = detectDocumentFormat()
|
||||||
|
val loader = when (format) {
|
||||||
|
"epub", "txt" -> createDocumentLoader(format)
|
||||||
|
else -> HttpPageLoader(chapter, source, chapterCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate = loader
|
||||||
|
isLocal = loader.isLocal
|
||||||
|
return loader.getPages()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadPage(page: ReaderPage) {
|
||||||
|
delegate?.loadPage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retryPage(page: ReaderPage) {
|
||||||
|
delegate?.retryPage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recycle() {
|
||||||
|
delegate?.recycle()
|
||||||
|
delegate = null
|
||||||
|
super.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun detectDocumentFormat(): String? {
|
||||||
|
val apiBase = source.baseUrl.trimEnd('/') + "/api"
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$apiBase/Chapter?chapterId=$chapterId")
|
||||||
|
.headers(source.headers)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
source.client.newCall(request).await().use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
// Preserve the extension's normal behavior when chapter metadata
|
||||||
|
// cannot be queried. It may still be readable as image content.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body.string()
|
||||||
|
if (body.isBlank()) return null
|
||||||
|
|
||||||
|
val chapterJson = JSONObject(body)
|
||||||
|
val files = chapterJson.optJSONArray("files") ?: return null
|
||||||
|
for (i in 0 until files.length()) {
|
||||||
|
val file = files.optJSONObject(i) ?: continue
|
||||||
|
val extension = normalizeExtension(
|
||||||
|
file.optString("extension").ifBlank { file.optString("filePath") },
|
||||||
|
)
|
||||||
|
if (extension == "epub" || extension == "txt") {
|
||||||
|
return extension
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current Kavita format enum: EPUB == 3. TXT has no dedicated
|
||||||
|
// enum value in upstream Kavita, so it is detected by extension.
|
||||||
|
if (file.optInt("format", -1) == 3) {
|
||||||
|
return "epub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeExtension(value: String): String =
|
||||||
|
value.substringAfterLast('.', value)
|
||||||
|
.trim()
|
||||||
|
.trimStart('.')
|
||||||
|
.lowercase(Locale.ROOT)
|
||||||
|
|
||||||
|
private suspend fun createDocumentLoader(format: String): PageLoader {
|
||||||
|
val file = getOrDownloadDocument(format)
|
||||||
|
val uniFile = UniFile.fromFile(file)
|
||||||
|
?: throw IOException("Unable to open cached Kavita ${format.uppercase()} file")
|
||||||
|
|
||||||
|
return when (format) {
|
||||||
|
"epub" -> EpubPageLoader(uniFile.epubReader(context), textStyle)
|
||||||
|
"txt" -> TextPageLoader(uniFile, textStyle)
|
||||||
|
else -> error("Unsupported Kavita document format: $format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getOrDownloadDocument(format: String): File {
|
||||||
|
val cacheDir = File(context.cacheDir, "kavita_reader").apply { mkdirs() }
|
||||||
|
val target = File(cacheDir, "${source.id}-$chapterId.$format")
|
||||||
|
if (target.isFile && target.length() > 0L) return target
|
||||||
|
|
||||||
|
val temp = File(cacheDir, "${target.name}.part")
|
||||||
|
if (temp.exists()) temp.delete()
|
||||||
|
|
||||||
|
val apiBase = source.baseUrl.trimEnd('/') + "/api"
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$apiBase/Download/chapter?chapterId=$chapterId")
|
||||||
|
.headers(source.headers)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
source.client.newCall(request).await().use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
val message = when (response.code) {
|
||||||
|
403 -> "Kavita account does not have Download permission"
|
||||||
|
404 -> "Kavita original file is missing or no longer available"
|
||||||
|
500 -> "Kavita could not access the original $format file"
|
||||||
|
else -> "Kavita document download failed: HTTP ${response.code}"
|
||||||
|
}
|
||||||
|
throw IOException(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
temp.outputStream().use { output ->
|
||||||
|
response.body.byteStream().use { input -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.length() <= 0L) {
|
||||||
|
temp.delete()
|
||||||
|
throw IOException("Kavita returned an empty ${format.uppercase()} file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!temp.renameTo(target)) {
|
||||||
|
temp.copyTo(target, overwrite = true)
|
||||||
|
temp.delete()
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,17 @@ import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ReaderSettingsViewModel(
|
class ReaderSettingsViewModel(
|
||||||
readerState: StateFlow<ReaderViewModel.State>,
|
readerState: StateFlow<ReaderViewModel.State>,
|
||||||
val onChangeReadingMode: (ReadingMode) -> Unit,
|
val onChangeReadingMode: (ReadingMode) -> Unit,
|
||||||
val onChangeOrientation: (ReaderOrientation) -> Unit,
|
val onChangeOrientation: (ReaderOrientation) -> Unit,
|
||||||
val onTextSettingsChanged: () -> Unit,
|
private val onTextSettingsChanged: () -> Unit,
|
||||||
val preferences: ReaderPreferences,
|
val preferences: ReaderPreferences,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -26,4 +29,22 @@ class ReaderSettingsViewModel(
|
|||||||
.map { it.manga }
|
.map { it.manga }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
||||||
|
|
||||||
|
private var textSettingsReloadJob: Job? = null
|
||||||
|
|
||||||
|
fun requestTextSettingsReload() {
|
||||||
|
textSettingsReloadJob?.cancel()
|
||||||
|
textSettingsReloadJob = viewModelScope.launch {
|
||||||
|
delay(150)
|
||||||
|
onTextSettingsChanged()
|
||||||
|
textSettingsReloadJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flushTextSettingsReload() {
|
||||||
|
if (textSettingsReloadJob == null) return
|
||||||
|
textSettingsReloadJob?.cancel()
|
||||||
|
textSettingsReloadJob = null
|
||||||
|
onTextSettingsChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user