Yellow color filter in reader

This commit is contained in:
Koitharu
2025-07-05 10:08:13 +03:00
parent add5c7dc17
commit aafefffc27
12 changed files with 114 additions and 20 deletions

View File

@@ -42,6 +42,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration23To24
import org.koitharu.kotatsu.core.db.migrations.Migration24To23 import org.koitharu.kotatsu.core.db.migrations.Migration24To23
import org.koitharu.kotatsu.core.db.migrations.Migration24To25 import org.koitharu.kotatsu.core.db.migrations.Migration24To25
import org.koitharu.kotatsu.core.db.migrations.Migration25To26 import org.koitharu.kotatsu.core.db.migrations.Migration25To26
import org.koitharu.kotatsu.core.db.migrations.Migration26To27
import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5 import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -69,7 +70,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 26 const val DATABASE_VERSION = 27
@Database( @Database(
entities = [ entities = [
@@ -140,6 +141,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration24To23(), Migration24To23(),
Migration24To25(), Migration24To25(),
Migration25To26(), Migration25To26(),
Migration26To27(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -26,6 +26,7 @@ data class MangaPrefsEntity(
@ColumnInfo(name = "cf_contrast") val cfContrast: Float, @ColumnInfo(name = "cf_contrast") val cfContrast: Float,
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean, @ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
@ColumnInfo(name = "cf_grayscale") val cfGrayscale: Boolean, @ColumnInfo(name = "cf_grayscale") val cfGrayscale: Boolean,
@ColumnInfo(name = "cf_book") val cfBookEffect: Boolean,
@ColumnInfo(name = "title_override") val titleOverride: String?, @ColumnInfo(name = "title_override") val titleOverride: String?,
@ColumnInfo(name = "cover_override") val coverUrlOverride: String?, @ColumnInfo(name = "cover_override") val coverUrlOverride: String?,
@ColumnInfo(name = "content_rating_override") val contentRatingOverride: String?, @ColumnInfo(name = "content_rating_override") val contentRatingOverride: String?,

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration26To27 : Migration(26, 27) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE preferences ADD COLUMN cf_book INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -197,8 +197,14 @@ class MangaDataRepository @Inject constructor(
} }
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? { private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale) { return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale || cfBookEffect) {
ReaderColorFilter(cfBrightness, cfContrast, cfInvert, cfGrayscale) ReaderColorFilter(
brightness = cfBrightness,
contrast = cfContrast,
isInverted = cfInvert,
isGrayscale = cfGrayscale,
isBookBackground = cfBookEffect
)
} else { } else {
null null
} }
@@ -219,10 +225,11 @@ class MangaDataRepository @Inject constructor(
private fun newEntity(mangaId: Long) = MangaPrefsEntity( private fun newEntity(mangaId: Long) = MangaPrefsEntity(
mangaId = mangaId, mangaId = mangaId,
mode = -1, mode = -1,
cfBrightness = 0f, cfBrightness = ReaderColorFilter.EMPTY.brightness,
cfContrast = 0f, cfContrast = ReaderColorFilter.EMPTY.contrast,
cfInvert = false, cfInvert = ReaderColorFilter.EMPTY.isInverted,
cfGrayscale = false, cfGrayscale = ReaderColorFilter.EMPTY.isGrayscale,
cfBookEffect = ReaderColorFilter.EMPTY.isBookBackground,
titleOverride = null, titleOverride = null,
coverUrlOverride = null, coverUrlOverride = null,
contentRatingOverride = null, contentRatingOverride = null,

View File

@@ -401,19 +401,29 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
var readerColorFilter: ReaderColorFilter? var readerColorFilter: ReaderColorFilter?
get() = runCatching { get() = runCatching {
val brightness = prefs.getFloat(KEY_CF_BRIGHTNESS, ReaderColorFilter.EMPTY.brightness) ReaderColorFilter(
val contrast = prefs.getFloat(KEY_CF_CONTRAST, ReaderColorFilter.EMPTY.contrast) brightness = prefs.getFloat(KEY_CF_BRIGHTNESS, ReaderColorFilter.EMPTY.brightness),
val inverted = prefs.getBoolean(KEY_CF_INVERTED, ReaderColorFilter.EMPTY.isInverted) contrast = prefs.getFloat(KEY_CF_CONTRAST, ReaderColorFilter.EMPTY.contrast),
val grayscale = prefs.getBoolean(KEY_CF_GRAYSCALE, ReaderColorFilter.EMPTY.isGrayscale) isInverted = prefs.getBoolean(KEY_CF_INVERTED, ReaderColorFilter.EMPTY.isInverted),
ReaderColorFilter(brightness, contrast, inverted, grayscale).takeUnless { it.isEmpty } isGrayscale = prefs.getBoolean(KEY_CF_GRAYSCALE, ReaderColorFilter.EMPTY.isGrayscale),
isBookBackground = prefs.getBoolean(KEY_CF_BOOK, ReaderColorFilter.EMPTY.isBookBackground),
).takeUnless { it.isEmpty }
}.getOrNull() }.getOrNull()
set(value) { set(value) {
prefs.edit { prefs.edit {
val cf = value ?: ReaderColorFilter.EMPTY if (value != null) {
putFloat(KEY_CF_BRIGHTNESS, cf.brightness) putFloat(KEY_CF_BRIGHTNESS, value.brightness)
putFloat(KEY_CF_CONTRAST, cf.contrast) putFloat(KEY_CF_CONTRAST, value.contrast)
putBoolean(KEY_CF_INVERTED, cf.isInverted) putBoolean(KEY_CF_INVERTED, value.isInverted)
putBoolean(KEY_CF_GRAYSCALE, cf.isGrayscale) putBoolean(KEY_CF_GRAYSCALE, value.isGrayscale)
putBoolean(KEY_CF_BOOK, value.isBookBackground)
} else {
remove(KEY_CF_BRIGHTNESS)
remove(KEY_CF_CONTRAST)
remove(KEY_CF_INVERTED)
remove(KEY_CF_GRAYSCALE)
remove(KEY_CF_BOOK)
}
} }
} }
@@ -740,6 +750,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_CF_CONTRAST = "cf_contrast" const val KEY_CF_CONTRAST = "cf_contrast"
const val KEY_CF_INVERTED = "cf_inverted" const val KEY_CF_INVERTED = "cf_inverted"
const val KEY_CF_GRAYSCALE = "cf_grayscale" const val KEY_CF_GRAYSCALE = "cf_grayscale"
const val KEY_CF_BOOK = "cf_book"
const val KEY_PAGES_TAB = "pages_tab" const val KEY_PAGES_TAB = "pages_tab"
const val KEY_DETAILS_TAB = "details_tab" const val KEY_DETAILS_TAB = "details_tab"
const val KEY_DETAILS_LAST_TAB = "details_last_tab" const val KEY_DETAILS_LAST_TAB = "details_last_tab"

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.reader.domain package org.koitharu.kotatsu.reader.domain
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.ColorMatrix import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter import android.graphics.ColorMatrixColorFilter
@@ -8,10 +10,11 @@ data class ReaderColorFilter(
val contrast: Float, val contrast: Float,
val isInverted: Boolean, val isInverted: Boolean,
val isGrayscale: Boolean, val isGrayscale: Boolean,
val isBookBackground: Boolean,
) { ) {
val isEmpty: Boolean val isEmpty: Boolean
get() = !isGrayscale && !isInverted && brightness == 0f && contrast == 0f get() = !isGrayscale && !isInverted && !isBookBackground && brightness == 0f && contrast == 0f
fun toColorFilter(): ColorMatrixColorFilter { fun toColorFilter(): ColorMatrixColorFilter {
val cm = ColorMatrix() val cm = ColorMatrix()
@@ -23,9 +26,19 @@ data class ReaderColorFilter(
} }
cm.setBrightness(brightness) cm.setBrightness(brightness)
cm.setContrast(contrast) cm.setContrast(contrast)
if (isBookBackground) {
cm.addBookEffect()
}
return ColorMatrixColorFilter(cm) return ColorMatrixColorFilter(cm)
} }
fun getBackgroundTint(): ColorStateList? = if (isBookBackground) {
val color = Color.rgb(255, 255, (255 * BOOK_BLUE_FACTOR).toInt())
ColorStateList.valueOf(color)
} else {
null
}
private fun ColorMatrix.setBrightness(brightness: Float) { private fun ColorMatrix.setBrightness(brightness: Float) {
val scale = brightness + 1f val scale = brightness + 1f
val matrix = ColorMatrix() val matrix = ColorMatrix()
@@ -60,13 +73,26 @@ data class ReaderColorFilter(
setSaturation(0f) setSaturation(0f)
} }
private fun ColorMatrix.addBookEffect() {
val removeBlueMatrix = floatArrayOf(
1f, 0f, 0f, 0f, 0f,
0f, 1f, 0f, 0f, 0f,
0f, 0f, BOOK_BLUE_FACTOR, 0f, 0f,
0f, 0f, 0f, 1f, 0f,
)
postConcat(ColorMatrix(removeBlueMatrix))
}
companion object { companion object {
private const val BOOK_BLUE_FACTOR = 0.92f
val EMPTY = ReaderColorFilter( val EMPTY = ReaderColorFilter(
brightness = 0.0f, brightness = 0.0f,
contrast = 0.0f, contrast = 0.0f,
isInverted = false, isInverted = false,
isGrayscale = false, isGrayscale = false,
isBookBackground = false,
) )
} }
} }

View File

@@ -53,6 +53,7 @@ class ColorFilterConfigActivity :
viewBinding.sliderBrightness.setLabelFormatter(formatter) viewBinding.sliderBrightness.setLabelFormatter(formatter)
viewBinding.switchInvert.setOnCheckedChangeListener(this) viewBinding.switchInvert.setOnCheckedChangeListener(this)
viewBinding.switchGrayscale.setOnCheckedChangeListener(this) viewBinding.switchGrayscale.setOnCheckedChangeListener(this)
viewBinding.switchBook.setOnCheckedChangeListener(this)
viewBinding.buttonDone.setOnClickListener(this) viewBinding.buttonDone.setOnClickListener(this)
viewBinding.buttonReset.setOnClickListener(this) viewBinding.buttonReset.setOnClickListener(this)
@@ -93,6 +94,7 @@ class ColorFilterConfigActivity :
when (buttonView.id) { when (buttonView.id) {
R.id.switch_invert -> viewModel.setInversion(isChecked) R.id.switch_invert -> viewModel.setInversion(isChecked)
R.id.switch_grayscale -> viewModel.setGrayscale(isChecked) R.id.switch_grayscale -> viewModel.setGrayscale(isChecked)
R.id.switch_book -> viewModel.setBookEffect(isChecked)
} }
} }
@@ -120,6 +122,7 @@ class ColorFilterConfigActivity :
viewBinding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f) viewBinding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f)
viewBinding.switchInvert.setChecked(readerColorFilter?.isInverted == true, false) viewBinding.switchInvert.setChecked(readerColorFilter?.isInverted == true, false)
viewBinding.switchGrayscale.setChecked(readerColorFilter?.isGrayscale == true, false) viewBinding.switchGrayscale.setChecked(readerColorFilter?.isGrayscale == true, false)
viewBinding.switchBook.setChecked(readerColorFilter?.isBookBackground == true, false)
viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
} }

