Color filter support in reader

This commit is contained in:
Koitharu
2022-08-27 16:37:46 +03:00
parent 6a0a4023ad
commit 92aa96a644
29 changed files with 578 additions and 177 deletions

View File

@@ -135,7 +135,7 @@
android:label="@string/sync" /> android:label="@string/sync" />
<activity <activity
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity" android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
android:label="@string/color_filter" /> android:label="@string/color_correction" />
<service <service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService" android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"

View File

@@ -10,6 +10,9 @@ import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -23,6 +26,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
private const val MIN_WEBTOON_RATIO = 2 private const val MIN_WEBTOON_RATIO = 2
@@ -31,15 +35,22 @@ class MangaDataRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
) { ) {
suspend fun savePreferences(manga: Manga, mode: ReaderMode) { suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
val tags = manga.tags.toEntities()
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) storeManga(manga)
db.mangaDao.upsert(manga.toEntity(), tags) val entity = db.preferencesDao.find(manga.id) ?: newEntity(manga.id)
db.preferencesDao.upsert(entity.copy(mode = mode.id))
}
}
suspend fun saveColorFilter(manga: Manga, colorFilter: ReaderColorFilter?) {
db.withTransaction {
storeManga(manga)
val entity = db.preferencesDao.find(manga.id) ?: newEntity(manga.id)
db.preferencesDao.upsert( db.preferencesDao.upsert(
MangaPrefsEntity( entity.copy(
mangaId = manga.id, cfBrightness = colorFilter?.brightness ?: 0f,
mode = mode.id, cfContrast = colorFilter?.contrast ?: 0f,
), ),
) )
} }
@@ -49,6 +60,16 @@ class MangaDataRepository @Inject constructor(
return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) } return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
} }
suspend fun getColorFilter(mangaId: Long): ReaderColorFilter? {
return db.preferencesDao.find(mangaId)?.getColorFilterOrNull()
}
fun observeColorFilter(mangaId: Long): Flow<ReaderColorFilter?> {
return db.preferencesDao.observe(mangaId)
.map { it?.getColorFilterOrNull() }
.distinctUntilChanged()
}
suspend fun findMangaById(mangaId: Long): Manga? { suspend fun findMangaById(mangaId: Long): Manga? {
return db.mangaDao.find(mangaId)?.toManga() return db.mangaDao.find(mangaId)?.toManga()
} }
@@ -71,6 +92,14 @@ class MangaDataRepository @Inject constructor(
return db.tagsDao.findTags(source.name).toMangaTags() return db.tagsDao.findTags(source.name).toMangaTags()
} }
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
return if (cfBrightness != 0f || cfContrast != 0f) {
ReaderColorFilter(cfBrightness, cfContrast)
} else {
null
}
}
/** /**
* Automatic determine type of manga by page size * Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide * @return ReaderMode.WEBTOON if page is wide
@@ -104,6 +133,13 @@ class MangaDataRepository @Inject constructor(
return size.width * MIN_WEBTOON_RATIO < size.height return size.width * MIN_WEBTOON_RATIO < size.height
} }
private fun newEntity(mangaId: Long) = MangaPrefsEntity(
mangaId = mangaId,
mode = -1,
cfBrightness = 0f,
cfContrast = 0f,
)
companion object { companion object {
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) { suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {

View File

@@ -35,7 +35,7 @@ import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
const val DATABASE_VERSION = 14 const val DATABASE_VERSION = 15
@Database( @Database(
entities = [ entities = [
@@ -86,6 +86,7 @@ val databaseMigrations: Array<Migration>
Migration11To12(), Migration11To12(),
Migration12To13(), Migration12To13(),
Migration13To14(), Migration13To14(),
Migration14To15(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.db.dao package org.koitharu.kotatsu.core.db.dao
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
@Dao @Dao
@@ -9,6 +10,9 @@ abstract class PreferencesDao {
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId") @Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
abstract suspend fun find(mangaId: Long): MangaPrefsEntity? abstract suspend fun find(mangaId: Long): MangaPrefsEntity?
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
abstract fun observe(mangaId: Long): Flow<MangaPrefsEntity?>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(pref: MangaPrefsEntity): Long abstract suspend fun insert(pref: MangaPrefsEntity): Long
@@ -21,4 +25,4 @@ abstract class PreferencesDao {
insert(pref) insert(pref)
} }
} }
} }

View File

@@ -12,12 +12,15 @@ import androidx.room.PrimaryKey
entity = MangaEntity::class, entity = MangaEntity::class,
parentColumns = ["manga_id"], parentColumns = ["manga_id"],
childColumns = ["manga_id"], childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE,
) ),
] ],
) )
class MangaPrefsEntity( data class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id")
@ColumnInfo(name = "mode") val mode: Int val mangaId: Long,
) @ColumnInfo(name = "mode") val mode: Int,
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
)

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration14To15 : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
}
}

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@HiltViewModel @HiltViewModel
@@ -32,7 +33,7 @@ class MainViewModel @Inject constructor(
val isResumeEnabled = historyRepository val isResumeEnabled = historyRepository
.observeHasItems() .observeHasItems()
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val counters = combine( val counters = combine(
appUpdateRepository.observeAvailableUpdate(), appUpdateRepository.observeAvailableUpdate(),

View File

@@ -0,0 +1,52 @@
package org.koitharu.kotatsu.reader.domain
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
class ReaderColorFilter(
val brightness: Float,
val contrast: Float,
) {
val isEmpty: Boolean
get() = brightness == 0f && contrast == 0f
fun toColorFilter(): ColorMatrixColorFilter {
val cm = ColorMatrix()
val scale = brightness + 1f
cm.setScale(scale, scale, scale, 1f)
cm.setContrast(contrast)
return ColorMatrixColorFilter(cm)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ReaderColorFilter
if (brightness != other.brightness) return false
if (contrast != other.contrast) return false
return true
}
override fun hashCode(): Int {
var result = brightness.hashCode()
result = 31 * result + contrast.hashCode()
return result
}
private fun ColorMatrix.setContrast(contrast: Float) {
val scale = contrast + 1f
val translate = (-.5f * scale + .5f) * 255f
val array = floatArrayOf(
scale, 0f, 0f, 0f, translate,
0f, scale, 0f, 0f, translate,
0f, 0f, scale, 0f, translate,
0f, 0f, 0f, 1f, 0f,
)
val matrix = ColorMatrix(array)
postConcat(matrix)
}
}

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.data.filterChapters
import org.koitharu.kotatsu.reader.domain.ChaptersLoader import org.koitharu.kotatsu.reader.domain.ChaptersLoader
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.asFlowLiveData
@@ -86,6 +87,14 @@ class ReaderViewModel @AssistedInject constructor(
valueProducer = { isReaderBarEnabled }, valueProducer = { isReaderBarEnabled },
) )
val readerSettings = ReaderSettings(
parentScope = viewModelScope,
settings = settings,
colorFilterFlow = mangaData.flatMapLatest {
if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
)
val isScreenshotsBlockEnabled = combine( val isScreenshotsBlockEnabled = combine(
mangaData, mangaData,
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }, settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
@@ -94,8 +103,6 @@ class ReaderViewModel @AssistedInject constructor(
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val onZoomChanged = SingleLiveEvent<Unit>()
val isBookmarkAdded: LiveData<Boolean> = currentState.flatMapLatest { state -> val isBookmarkAdded: LiveData<Boolean> = currentState.flatMapLatest { state ->
val manga = mangaData.value val manga = mangaData.value
if (state == null || manga == null) { if (state == null || manga == null) {
@@ -108,7 +115,6 @@ class ReaderViewModel @AssistedInject constructor(
init { init {
loadImpl() loadImpl()
subscribeToSettings()
} }
override fun onCleared() { override fun onCleared() {
@@ -124,7 +130,7 @@ class ReaderViewModel @AssistedInject constructor(
fun switchMode(newMode: ReaderMode) { fun switchMode(newMode: ReaderMode) {
launchJob { launchJob {
val manga = checkNotNull(mangaData.value) val manga = checkNotNull(mangaData.value)
dataRepository.savePreferences( dataRepository.saveReaderMode(
manga = manga, manga = manga,
mode = newMode, mode = newMode,
) )
@@ -300,13 +306,6 @@ class ReaderViewModel @AssistedInject constructor(
} }
} }
private fun subscribeToSettings() {
settings.observe()
.onEach { key ->
if (key == AppSettings.KEY_ZOOM_MODE) onZoomChanged.postCall(Unit)
}.launchIn(viewModelScope + Dispatchers.Default)
}
private fun <T> List<T>.trySublist(fromIndex: Int, toIndex: Int): List<T> { private fun <T> List<T>.trySublist(fromIndex: Int, toIndex: Int): List<T> {
val fromIndexBounded = fromIndex.coerceAtMost(lastIndex) val fromIndexBounded = fromIndex.coerceAtMost(lastIndex)
val toIndexBounded = toIndex.coerceIn(fromIndexBounded, lastIndex) val toIndexBounded = toIndex.coerceIn(fromIndexBounded, lastIndex)
@@ -331,7 +330,7 @@ class ReaderViewModel @AssistedInject constructor(
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages) val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess { }.onSuccess {
dataRepository.savePreferences(manga, it) dataRepository.saveReaderMode(manga, it)
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
}.getOrDefault(defaultMode) }.getOrDefault(defaultMode)

View File

@@ -2,40 +2,54 @@ package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.content.res.Resources
import android.graphics.LightingColorFilter
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Scale import coil.size.Scale
import coil.size.ViewSizeResolver import coil.size.ViewSizeResolver
import com.google.android.material.R as materialR
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.referer
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import org.koitharu.kotatsu.R
import com.google.android.material.R as materialR import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.referer
@AndroidEntryPoint @AndroidEntryPoint
class ColorFilterConfigActivity : BaseActivity<ActivityColorFilterBinding>(), Slider.OnChangeListener { class ColorFilterConfigActivity :
BaseActivity<ActivityColorFilterBinding>(),
Slider.OnChangeListener,
View.OnClickListener {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject @Inject
lateinit var mangaRepositoryFacotry: MangaRepository.Factory lateinit var viewModelFactory: ColorFilterConfigViewModel.Factory
private val viewModel: ColorFilterConfigViewModel by assistedViewModels {
viewModelFactory.create(
manga = checkNotNull(intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga),
page = checkNotNull(intent.getParcelableExtraCompat<ParcelableMangaPages>(EXTRA_PAGES)?.pages?.firstOrNull()),
)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -44,14 +58,38 @@ class ColorFilterConfigActivity : BaseActivity<ActivityColorFilterBinding>(), Sl
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
} }
binding.sliderLightness.addOnChangeListener(this) binding.sliderBrightness.addOnChangeListener(this)
binding.sliderSaturation.addOnChangeListener(this) binding.sliderContrast.addOnChangeListener(this)
initPreview() val formatter = PercentLabelFormatter(resources)
updateFilter() binding.sliderContrast.setLabelFormatter(formatter)
binding.sliderBrightness.setLabelFormatter(formatter)
binding.buttonDone.setOnClickListener(this)
binding.buttonReset.setOnClickListener(this)
onBackPressedDispatcher.addCallback(ColorFilterConfigBackPressedDispatcher(this, viewModel))
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
viewModel.isLoading.observe(this, this::onLoadingChanged)
viewModel.preview.observe(this, this::onPreviewChanged)
viewModel.onDismiss.observe(this) {
finishAfterTransition()
}
} }
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
updateFilter() if (fromUser) {
when (slider.id) {
R.id.slider_brightness -> viewModel.setBrightness(value)
R.id.slider_contrast -> viewModel.setContrast(value)
}
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> viewModel.save()
R.id.button_reset -> viewModel.reset()
}
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
@@ -67,42 +105,49 @@ class ColorFilterConfigActivity : BaseActivity<ActivityColorFilterBinding>(), Sl
} }
} }
private fun updateFilter() { private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) {
binding.sliderBrightness.value = readerColorFilter?.brightness ?: 0f
fun Int.toColor() = Color.rgb(this, this, this) binding.sliderContrast.value = readerColorFilter?.contrast ?: 0f
binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
val cf = LightingColorFilter(
binding.sliderSaturation.value.roundToInt().toColor(),
binding.sliderLightness.value.roundToInt().toColor(),
)
binding.imageViewAfter.colorFilter = cf
} }
private fun initPreview() { private fun onPreviewChanged(preview: MangaPage?) {
val page = intent?.getParcelableExtra<ParcelableMangaPages>(EXTRA_PAGES)?.pages?.firstOrNull() if (preview == null) return
if (page == null) { ImageRequest.Builder(this@ColorFilterConfigActivity)
finishAfterTransition() .data(preview.url)
return .referer(preview.referer)
} .scale(Scale.FILL)
lifecycleScope.launch { .error(R.drawable.ic_error_placeholder)
val repository = mangaRepositoryFacotry.create(page.source) .size(ViewSizeResolver(binding.imageViewBefore))
val url = repository.getPageUrl(page) .allowRgb565(false)
ImageRequest.Builder(this@ColorFilterConfigActivity) .target(ShadowViewTarget(binding.imageViewBefore, binding.imageViewAfter))
.data(url) .enqueueWith(coil)
.referer(page.referer) }
.scale(Scale.FILL)
.size(ViewSizeResolver(binding.imageViewBefore)) private fun onLoadingChanged(isLoading: Boolean) {
.allowRgb565(false) binding.sliderContrast.isEnabled = !isLoading
.target(ShadowViewTarget(binding.imageViewBefore, binding.imageViewAfter)) binding.sliderBrightness.isEnabled = !isLoading
.enqueueWith(coil) binding.buttonDone.isEnabled = !isLoading
}
private class PercentLabelFormatter(resources: Resources) : LabelFormatter {
private val pattern = resources.getString(R.string.percent_string_pattern)
override fun getFormattedValue(value: Float): String {
val percent = ((value + 1f) * 100).format(0)
return pattern.format(percent)
} }
} }
companion object { companion object {
private const val EXTRA_PAGES = "pages" private const val EXTRA_PAGES = "pages"
private const val EXTRA_MANGA = "manga_id"
fun newIntent(context: Context, page: MangaPage) = Intent(context, ColorFilterConfigActivity::class.java) fun newIntent(context: Context, manga: Manga, page: MangaPage) =
.putExtra(EXTRA_PAGES, ParcelableMangaPages(listOf(page))) Intent(context, ColorFilterConfigActivity::class.java)
.putExtra(EXTRA_MANGA, ParcelableManga(manga, false))
.putExtra(EXTRA_PAGES, ParcelableMangaPages(listOf(page)))
} }
} }

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.Context
import android.content.DialogInterface
import androidx.activity.OnBackPressedCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
class ColorFilterConfigBackPressedDispatcher(
private val context: Context,
private val viewModel: ColorFilterConfigViewModel,
) : OnBackPressedCallback(true), DialogInterface.OnClickListener {
override fun handleOnBackPressed() {
if (viewModel.isChanged) {
showConfirmation()
} else {
viewModel.onDismiss.call(Unit)
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (which) {
DialogInterface.BUTTON_NEGATIVE -> viewModel.onDismiss.call(Unit)
DialogInterface.BUTTON_NEUTRAL -> dialog.dismiss()
DialogInterface.BUTTON_POSITIVE -> viewModel.save()
}
}
private fun showConfirmation() {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.color_correction)
.setMessage(R.string.text_unsaved_changes_prompt)
.setNegativeButton(R.string.discard, this)
.setNeutralButton(android.R.string.cancel, this)
.setPositiveButton(R.string.save, this)
.show()
}
}

View File

@@ -0,0 +1,74 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import androidx.lifecycle.MutableLiveData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.utils.SingleLiveEvent
class ColorFilterConfigViewModel @AssistedInject constructor(
@Assisted private val manga: Manga,
@Assisted page: MangaPage,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository,
) : BaseViewModel() {
private var initialColorFilter: ReaderColorFilter? = null
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
val onDismiss = SingleLiveEvent<Unit>()
val preview = MutableLiveData<MangaPage?>(null)
val isChanged: Boolean
get() = colorFilter.value != initialColorFilter
init {
launchLoadingJob {
initialColorFilter = mangaDataRepository.getColorFilter(manga.id)
colorFilter.value = initialColorFilter
}
launchLoadingJob {
val repository = mangaRepositoryFactory.create(page.source)
val url = repository.getPageUrl(page)
preview.value = MangaPage(
id = page.id,
url = url,
referer = page.referer,
preview = page.preview,
source = page.source,
)
}
}
fun setBrightness(brightness: Float) {
val cf = colorFilter.value
colorFilter.value = ReaderColorFilter(brightness, cf?.contrast ?: 0f).takeUnless { it.isEmpty }
}
fun setContrast(contrast: Float) {
val cf = colorFilter.value
colorFilter.value = ReaderColorFilter(cf?.brightness ?: 0f, contrast).takeUnless { it.isEmpty }
}
fun reset() {
colorFilter.value = null
}
fun save() {
launchLoadingJob {
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
onDismiss.call(Unit)
}
}
@AssistedFactory
interface Factory {
fun create(manga: Manga, page: MangaPage): ColorFilterConfigViewModel
}
}

View File

@@ -84,7 +84,8 @@ class ReaderConfigBottomSheet :
} }
R.id.button_color_filter -> { R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return val page = viewModel.getCurrentPage() ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, page)) val manga = viewModel.manga ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
} }
} }
} }

View File

@@ -0,0 +1,68 @@
package org.koitharu.kotatsu.reader.ui.config
import android.content.SharedPreferences
import androidx.lifecycle.MediatorLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
class ReaderSettings(
private val parentScope: CoroutineScope,
private val settings: AppSettings,
private val colorFilterFlow: StateFlow<ReaderColorFilter?>,
) : MediatorLiveData<ReaderSettings>() {
private val internalObserver = InternalObserver()
private var collectJob: Job? = null
val zoomMode: ZoomMode
get() = settings.zoomMode
val colorFilter: ReaderColorFilter?
get() = colorFilterFlow.value
val isPagesNumbersEnabled: Boolean
get() = settings.isPagesNumbersEnabled
override fun onInactive() {
super.onInactive()
settings.unsubscribe(internalObserver)
collectJob?.cancel()
collectJob = null
}
override fun onActive() {
super.onActive()
settings.subscribe(internalObserver)
collectJob?.cancel()
collectJob = parentScope.launch {
colorFilterFlow.collect(internalObserver)
}
}
override fun getValue() = this
private fun notifyChanged() {
value = value
}
private inner class InternalObserver :
FlowCollector<ReaderColorFilter?>,
SharedPreferences.OnSharedPreferenceChangeListener {
override suspend fun emit(value: ReaderColorFilter?) {
withContext(Dispatchers.Main.immediate) {
notifyChanged()
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == AppSettings.KEY_ZOOM_MODE || key == AppSettings.KEY_PAGES_NUMBERS) {
notifyChanged()
}
}
}
}

View File

@@ -5,15 +5,15 @@ import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
abstract class BasePageHolder<B : ViewBinding>( abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B, protected val binding: B,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback { ) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis") @Suppress("LeakingThis")
@@ -37,8 +37,16 @@ abstract class BasePageHolder<B : ViewBinding>(
protected abstract fun onBind(data: ReaderPage) protected abstract fun onBind(data: ReaderPage)
fun onAttachedToWindow() {
delegate.onAttachedToWindow()
}
fun onDetachedFromWindow() {
delegate.onDetachedFromWindow()
}
@CallSuper @CallSuper
open fun onRecycled() { open fun onRecycled() {
delegate.onRecycle() delegate.onRecycle()
} }
} }

View File

@@ -7,14 +7,14 @@ import androidx.recyclerview.widget.RecyclerView
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.utils.ext.resetTransformations import org.koitharu.kotatsu.utils.ext.resetTransformations
@Suppress("LeakingThis") @Suppress("LeakingThis")
abstract class BaseReaderAdapter<H : BasePageHolder<*>>( abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
private val loader: PageLoader, private val loader: PageLoader,
private val settings: AppSettings, private val readerSettings: ReaderSettings,
private val exceptionResolver: ExceptionResolver, private val exceptionResolver: ExceptionResolver,
) : RecyclerView.Adapter<H>() { ) : RecyclerView.Adapter<H>() {
@@ -35,6 +35,16 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
super.onViewRecycled(holder) super.onViewRecycled(holder)
} }
override fun onViewAttachedToWindow(holder: H) {
super.onViewAttachedToWindow(holder)
holder.onAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: H) {
holder.onDetachedFromWindow()
super.onViewDetachedFromWindow(holder)
}
open fun getItem(position: Int): ReaderPage = differ.currentList[position] open fun getItem(position: Int): ReaderPage = differ.currentList[position]
open fun getItemOrNull(position: Int) = differ.currentList.getOrNull(position) open fun getItemOrNull(position: Int) = differ.currentList.getOrNull(position)
@@ -46,7 +56,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
final override fun onCreateViewHolder( final override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int, viewType: Int,
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver) ): H = onCreateViewHolder(parent, loader, readerSettings, exceptionResolver)
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont -> suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
differ.submitList(items) { differ.submitList(items) {
@@ -57,7 +67,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
protected abstract fun onCreateViewHolder( protected abstract fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver, exceptionResolver: ExceptionResolver,
): H ): H

View File

@@ -2,26 +2,26 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Observer
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.io.File
import java.io.IOException
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import java.io.IOException
class PageHolderDelegate( class PageHolderDelegate(
private val loader: PageLoader, private val loader: PageLoader,
private val settings: AppSettings, private val readerSettings: ReaderSettings,
private val callback: Callback, private val callback: Callback,
private val exceptionResolver: ExceptionResolver private val exceptionResolver: ExceptionResolver,
) : SubsamplingScaleImageView.DefaultOnImageEventListener() { ) : SubsamplingScaleImageView.DefaultOnImageEventListener(), Observer<ReaderSettings> {
private val scope = loader.loaderScope + Dispatchers.Main.immediate private val scope = loader.loaderScope + Dispatchers.Main.immediate
private var state = State.EMPTY private var state = State.EMPTY
@@ -49,6 +49,14 @@ class PageHolderDelegate(
} }
} }
fun onAttachedToWindow() {
readerSettings.observeForever(this)
}
fun onDetachedFromWindow() {
readerSettings.removeObserver(this)
}
fun onRecycle() { fun onRecycle() {
state = State.EMPTY state = State.EMPTY
file = null file = null
@@ -59,7 +67,7 @@ class PageHolderDelegate(
override fun onReady() { override fun onReady() {
state = State.SHOWING state = State.SHOWING
error = null error = null
callback.onImageShowing(settings.zoomMode) callback.onImageShowing(readerSettings)
} }
override fun onImageLoaded() { override fun onImageLoaded() {
@@ -79,6 +87,12 @@ class PageHolderDelegate(
} }
} }
override fun onChanged(t: ReaderSettings?) {
if (state == State.SHOWN) {
callback.onImageShowing(readerSettings)
}
}
private fun tryConvert(file: File, e: Exception) { private fun tryConvert(file: File, e: Exception) {
val prevJob = job val prevJob = job
job = scope.launch { job = scope.launch {
@@ -134,10 +148,10 @@ class PageHolderDelegate(
fun onImageReady(uri: Uri) fun onImageReady(uri: Uri)
fun onImageShowing(zoom: ZoomMode) fun onImageShowing(settings: ReaderSettings)
fun onImageShown() fun onImageShown()
fun onProgressChanged(progress: Int) fun onProgressChanged(progress: Int)
} }
} }

View File

@@ -6,16 +6,16 @@ import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
class ReversedPageHolder( class ReversedPageHolder(
binding: ItemPageBinding, binding: ItemPageBinding,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) : PageHolder(binding, loader, settings, exceptionResolver) { ) : PageHolder(binding, loader, settings, exceptionResolver) {
init { init {
@@ -23,13 +23,14 @@ class ReversedPageHolder(
.gravity = Gravity.START or Gravity.BOTTOM .gravity = Gravity.START or Gravity.BOTTOM
} }
override fun onImageShowing(zoom: ZoomMode) { override fun onImageShowing(settings: ReaderSettings) {
with(binding.ssiv) { with(binding.ssiv) {
maxScale = 2f * maxOf( maxScale = 2f * maxOf(
width / sWidth.toFloat(), width / sWidth.toFloat(),
height / sHeight.toFloat() height / sHeight.toFloat(),
) )
when (zoom) { binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
when (settings.zoomMode) {
ZoomMode.FIT_CENTER -> { ZoomMode.FIT_CENTER -> {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE) setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
resetScaleAndCenter() resetScaleAndCenter()
@@ -39,7 +40,7 @@ class ReversedPageHolder(
minScale = height / sHeight.toFloat() minScale = height / sHeight.toFloat()
setScaleAndCenter( setScaleAndCenter(
minScale, minScale,
PointF(sWidth.toFloat(), sHeight / 2f) PointF(sWidth.toFloat(), sHeight / 2f),
) )
} }
ZoomMode.FIT_WIDTH -> { ZoomMode.FIT_WIDTH -> {
@@ -47,17 +48,17 @@ class ReversedPageHolder(
minScale = width / sWidth.toFloat() minScale = width / sWidth.toFloat()
setScaleAndCenter( setScaleAndCenter(
minScale, minScale,
PointF(sWidth / 2f, 0f) PointF(sWidth / 2f, 0f),
) )
} }
ZoomMode.KEEP_START -> { ZoomMode.KEEP_START -> {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE) setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
setScaleAndCenter( setScaleAndCenter(
maxScale, maxScale,
PointF(sWidth.toFloat(), 0f) PointF(sWidth.toFloat(), 0f),
) )
} }
} }
} }
} }
} }

View File

@@ -3,26 +3,26 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class ReversedPagesAdapter( class ReversedPagesAdapter(
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) { ) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) = ReversedPageHolder( ) = ReversedPageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false), binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader, loader = loader,
settings = settings, settings = settings,
exceptionResolver = exceptionResolver exceptionResolver = exceptionResolver,
) )
} }

View File

@@ -7,10 +7,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlinx.coroutines.async import kotlinx.coroutines.async
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -25,9 +23,6 @@ import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@AndroidEntryPoint @AndroidEntryPoint
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var settings: AppSettings
private var pagerAdapter: ReversedPagesAdapter? = null private var pagerAdapter: ReversedPagesAdapter? = null
override fun onInflateView( override fun onInflateView(
@@ -38,7 +33,7 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, settings, exceptionResolver) pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
with(binding.pager) { with(binding.pager) {
adapter = pagerAdapter adapter = pagerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
@@ -54,9 +49,6 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
} }
} }
} }
viewModel.onZoomChanged.observe(viewLifecycleOwner) {
pagerAdapter?.notifyDataSetChanged()
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@@ -12,9 +12,9 @@ import kotlinx.coroutines.asExecutor
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@@ -22,7 +22,7 @@ import org.koitharu.kotatsu.utils.ext.*
open class PageHolder( open class PageHolder(
binding: ItemPageBinding, binding: ItemPageBinding,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver, exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver), ) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver),
View.OnClickListener { View.OnClickListener {
@@ -66,12 +66,13 @@ open class PageHolder(
binding.ssiv.setImage(ImageSource.uri(uri)) binding.ssiv.setImage(ImageSource.uri(uri))
} }
override fun onImageShowing(zoom: ZoomMode) { override fun onImageShowing(settings: ReaderSettings) {
binding.ssiv.maxScale = 2f * maxOf( binding.ssiv.maxScale = 2f * maxOf(
binding.ssiv.width / binding.ssiv.sWidth.toFloat(), binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
binding.ssiv.height / binding.ssiv.sHeight.toFloat(), binding.ssiv.height / binding.ssiv.sHeight.toFloat(),
) )
when (zoom) { binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
when (settings.zoomMode) {
ZoomMode.FIT_CENTER -> { ZoomMode.FIT_CENTER -> {
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE) binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
binding.ssiv.resetScaleAndCenter() binding.ssiv.resetScaleAndCenter()

View File

@@ -7,10 +7,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlinx.coroutines.async import kotlinx.coroutines.async
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -24,9 +22,6 @@ import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@AndroidEntryPoint @AndroidEntryPoint
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var settings: AppSettings
private var pagesAdapter: PagesAdapter? = null private var pagesAdapter: PagesAdapter? = null
override fun onInflateView( override fun onInflateView(
@@ -37,7 +32,7 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pagesAdapter = PagesAdapter(viewModel.pageLoader, settings, exceptionResolver) pagesAdapter = PagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
with(binding.pager) { with(binding.pager) {
adapter = pagesAdapter adapter = pagesAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
@@ -53,9 +48,6 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
} }
} }
} }
viewModel.onZoomChanged.observe(viewLifecycleOwner) {
pagesAdapter?.notifyDataSetChanged()
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@@ -3,26 +3,26 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter( class PagesAdapter(
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) { ) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) = PageHolder( ) = PageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false), binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader, loader = loader,
settings = settings, settings = settings,
exceptionResolver = exceptionResolver exceptionResolver = exceptionResolver,
) )
} }

View File

@@ -6,27 +6,28 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter( class WebtoonAdapter(
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) { ) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver exceptionResolver: ExceptionResolver,
) = WebtoonHolder( ) = WebtoonHolder(
binding = ItemPageWebtoonBinding.inflate( binding = ItemPageWebtoonBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false,
), ),
loader = loader, loader = loader,
settings = settings, settings = settings,
exceptionResolver = exceptionResolver exceptionResolver = exceptionResolver,
) )
} }

View File

@@ -7,10 +7,9 @@ import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
@@ -19,7 +18,7 @@ import org.koitharu.kotatsu.utils.ext.*
class WebtoonHolder( class WebtoonHolder(
binding: ItemPageWebtoonBinding, binding: ItemPageWebtoonBinding,
loader: PageLoader, loader: PageLoader,
settings: AppSettings, settings: ReaderSettings,
exceptionResolver: ExceptionResolver, exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver), ) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
View.OnClickListener { View.OnClickListener {
@@ -60,7 +59,8 @@ class WebtoonHolder(
binding.ssiv.setImage(ImageSource.uri(uri)) binding.ssiv.setImage(ImageSource.uri(uri))
} }
override fun onImageShowing(zoom: ZoomMode) { override fun onImageShowing(settings: ReaderSettings) {
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
with(binding.ssiv) { with(binding.ssiv) {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM) setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
minScale = width / sWidth.toFloat() minScale = width / sWidth.toFloat()
@@ -70,7 +70,7 @@ class WebtoonHolder(
scrollToRestore != 0 -> scrollToRestore scrollToRestore != 0 -> scrollToRestore
itemView.top < 0 -> getScrollRange() itemView.top < 0 -> getScrollRange()
else -> 0 else -> 0
} },
) )
scrollToRestore = 0 scrollToRestore = 0
} }
@@ -89,7 +89,7 @@ class WebtoonHolder(
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
bindingInfo.textViewError.text = e.getDisplayMessage(context.resources) bindingInfo.textViewError.text = e.getDisplayMessage(context.resources)
bindingInfo.buttonRetry.setText( bindingInfo.buttonRetry.setText(
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again } ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
) )
bindingInfo.layoutError.isVisible = true bindingInfo.layoutError.isVisible = true
bindingInfo.progressBar.hideCompat() bindingInfo.progressBar.hideCompat()
@@ -104,4 +104,4 @@ class WebtoonHolder(
scrollToRestore = scroll scrollToRestore = scroll
} }
} }
} }

View File

@@ -6,9 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.async import kotlinx.coroutines.async
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -21,9 +19,6 @@ import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@AndroidEntryPoint @AndroidEntryPoint
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() { class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
@Inject
lateinit var settings: AppSettings
private val scrollInterpolator = AccelerateDecelerateInterpolator() private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null private var webtoonAdapter: WebtoonAdapter? = null
@@ -34,7 +29,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, settings, exceptionResolver) webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = webtoonAdapter adapter = webtoonAdapter

View File

@@ -12,7 +12,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
tools:navigationIcon="@drawable/abc_ic_clear_material" tools:navigationIcon="@drawable/abc_ic_clear_material"
tools:title="@string/color_filter"> tools:title="@string/color_correction">
<Button <Button
android:id="@+id/button_done" android:id="@+id/button_done"
@@ -42,6 +42,7 @@
android:id="@+id/imageView_before" android:id="@+id/imageView_before"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:padding="2dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="W,14:9" app:layout_constraintDimensionRatio="W,14:9"
app:layout_constraintEnd_toStartOf="@id/imageView_arrow" app:layout_constraintEnd_toStartOf="@id/imageView_arrow"
@@ -56,6 +57,7 @@
android:id="@+id/imageView_arrow" android:id="@+id/imageView_arrow"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null"
android:padding="@dimen/margin_normal" android:padding="@dimen/margin_normal"
android:src="@drawable/ic_arrow_forward" android:src="@drawable/ic_arrow_forward"
app:layout_constraintBottom_toBottomOf="@id/imageView_before" app:layout_constraintBottom_toBottomOf="@id/imageView_before"
@@ -67,6 +69,7 @@
android:id="@+id/imageView_after" android:id="@+id/imageView_after"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:padding="2dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="W,14:9" app:layout_constraintDimensionRatio="W,14:9"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -77,29 +80,72 @@
app:strokeWidth="1dp" app:strokeWidth="1dp"
tools:src="@sample/covers" /> tools:src="@sample/covers" />
<com.google.android.material.slider.Slider <TextView
android:id="@+id/slider_saturation" android:id="@+id/textView_brightness"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_normal"
android:value="255.0" android:text="@string/brightness"
android:valueFrom="0.0" android:textAppearance="?textAppearanceTitleMedium"
android:valueTo="255.0"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView_before" /> app:layout_constraintTop_toBottomOf="@id/imageView_before" />
<com.google.android.material.slider.Slider <com.google.android.material.slider.Slider
android:id="@+id/slider_lightness" android:id="@+id/slider_brightness"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_small"
android:labelFor="@id/textView_brightness"
android:value="0.0" android:value="0.0"
android:valueFrom="0.0" android:valueFrom="-1.0"
android:valueTo="255.0" android:valueTo="1.0"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_saturation" /> app:layout_constraintTop_toBottomOf="@id/textView_brightness" />
<TextView
android:id="@+id/textView_contrast"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/contrast"
android:textAppearance="?textAppearanceTitleMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_brightness" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_contrast"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:value="0.0"
android:valueFrom="-1.0"
android:valueTo="1.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_contrast" />
<Button
android:id="@+id/button_reset"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:text="@string/reset"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/slider_contrast" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_normal"
android:text="@string/color_correction_hint"
android:textAppearance="?textAppearanceBodySmall"
app:layout_constraintEnd_toStartOf="@id/button_reset"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/button_reset" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -155,7 +155,7 @@
android:drawablePadding="?android:listPreferredItemPaddingStart" android:drawablePadding="?android:listPreferredItemPaddingStart"
android:paddingStart="?android:listPreferredItemPaddingStart" android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd" android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/color_filter" android:text="@string/color_correction"
android:textAppearance="?attr/textAppearanceButton" android:textAppearance="?attr/textAppearanceButton"
app:drawableStartCompat="@drawable/ic_appearance" /> app:drawableStartCompat="@drawable/ic_appearance" />

View File

@@ -381,5 +381,11 @@
<string name="history_shortcuts_summary">Make recent manga available by long pressing on application icon</string> <string name="history_shortcuts_summary">Make recent manga available by long pressing on application icon</string>
<string name="reader_control_ltr_summary">Tap on the right edge or pressing the right key always switches to the next page</string> <string name="reader_control_ltr_summary">Tap on the right edge or pressing the right key always switches to the next page</string>
<string name="reader_control_ltr">Ergonomic reader control</string> <string name="reader_control_ltr">Ergonomic reader control</string>
<string name="color_filter">Color filter</string> <string name="color_correction">Color correction</string>
<string name="brightness">Brightness</string>
<string name="contrast">Contrast</string>
<string name="reset">Reset</string>
<string name="color_correction_hint">The chosen color settings will be remembered for this manga</string>
<string name="text_unsaved_changes_prompt">You have unsaved changes, do you want to save or discard them?</string>
<string name="discard">Discard</string>
</resources> </resources>