Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0e0ef1575 |
@@ -53,7 +53,7 @@ internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
|
|||||||
selected = readerTheme == value,
|
selected = readerTheme == value,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.preferences.readerTheme.set(value)
|
viewModel.preferences.readerTheme.set(value)
|
||||||
viewModel.requestTextSettingsReload()
|
viewModel.requestTextAppearanceReload()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(labelRes)) },
|
label = { Text(stringResource(labelRes)) },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -186,6 +187,12 @@ class ReaderViewModel(
|
|||||||
*/
|
*/
|
||||||
private var loader: ChapterLoader? = null
|
private var loader: ChapterLoader? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only the newest text repagination request is allowed to finish. Slider
|
||||||
|
* changes can arrive faster than a large TXT/EPUB chapter can be rebuilt.
|
||||||
|
*/
|
||||||
|
private var textRepaginationJob: Job? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the chapter was started reading
|
* The time the chapter was started reading
|
||||||
*/
|
*/
|
||||||
@@ -841,17 +848,26 @@ class ReaderViewModel(
|
|||||||
if (chapter.pageLoader?.usesTextReaderStyle != true) return
|
if (chapter.pageLoader?.usesTextReaderStyle != true) return
|
||||||
if (chapter.pages?.any { it is TextReaderPage } != true) return
|
if (chapter.pages?.any { it is TextReaderPage } != true) return
|
||||||
val chapterLoader = loader ?: return
|
val chapterLoader = loader ?: return
|
||||||
val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0)
|
|
||||||
val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
|
|
||||||
val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex
|
|
||||||
|
|
||||||
viewModelScope.launchIO {
|
val oldPageCount = chapter.pages?.size?.coerceAtLeast(1) ?: 1
|
||||||
|
val requestedPage = chapter.requestedPage.coerceIn(0, oldPageCount - 1)
|
||||||
|
// Anchor at the middle of the visible page so position remains stable
|
||||||
|
// when font size/spacing changes the total page count.
|
||||||
|
val readingProgress = ((requestedPage + 0.5f) / oldPageCount)
|
||||||
|
.coerceIn(0f, 0.999999f)
|
||||||
|
|
||||||
|
textRepaginationJob?.cancel()
|
||||||
|
textRepaginationJob = viewModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
val oldLoader = chapterLoader.reloadTextChapter(chapter, readingProgress) ?: return@launchIO
|
val oldLoader = chapterLoader.reloadTextChapter(chapter, readingProgress) ?: return@launchIO
|
||||||
eventChannel.send(Event.ReloadTextViewerChapters)
|
|
||||||
// Give old visible holders time to detach before closing resources such as EpubReader.
|
// Resource cleanup must survive cancellation by the next slider value.
|
||||||
delay(2000)
|
viewModelScope.launchNonCancellable {
|
||||||
oldLoader.recycle()
|
delay(750)
|
||||||
|
oldLoader.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
eventChannel.trySend(Event.ReloadTextViewerChapters)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to repaginate text reader style" }
|
logcat(LogPriority.ERROR, e) { "Failed to repaginate text reader style" }
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import tachiyomi.domain.source.model.StubSource
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.source.local.LocalSource
|
import tachiyomi.source.local.LocalSource
|
||||||
import tachiyomi.source.local.io.Format
|
import tachiyomi.source.local.io.Format
|
||||||
import kotlin.math.roundToInt
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to retrieve the [PageLoader] for a given chapter.
|
* Loader used to retrieve the [PageLoader] for a given chapter.
|
||||||
@@ -90,10 +91,14 @@ class ChapterLoader(
|
|||||||
throw Exception(context.stringResource(MR.strings.page_list_empty_error))
|
throw Exception(context.stringResource(MR.strings.page_list_empty_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a newer slider value cancelled this rebuild while getPages()
|
||||||
|
// was working, do not publish stale pages into the active chapter.
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
|
||||||
val oldLoader = chapter.pageLoader
|
val oldLoader = chapter.pageLoader
|
||||||
chapter.pageLoader = newLoader
|
chapter.pageLoader = newLoader
|
||||||
chapter.requestedPage = (readingProgress * pages.lastIndex)
|
chapter.requestedPage = (readingProgress.coerceIn(0f, 0.999999f) * pages.size)
|
||||||
.roundToInt()
|
.toInt()
|
||||||
.coerceIn(0, pages.lastIndex)
|
.coerceIn(0, pages.lastIndex)
|
||||||
chapter.state = ReaderChapter.State.Loaded(pages)
|
chapter.state = ReaderChapter.State.Loaded(pages)
|
||||||
oldLoader
|
oldLoader
|
||||||
|
|||||||
@@ -16,13 +16,10 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
internal object TextPageRenderer {
|
internal object TextPageRenderer {
|
||||||
private const val PAGE_WIDTH = 1440
|
private const val MARGIN_DP = 32f
|
||||||
private const val PAGE_HEIGHT = 2160
|
|
||||||
private const val MARGIN = 96
|
|
||||||
private const val CONTENT_WIDTH = PAGE_WIDTH - (MARGIN * 2)
|
|
||||||
private const val CONTENT_HEIGHT = PAGE_HEIGHT - (MARGIN * 2)
|
|
||||||
private const val STREAM_BUFFER_CHARS = 64 * 1024
|
private const val STREAM_BUFFER_CHARS = 64 * 1024
|
||||||
private const val STREAM_READ_CHARS = 8 * 1024
|
private const val STREAM_READ_CHARS = 8 * 1024
|
||||||
|
|
||||||
@@ -101,7 +98,8 @@ internal object TextPageRenderer {
|
|||||||
private fun calculatePageSlices(text: String, style: TextReaderStyle): List<PageSlice> {
|
private fun calculatePageSlices(text: String, style: TextReaderStyle): List<PageSlice> {
|
||||||
if (text.isEmpty()) return emptyList()
|
if (text.isEmpty()) return emptyList()
|
||||||
|
|
||||||
val layout = createLayout(text, style)
|
val metrics = resolveLayoutMetrics(style)
|
||||||
|
val layout = createLayout(text, style, metrics.contentWidth)
|
||||||
if (layout.lineCount == 0) return listOf(PageSlice(0, text.length))
|
if (layout.lineCount == 0) return listOf(PageSlice(0, text.length))
|
||||||
|
|
||||||
val pages = mutableListOf<PageSlice>()
|
val pages = mutableListOf<PageSlice>()
|
||||||
@@ -111,7 +109,7 @@ internal object TextPageRenderer {
|
|||||||
var endLine = startLine
|
var endLine = startLine
|
||||||
while (
|
while (
|
||||||
endLine + 1 < layout.lineCount &&
|
endLine + 1 < layout.lineCount &&
|
||||||
layout.getLineBottom(endLine + 1) - startTop <= CONTENT_HEIGHT
|
layout.getLineBottom(endLine + 1) - startTop <= metrics.contentHeight
|
||||||
) {
|
) {
|
||||||
endLine++
|
endLine++
|
||||||
}
|
}
|
||||||
@@ -125,13 +123,14 @@ internal object TextPageRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun render(text: String, style: TextReaderStyle): InputStream {
|
fun render(text: String, style: TextReaderStyle): InputStream {
|
||||||
val bitmap = Bitmap.createBitmap(PAGE_WIDTH, PAGE_HEIGHT, Bitmap.Config.ARGB_8888)
|
val metrics = resolveLayoutMetrics(style)
|
||||||
|
val bitmap = Bitmap.createBitmap(metrics.pageWidth, metrics.pageHeight, Bitmap.Config.ARGB_8888)
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
val (backgroundColor, _) = resolveColors(style)
|
val (backgroundColor, _) = resolveColors(style)
|
||||||
canvas.drawColor(backgroundColor)
|
canvas.drawColor(backgroundColor)
|
||||||
canvas.save()
|
canvas.save()
|
||||||
canvas.translate(MARGIN.toFloat(), MARGIN.toFloat())
|
canvas.translate(metrics.horizontalMargin.toFloat(), metrics.verticalMargin.toFloat())
|
||||||
createLayout(text, style).draw(canvas)
|
createLayout(text, style, metrics.contentWidth).draw(canvas)
|
||||||
canvas.restore()
|
canvas.restore()
|
||||||
|
|
||||||
val output = ByteArrayOutputStream()
|
val output = ByteArrayOutputStream()
|
||||||
@@ -140,25 +139,46 @@ internal object TextPageRenderer {
|
|||||||
return ByteArrayInputStream(output.toByteArray())
|
return ByteArrayInputStream(output.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLayout(text: String, style: TextReaderStyle): StaticLayout {
|
private fun createLayout(
|
||||||
|
text: String,
|
||||||
|
style: TextReaderStyle,
|
||||||
|
contentWidth: Int,
|
||||||
|
): StaticLayout {
|
||||||
val (_, textColor) = resolveColors(style)
|
val (_, textColor) = resolveColors(style)
|
||||||
val paint = TextPaint().apply {
|
val paint = TextPaint().apply {
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
color = textColor
|
color = textColor
|
||||||
textSize = style.fontSize * 3f
|
textSize = style.fontSizePx.coerceAtLeast(1f)
|
||||||
typeface = style.fontFamily.androidFamilyName
|
typeface = style.fontFamily.androidFamilyName
|
||||||
?.let { Typeface.create(it, Typeface.NORMAL) }
|
?.let { Typeface.create(it, Typeface.NORMAL) }
|
||||||
?: Typeface.DEFAULT
|
?: Typeface.DEFAULT
|
||||||
}
|
}
|
||||||
val styledText = addParagraphSpacing(text, style.paragraphSpacing * 3)
|
val styledText = addParagraphSpacing(text, style.paragraphSpacingPx)
|
||||||
return StaticLayout.Builder
|
return StaticLayout.Builder
|
||||||
.obtain(styledText, 0, styledText.length, paint, CONTENT_WIDTH)
|
.obtain(styledText, 0, styledText.length, paint, contentWidth)
|
||||||
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
||||||
.setIncludePad(true)
|
.setIncludePad(true)
|
||||||
.setLineSpacing(0f, style.lineSpacingPercent / 100f)
|
.setLineSpacing(0f, style.lineSpacingPercent / 100f)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveLayoutMetrics(style: TextReaderStyle): LayoutMetrics {
|
||||||
|
val pageWidth = style.pageWidthPx.coerceAtLeast(1)
|
||||||
|
val pageHeight = style.pageHeightPx.coerceAtLeast(1)
|
||||||
|
val requestedMargin = (MARGIN_DP * style.density).roundToInt().coerceAtLeast(1)
|
||||||
|
val horizontalMargin = requestedMargin.coerceAtMost(((pageWidth - 1) / 2).coerceAtLeast(0))
|
||||||
|
val verticalMargin = requestedMargin.coerceAtMost(((pageHeight - 1) / 2).coerceAtLeast(0))
|
||||||
|
|
||||||
|
return LayoutMetrics(
|
||||||
|
pageWidth = pageWidth,
|
||||||
|
pageHeight = pageHeight,
|
||||||
|
horizontalMargin = horizontalMargin,
|
||||||
|
verticalMargin = verticalMargin,
|
||||||
|
contentWidth = (pageWidth - horizontalMargin * 2).coerceAtLeast(1),
|
||||||
|
contentHeight = (pageHeight - verticalMargin * 2).coerceAtLeast(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun resolveColors(style: TextReaderStyle): Pair<Int, Int> {
|
private fun resolveColors(style: TextReaderStyle): Pair<Int, Int> {
|
||||||
val useDark = when (style.readerTheme) {
|
val useDark = when (style.readerTheme) {
|
||||||
0 -> false
|
0 -> false
|
||||||
@@ -205,6 +225,15 @@ internal object TextPageRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class LayoutMetrics(
|
||||||
|
val pageWidth: Int,
|
||||||
|
val pageHeight: Int,
|
||||||
|
val horizontalMargin: Int,
|
||||||
|
val verticalMargin: Int,
|
||||||
|
val contentWidth: Int,
|
||||||
|
val contentHeight: Int,
|
||||||
|
)
|
||||||
|
|
||||||
private data class PageSlice(
|
private data class PageSlice(
|
||||||
val start: Int,
|
val start: Int,
|
||||||
val endExclusive: Int,
|
val endExclusive: Int,
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A rendered text page whose style can be changed without rebuilding the chapter.
|
* A rendered text page whose visual style can be refreshed cheaply while layout-affecting
|
||||||
* Page boundaries remain stable during the live preview; a full repagination is done
|
* changes trigger a debounced chapter repagination to rebuild page boundaries.
|
||||||
* once when the settings dialog is dismissed.
|
|
||||||
*/
|
*/
|
||||||
internal class TextReaderPage(
|
internal class TextReaderPage(
|
||||||
index: Int,
|
index: Int,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.setting
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.TypedValue
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
@@ -137,15 +138,44 @@ class ReaderPreferences(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun textReaderStyle(context: Context) = TextReaderStyle(
|
fun textReaderStyle(context: Context): TextReaderStyle {
|
||||||
fontSize = textFontSize.get(),
|
val displayMetrics = context.resources.displayMetrics
|
||||||
lineSpacingPercent = textLineSpacing.get(),
|
val rawWidth = displayMetrics.widthPixels.coerceAtLeast(1)
|
||||||
paragraphSpacing = textParagraphSpacing.get(),
|
val rawHeight = displayMetrics.heightPixels.coerceAtLeast(1)
|
||||||
fontFamily = textFontFamily.get(),
|
|
||||||
readerTheme = readerTheme.get(),
|
// Keep the same physical page proportions as the device while capping the
|
||||||
automaticDark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
// raster size so high-resolution phones do not create oversized bitmaps.
|
||||||
Configuration.UI_MODE_NIGHT_YES,
|
val renderScale = minOf(
|
||||||
)
|
1f,
|
||||||
|
TEXT_READER_MAX_RENDER_WIDTH / rawWidth.toFloat(),
|
||||||
|
TEXT_READER_MAX_RENDER_HEIGHT / rawHeight.toFloat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return TextReaderStyle(
|
||||||
|
fontSize = textFontSize.get(),
|
||||||
|
lineSpacingPercent = textLineSpacing.get(),
|
||||||
|
paragraphSpacing = textParagraphSpacing.get(),
|
||||||
|
fontFamily = textFontFamily.get(),
|
||||||
|
readerTheme = readerTheme.get(),
|
||||||
|
automaticDark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
||||||
|
Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
pageWidthPx = (rawWidth * renderScale).roundToInt().coerceAtLeast(1),
|
||||||
|
pageHeightPx = (rawHeight * renderScale).roundToInt().coerceAtLeast(1),
|
||||||
|
density = displayMetrics.density * renderScale,
|
||||||
|
fontSizePx = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_SP,
|
||||||
|
textFontSize.get().toFloat(),
|
||||||
|
displayMetrics,
|
||||||
|
) * renderScale,
|
||||||
|
paragraphSpacingPx = (
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
textParagraphSpacing.get().toFloat(),
|
||||||
|
displayMetrics,
|
||||||
|
) * renderScale
|
||||||
|
).roundToInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@@ -336,6 +366,9 @@ class ReaderPreferences(
|
|||||||
const val WEBTOON_PADDING_MIN = 0
|
const val WEBTOON_PADDING_MIN = 0
|
||||||
const val WEBTOON_PADDING_MAX = 25
|
const val WEBTOON_PADDING_MAX = 25
|
||||||
|
|
||||||
|
private const val TEXT_READER_MAX_RENDER_WIDTH = 1440
|
||||||
|
private const val TEXT_READER_MAX_RENDER_HEIGHT = 2400
|
||||||
|
|
||||||
const val MILLI_CONVERSION = 100
|
const val MILLI_CONVERSION = 100
|
||||||
|
|
||||||
val TapZones = listOf(
|
val TapZones = listOf(
|
||||||
@@ -399,4 +432,9 @@ data class TextReaderStyle(
|
|||||||
val fontFamily: ReaderPreferences.TextFontFamily,
|
val fontFamily: ReaderPreferences.TextFontFamily,
|
||||||
val readerTheme: Int,
|
val readerTheme: Int,
|
||||||
val automaticDark: Boolean,
|
val automaticDark: Boolean,
|
||||||
|
val pageWidthPx: Int,
|
||||||
|
val pageHeightPx: Int,
|
||||||
|
val density: Float,
|
||||||
|
val fontSizePx: Float,
|
||||||
|
val paragraphSpacingPx: Int,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,20 +32,49 @@ class ReaderSettingsViewModel(
|
|||||||
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
||||||
|
|
||||||
private var textSettingsReloadJob: Job? = null
|
private var textSettingsReloadJob: Job? = null
|
||||||
|
private var textAppearanceReloadJob: Job? = null
|
||||||
private var textSettingsDirty = false
|
private var textSettingsDirty = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout-affecting text settings are previewed quickly, then the chapter is
|
||||||
|
* repaginated after a short debounce. This keeps the slider responsive while
|
||||||
|
* ensuring the number of characters/lines on a page follows the new style.
|
||||||
|
*/
|
||||||
fun requestTextSettingsReload() {
|
fun requestTextSettingsReload() {
|
||||||
textSettingsDirty = true
|
textSettingsDirty = true
|
||||||
textSettingsReloadJob?.cancel()
|
textSettingsReloadJob?.cancel()
|
||||||
textSettingsReloadJob = viewModelScope.launch {
|
textSettingsReloadJob = viewModelScope.launch {
|
||||||
delay(200)
|
delay(250)
|
||||||
onTextSettingsPreview()
|
onTextSettingsPreview()
|
||||||
|
textSettingsDirty = false
|
||||||
|
onTextSettingsCommit()
|
||||||
textSettingsReloadJob = null
|
textSettingsReloadJob = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme-only changes do not affect page boundaries, so avoid an unnecessary
|
||||||
|
* full repagination.
|
||||||
|
*/
|
||||||
|
fun requestTextAppearanceReload() {
|
||||||
|
textAppearanceReloadJob?.cancel()
|
||||||
|
textAppearanceReloadJob = viewModelScope.launch {
|
||||||
|
delay(50)
|
||||||
|
onTextSettingsPreview()
|
||||||
|
textAppearanceReloadJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun flushTextSettingsReload() {
|
fun flushTextSettingsReload() {
|
||||||
if (!textSettingsDirty) return
|
val hadPendingAppearanceReload = textAppearanceReloadJob != null
|
||||||
|
textAppearanceReloadJob?.cancel()
|
||||||
|
textAppearanceReloadJob = null
|
||||||
|
if (!textSettingsDirty) {
|
||||||
|
if (hadPendingAppearanceReload) {
|
||||||
|
onTextSettingsPreview()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
textSettingsReloadJob?.cancel()
|
textSettingsReloadJob?.cancel()
|
||||||
textSettingsReloadJob = null
|
textSettingsReloadJob = null
|
||||||
onTextSettingsPreview()
|
onTextSettingsPreview()
|
||||||
|
|||||||
Reference in New Issue
Block a user