View File

@@ -56,6 +56,10 @@ class ColorFilterConfigViewModel @Inject constructor(
updateColorFilter { it.copy(isGrayscale = grayscale) } updateColorFilter { it.copy(isGrayscale = grayscale) }
} }
fun setBookEffect(book: Boolean) {
updateColorFilter { it.copy(isBookBackground = book) }
}
fun reset() { fun reset() {
colorFilter.value = null colorFilter.value = null
} }

View File

@@ -57,6 +57,11 @@ data class ReaderSettings(
fun applyBackground(view: View) { fun applyBackground(view: View) {
view.background = background.resolve(view.context) view.background = background.resolve(view.context)
view.backgroundTintList = if (background.isLight(view.context)) {
colorFilter?.getBackgroundTint()
} else {
null
}
} }
fun isPagesCropEnabled(isWebtoon: Boolean) = if (isWebtoon) { fun isPagesCropEnabled(isWebtoon: Boolean) = if (isWebtoon) {

View File

@@ -191,6 +191,18 @@
app:layout_constraintStart_toEndOf="@id/guideline_vertical" app:layout_constraintStart_toEndOf="@id/guideline_vertical"
app:layout_constraintTop_toBottomOf="@id/textView_contrast" /> app:layout_constraintTop_toBottomOf="@id/textView_contrast" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_book"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="8dp"
android:text="@string/book_effect"
android:textAppearance="?textAppearanceTitleMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline_vertical"
app:layout_constraintTop_toBottomOf="@id/slider_contrast" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>

View File

@@ -170,15 +170,26 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_contrast" /> app:layout_constraintTop_toBottomOf="@id/textView_contrast" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_book"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/book_effect"
android:textAppearance="?textAppearanceTitleMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_contrast" />
<Button <Button
android:id="@+id/button_reset" android:id="@+id/button_reset"
style="?materialButtonOutlinedStyle" style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="16dp"
android:text="@string/reset" android:text="@string/reset"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_contrast" /> app:layout_constraintTop_toBottomOf="@id/switch_book" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -853,4 +853,5 @@
<string name="reader_multitask_summary">Allows you to keep multiple readers with different manga open at the same time</string> <string name="reader_multitask_summary">Allows you to keep multiple readers with different manga open at the same time</string>
<string name="theme_name_itsuka">Itsuka</string> <string name="theme_name_itsuka">Itsuka</string>
<string name="theme_name_totoro">Totoro</string> <string name="theme_name_totoro">Totoro</string>
<string name="book_effect">Yellowish background (blue filter)</string>
</resources> </resources>