Compare commits

...

24 Commits

Author SHA1 Message Date
6243fc88c9 Sorting local manga #219 #217 2022-09-03 15:45:06 +03:00
Neko Nekowazarashi
f74e865b06 Translated using Weblate (Indonesian)
Currently translated at 91.7% (356 of 388 strings)

Co-authored-by: Neko Nekowazarashi <kodra@nekoweb.my.id>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
no
1a2bc02188 Translated using Weblate (Turkish)
Currently translated at 88.9% (345 of 388 strings)

Co-authored-by: no <cbasroyalasroyal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
b0153e9f61 Translated using Weblate (Russian)
Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Zakhar Timoshenko <vp1984tanki@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
J. Lavoie
7f3fc1b88a Translated using Weblate (French)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
Zero O
40bf0c75bf Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Added translation using Weblate (Chinese (Simplified))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Zero O <godarms2010@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-09-03 13:45:59 +03:00
Dpper
9407bd205c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
kuragehime
3317c8dddc Translated using Weblate (Japanese)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
fc4b6eb1af Respect display cutout in reader bar 2022-08-27 18:17:01 +03:00
2aaaf2f4a2 Fix pages thumbnails for webtoons 2022-08-27 17:55:34 +03:00
92aa96a644 Color filter support in reader 2022-08-27 16:37:46 +03:00
6a0a4023ad Merge branch 'devel' into feature/colorfilter 2022-08-27 13:02:05 +03:00
8569610e52 Fix BottomSheet blinking 2022-08-27 12:47:42 +03:00
51ffa4d469 Merge branch 'master' into devel 2022-08-27 10:55:08 +03:00
e4c4d2bbf0 Fix crashes 2022-08-27 09:38:54 +03:00
35d6f1fb34 Local list sort order (wip) #217 2022-08-26 13:21:25 +03:00
edac5fda8c Reader control direction depends on mode #214 2022-08-26 12:27:57 +03:00
040d3e4433 Reader control direction depends on mode #214 2022-08-26 12:13:18 +03:00
011dd4c069 Option to disable dynamic shortcuts 2022-08-26 10:52:09 +03:00
b4f93fc0a5 Fix showing reader control by long press 2022-08-26 10:09:48 +03:00
e0d74ba2a9 Fix showing reader control by long press 2022-08-26 10:07:07 +03:00
935826617e Upgrade targetSdk to 33 2022-08-24 11:14:22 +03:00
b9f2effb86 Fix some crashes 2022-08-24 09:13:36 +03:00
d853bb2c62 Pages color filter implementation draft 2022-08-13 16:27:53 +03:00
88 changed files with 1824 additions and 320 deletions

View File

@@ -7,16 +7,16 @@ plugins {
}
android {
compileSdkVersion 32
buildToolsVersion '32.0.0'
compileSdkVersion 33
buildToolsVersion '33.0.0'
namespace 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 32
versionCode 491
versionName '4.0-a2'
targetSdkVersion 33
versionCode 495
versionName '4.0-a6'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -82,7 +82,7 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') {
implementation('com.github.KotatsuApp:kotatsu-parsers:551a1d70ae') {
exclude group: 'org.json', module: 'json'
}
@@ -102,7 +102,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
implementation 'com.google.android.material:material:1.7.0-beta01'
implementation 'com.google.android.material:material:1.7.0-rc01'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
@@ -117,19 +117,18 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-compiler:2.42"
implementation "com.google.dagger:hilt-android:2.43.2"
kapt "com.google.dagger:hilt-compiler:2.43.2"
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.2.0'
implementation 'io.coil-kt:coil-svg:2.1.0'
// implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'io.coil-kt:coil-svg:2.2.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2942b797a2'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'ch.acra:acra-http:5.9.5'
implementation 'ch.acra:acra-dialog:5.9.5'
implementation 'ch.acra:acra-http:5.9.6'
implementation 'ch.acra:acra-dialog:5.9.6'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
@@ -147,6 +146,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.4.3'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2'
}

View File

@@ -10,4 +10,7 @@
}
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment

View File

@@ -17,6 +17,7 @@
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name="org.koitharu.kotatsu.KotatsuApp"
@@ -132,11 +133,14 @@
<activity
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
android:label="@string/sync" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
android:label="@string/color_correction" />
<service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
android:stopWithTask="false"
android:foregroundServiceType="dataSync" />
android:foregroundServiceType="dataSync"
android:stopWithTask="false" />
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
<service android:name="org.koitharu.kotatsu.local.ui.ImportService" />
<service

View File

@@ -10,6 +10,9 @@ import java.util.zip.ZipFile
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
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.MangaTag
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
private const val MIN_WEBTOON_RATIO = 2
@@ -31,15 +35,22 @@ class MangaDataRepository @Inject constructor(
private val db: MangaDatabase,
) {
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
val tags = manga.tags.toEntities()
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags)
storeManga(manga)
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(
MangaPrefsEntity(
mangaId = manga.id,
mode = mode.id,
entity.copy(
cfBrightness = colorFilter?.brightness ?: 0f,
cfContrast = colorFilter?.contrast ?: 0f,
),
)
}
@@ -49,6 +60,16 @@ class MangaDataRepository @Inject constructor(
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? {
return db.mangaDao.find(mangaId)?.toManga()
}
@@ -71,6 +92,14 @@ class MangaDataRepository @Inject constructor(
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
* @return ReaderMode.WEBTOON if page is wide
@@ -104,6 +133,13 @@ class MangaDataRepository @Inject constructor(
return size.width * MIN_WEBTOON_RATIO < size.height
}
private fun newEntity(mangaId: Long) = MangaPrefsEntity(
mangaId = mangaId,
mode = -1,
cfBrightness = 0f,
cfContrast = 0f,
)
companion object {
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {

View File

@@ -5,6 +5,8 @@ import android.net.Uri
import android.os.Bundle
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
class MangaIntent private constructor(
val manga: Manga?,
@@ -13,15 +15,15 @@ class MangaIntent private constructor(
) {
constructor(intent: Intent?) : this(
manga = intent?.getParcelableExtra<ParcelableManga>(KEY_MANGA)?.manga,
manga = intent?.getParcelableExtraCompat<ParcelableManga>(KEY_MANGA)?.manga,
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
uri = intent?.data,
)
constructor(args: Bundle?) : this(
manga = args?.getParcelable<ParcelableManga>(KEY_MANGA)?.manga,
manga = args?.getParcelableCompat<ParcelableManga>(KEY_MANGA)?.manga,
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
uri = null,
)
companion object {

View File

@@ -8,16 +8,18 @@ import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import kotlin.math.hypot
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.measureWidth
import kotlin.math.hypot
class BubbleAnimator(
private val bubble: View,
) {
private val animationDuration = (bubble.resources.getInteger(android.R.integer.config_shortAnimTime) *
bubble.context.animatorDurationScale).toLong()
private val animationDuration = (
bubble.resources.getInteger(android.R.integer.config_shortAnimTime) *
bubble.context.animatorDurationScale
).toLong()
private var animator: Animator? = null
private var isHiding = false
@@ -65,12 +67,12 @@ class BubbleAnimator(
private var isCancelled = false
override fun onAnimationCancel(animation: Animator?) {
override fun onAnimationCancel(animation: Animator) {
super.onAnimationCancel(animation)
isCancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@BubbleAnimator.animator) {
bubble.isInvisible = true
@@ -79,4 +81,4 @@ class BubbleAnimator(
}
}
}
}
}

View File

@@ -14,8 +14,10 @@ class ScrollbarAnimator(
private val scrollbarPaddingEnd: Float,
) {
private val animationDuration = (scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) *
scrollbar.context.animatorDurationScale).toLong()
private val animationDuration = (
scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) *
scrollbar.context.animatorDurationScale
).toLong()
private var animator: ViewPropertyAnimator? = null
private var isHiding = false
@@ -40,30 +42,32 @@ class ScrollbarAnimator(
}
animator?.cancel()
isHiding = true
animator = scrollbar
.animate()
.translationX(scrollbarPaddingEnd)
.alpha(0f)
.setDuration(animationDuration)
.setListener(HideListener())
animator = scrollbar.animate().apply {
translationX(scrollbarPaddingEnd)
alpha(0f)
duration = animationDuration
setListener(HideListener(this))
}
}
private inner class HideListener : AnimatorListenerAdapter() {
private inner class HideListener(
private val viewPropertyAnimator: ViewPropertyAnimator,
) : AnimatorListenerAdapter() {
private var isCancelled = false
override fun onAnimationCancel(animation: Animator?) {
override fun onAnimationCancel(animation: Animator) {
super.onAnimationCancel(animation)
isCancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@ScrollbarAnimator.animator) {
if (!isCancelled && this@ScrollbarAnimator.animator === viewPropertyAnimator) {
scrollbar.isInvisible = true
isHiding = false
this@ScrollbarAnimator.animator = null
}
}
}
}
}

View File

@@ -26,6 +26,8 @@ import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import org.koitharu.kotatsu.utils.ext.parents
private const val THROTTLE_DELAY = 200L
class BottomSheetHeaderBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -35,6 +37,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
private val bottomSheetCallback = Callback()
private val adjustStateRunnable = Runnable { adjustState() }
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
private val locationBuffer = IntArray(2)
private val expansionListeners = LinkedList<OnExpansionChangeListener>()
@@ -43,6 +46,8 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
private val maxHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_max)
private var isLayoutSuppressedCompat = false
private var isLayoutCalledWhileSuppressed = false
private var isBsExpanded = false
private var stateAdjustedAt = 0L
@Deprecated("")
val toolbar: MaterialToolbar
@@ -173,16 +178,11 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
private fun onBottomSheetStateChanged(newState: Int) {
val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
if (isExpanded == binding.dragHandle.isGone) {
return
val expanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
if (isBsExpanded != expanded) {
isBsExpanded = expanded
postAdjustState()
}
suppressLayoutCompat(true)
binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null)
binding.dragHandle.isGone = isExpanded
expansionListeners.forEach { it.onExpansionStateChanged(this, isExpanded) }
dispatchInsets(ViewCompat.getRootWindowInsets(this))
suppressLayoutCompat(false)
}
private fun suppressLayoutCompat(suppress: Boolean) {
@@ -262,6 +262,26 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
}
private fun postAdjustState() {
removeCallbacks(adjustStateRunnable)
val now = System.currentTimeMillis()
if (stateAdjustedAt + THROTTLE_DELAY < now) {
adjustState()
} else {
postDelayed(adjustStateRunnable, THROTTLE_DELAY)
}
}
private fun adjustState() {
suppressLayoutCompat(true)
binding.toolbar.navigationIcon = (if (isBsExpanded) closeDrawable else null)
binding.dragHandle.isGone = isBsExpanded
expansionListeners.forEach { it.onExpansionStateChanged(this, isBsExpanded) }
dispatchInsets(ViewCompat.getRootWindowInsets(this))
stateAdjustedAt = System.currentTimeMillis()
suppressLayoutCompat(false)
}
private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener {
override fun onStateChanged(bottomSheet: View, newState: Int) {

View File

@@ -100,7 +100,7 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
.applySystemAnimatorScale(context)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
currentAnimator = null
postInvalidate()
}

View File

@@ -55,6 +55,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
override fun onDestroyView() {
binding.webView.stopLoading()
binding.webView.destroy()
super.onDestroyView()
}
@@ -83,7 +84,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
override fun onCheckPassed() {
pendingResult.putBoolean(EXTRA_RESULT, true)
dismiss()
dismissAllowingStateLoss()
}
companion object {

View File

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

View File

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

View File

@@ -12,12 +12,15 @@ import androidx.room.PrimaryKey
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
onDelete = ForeignKey.CASCADE,
),
],
)
class MangaPrefsEntity(
data class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "mode") val mode: Int
)
@ColumnInfo(name = "manga_id")
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

@@ -2,10 +2,12 @@ package org.koitharu.kotatsu.core.os
import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.media.ThumbnailUtils
import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@@ -22,6 +24,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
@@ -35,13 +38,18 @@ class ShortcutsUpdater @Inject constructor(
private val coil: ImageLoader,
private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository,
) : InvalidationTracker.Observer(TABLE_HISTORY) {
private val settings: AppSettings,
) : InvalidationTracker.Observer(TABLE_HISTORY), SharedPreferences.OnSharedPreferenceChangeListener {
private val iconSize by lazy { getIconSize(context) }
private var shortcutsUpdateJob: Job? = null
override fun onInvalidated(tables: MutableSet<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
init {
settings.subscribe(this)
}
override fun onInvalidated(tables: Set<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || !settings.isDynamicShortcutsEnabled) {
return
}
val prevJob = shortcutsUpdateJob
@@ -51,6 +59,16 @@ class ShortcutsUpdater @Inject constructor(
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && key == AppSettings.KEY_SHORTCUTS) {
if (settings.isDynamicShortcutsEnabled) {
onInvalidated(emptySet())
} else {
clearShortcuts()
}
}
}
suspend fun requestPinShortcut(manga: Manga): Boolean {
return ShortcutManagerCompat.requestPinShortcut(
context,
@@ -64,6 +82,15 @@ class ShortcutsUpdater @Inject constructor(
return shortcutsUpdateJob?.join() != null
}
fun isDynamicShortcutsAvailable(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return false
}
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
return manager.maxShortcutCountPerActivity > 0
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatching {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
@@ -74,6 +101,15 @@ class ShortcutsUpdater @Inject constructor(
it.printStackTraceDebug()
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun clearShortcuts() {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
try {
manager.removeAllDynamicShortcuts()
} catch (_: IllegalStateException) {
}
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching {
val bmp = coil.execute(

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue
@@ -65,6 +66,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val readerPageSwitch: Set<String>
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
val isReaderTapsAdaptive: Boolean
get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
var isTrafficWarningEnabled: Boolean
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
@@ -134,6 +138,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isExitConfirmationEnabled: Boolean
get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false)
val isDynamicShortcutsEnabled: Boolean
get() = prefs.getBoolean(KEY_SHORTCUTS, true)
var sourcesOrder: List<String>
get() = prefs.getString(KEY_SOURCES_ORDER, null)
?.split('|')
@@ -202,6 +209,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val dnsOverHttps: DoHProvider
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
var localListOrder: SortOrder
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
NETWORK_ALWAYS -> true
@@ -317,6 +328,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_INCOGNITO_MODE = "incognito"
const val KEY_SYNC = "sync"
const val KEY_READER_BAR = "reader_bar"
const val KEY_SHORTCUTS = "dynamic_shortcuts"
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -16,6 +16,8 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.databinding.DialogMangaErrorBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.report
import org.koitharu.kotatsu.utils.ext.requireParcelable
import org.koitharu.kotatsu.utils.ext.requireSerializable
import org.koitharu.kotatsu.utils.ext.withArgs
class MangaErrorDialog : AlertDialogFragment<DialogMangaErrorBinding>() {
@@ -26,8 +28,8 @@ class MangaErrorDialog : AlertDialogFragment<DialogMangaErrorBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = requireArguments()
manga = requireNotNull(args.getParcelable<ParcelableManga>(ARG_MANGA)?.manga)
error = args.getSerializable(ARG_ERROR) as Throwable
manga = args.requireParcelable<ParcelableManga>(ARG_MANGA).manga
error = args.requireSerializable(ARG_ERROR)
}
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogMangaErrorBinding {

View File

@@ -35,13 +35,13 @@ class ChaptersMenuProvider(
else -> false
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
bottomSheetMediator?.lock()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
(item?.actionView as? SearchView)?.setQuery("", false)
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
(item.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null)
bottomSheetMediator?.unlock()
return true

View File

@@ -14,6 +14,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.R as materialR
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
@@ -25,6 +26,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getSerializableCompat
@AndroidEntryPoint
class FavouritesCategoryEditActivity :
@@ -46,7 +48,7 @@ class FavouritesCategoryEditActivity :
setContentView(ActivityCategoryEditBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
initSortSpinner()
binding.buttonDone.setOnClickListener(this)
@@ -69,8 +71,8 @@ class FavouritesCategoryEditActivity :
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val order = savedInstanceState.getSerializable(KEY_SORT_ORDER)
if (order != null && order is SortOrder) {
val order = savedInstanceState.getSerializableCompat<SortOrder>(KEY_SORT_ORDER)
if (order != null) {
selectedSortOrder = order
}
}

View File

@@ -56,17 +56,17 @@ class ReadingProgressView @JvmOverloads constructor(
getProgressDrawable().progress = p
}
override fun onAnimationStart(animation: Animator?) = Unit
override fun onAnimationStart(animation: Animator) = Unit
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
if (percentAnimator === animation) {
percentAnimator = null
}
}
override fun onAnimationCancel(animation: Animator?) = Unit
override fun onAnimationCancel(animation: Animator) = Unit
override fun onAnimationRepeat(animation: Animator?) = Unit
override fun onAnimationRepeat(animation: Animator) = Unit
fun setPercent(value: Float, animate: Boolean) {
val currentDrawable = peekProgressDrawable()

View File

@@ -6,6 +6,7 @@ import android.view.View
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.view.updateLayoutParams
import androidx.core.widget.TextViewCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlin.math.roundToInt
@@ -46,12 +47,12 @@ class ItemSizeResolver(resources: Resources, private val settings: AppSettings)
}
}
override fun onViewAttachedToWindow(v: View?) {
override fun onViewAttachedToWindow(v: View) {
settings.subscribe(this)
update()
}
override fun onViewDetachedFromWindow(v: View?) {
override fun onViewDetachedFromWindow(v: View) {
settings.unsubscribe(this)
}
@@ -77,7 +78,7 @@ class ItemSizeResolver(resources: Resources, private val settings: AppSettings)
}
if (textAppearanceResId != prevTextAppearance) {
prevTextAppearance = textAppearanceResId
setTextAppearance(textAppearanceResId)
TextViewCompat.setTextAppearance(this, textAppearanceResId)
requestLayout()
}
}

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
class FilterAdapter(
listener: OnFilterChangedListener,
listListener: ListListener<FilterItem>,
) : AsyncListDifferDelegationAdapter<FilterItem>(
FilterDiffCallback(),
filterSortDelegate(listener),
@@ -11,4 +13,9 @@ class FilterAdapter(
filterHeaderDelegate(),
filterLoadingDelegate(),
filterErrorDelegate(),
)
) {
init {
differ.addListListener(listListener)
}
}

View File

@@ -6,18 +6,21 @@ import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.FragmentManager
import dagger.hilt.android.AndroidEntryPoint
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.LinearLayoutManager
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import org.koitharu.kotatsu.utils.ext.isScrolledToTop
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
class FilterBottomSheet :
BaseBottomSheet<SheetFilterBinding>(),
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
DialogInterface.OnKeyListener {
DialogInterface.OnKeyListener,
AsyncListDiffer.ListListener<FilterItem> {
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
@@ -33,13 +36,13 @@ class FilterBottomSheet :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = FilterAdapter(viewModel)
val adapter = FilterAdapter(viewModel, this)
binding.recyclerView.adapter = adapter
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
initOptionsMenu()
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
return true
}
@@ -71,6 +74,12 @@ class FilterBottomSheet :
return false
}
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
if (currentList.size > previousList.size && view != null) {
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
}
}
private fun initOptionsMenu() {
binding.headerBar.toolbar.inflateMenu(R.menu.opt_filter)
val searchMenuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search)

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.local.domain
import java.io.File
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
class LocalManga(
val manga: Manga,
val file: File,
) {
var createdAt: Long = -1L
get() {
if (field == -1L) {
field = file.lastModified()
}
return field
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LocalManga
if (manga != other.manga) return false
if (file != other.file) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + file.hashCode()
return result
}
}
fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }
fun LocalManga.isMatchesQuery(query: String): Boolean {
return manga.title.contains(query, ignoreCase = true) ||
manga.altTitle?.contains(query, ignoreCase = true) == true
}
fun LocalManga.containsTags(tags: Set<MangaTag>): Boolean {
return manga.tags.containsAll(tags)
}

View File

@@ -45,12 +45,9 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
}
val list = getRawList()
if (query.isNotEmpty()) {
list.retainAll { x ->
x.title.contains(query, ignoreCase = true) ||
x.altTitle?.contains(query, ignoreCase = true) == true
}
list.retainAll { x -> x.isMatchesQuery(query) }
}
return list
return list.unwrap()
}
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
@@ -59,11 +56,17 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
}
val list = getRawList()
if (!tags.isNullOrEmpty()) {
list.retainAll { x ->
x.tags.containsAll(tags)
}
list.retainAll { x -> x.containsTags(tags) }
}
return list
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
when (sortOrder) {
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
SortOrder.RATING -> list.sortByDescending { it.manga.rating }
SortOrder.NEWEST,
SortOrder.UPDATED,
-> list.sortByDescending { it.createdAt }
}
return list.unwrap()
}
override suspend fun getDetails(manga: Manga) = when {
@@ -231,9 +234,9 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
private fun CoroutineScope.getFromFileAsync(
file: File,
context: CoroutineContext,
): Deferred<Manga?> = async(context) {
): Deferred<LocalManga?> = async(context) {
runInterruptible {
runCatching { getFromFile(file) }.getOrNull()
runCatching { LocalManga(getFromFile(file), file) }.getOrNull()
}
}
@@ -250,7 +253,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
}
}
override val sortOrders = setOf(SortOrder.ALPHABETICAL)
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
override suspend fun getPageUrl(page: MangaPage) = page.url
@@ -279,7 +282,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
locks.unlock(id)
}
private suspend fun getRawList(): ArrayList<Manga> {
private suspend fun getRawList(): ArrayList<LocalManga> {
val files = getAllFiles()
return coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)

View File

@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.fragment.app.viewModels
@@ -13,10 +14,11 @@ import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.addMenuProvider
class LocalListFragment : MangaListFragment() {
class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
override val viewModel by viewModels<LocalListViewModel>()
@@ -30,6 +32,14 @@ class LocalListFragment : MangaListFragment() {
ImportDialogFragment.show(childFragmentManager)
}
override fun onFilterClick(view: View?) {
super.onFilterClick(view)
val menu = PopupMenu(requireContext(), view ?: binding.recyclerView)
menu.inflate(R.menu.popup_order)
menu.setOnMenuItemClickListener(this)
menu.show()
}
override fun onScrolledToEnd() = Unit
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
@@ -53,6 +63,17 @@ class LocalListFragment : MangaListFragment() {
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val order = when (item.itemId) {
R.id.action_order_new -> SortOrder.NEWEST
R.id.action_order_abs -> SortOrder.ALPHABETICAL
R.id.action_order_rating -> SortOrder.RATING
else -> return false
}
viewModel.setSortOrder(order)
return true
}
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.delete_manga)

View File

@@ -23,4 +23,4 @@ class LocalListMenuProvider(
else -> false
}
}
}
}

View File

@@ -1,27 +1,31 @@
package org.koitharu.kotatsu.local.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.IOException
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
@@ -30,18 +34,24 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class LocalListViewModel @Inject constructor(
private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
settings: AppSettings,
) : MangaListViewModel(settings) {
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
) : MangaListViewModel(settings), ListExtraProvider {
val onMangaRemoved = SingleLiveEvent<Unit>()
val sortOrder = MutableLiveData(settings.localListOrder)
private val listError = MutableStateFlow<Throwable?>(null)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
private var refreshJob: Job? = null
override val content = combine(
mangaList,
createListModeFlow(),
sortOrder.asFlow(),
selectedTags,
listError,
) { list, mode, error ->
) { list, mode, order, tags, error ->
when {
error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState)
@@ -53,7 +63,10 @@ class LocalListViewModel @Inject constructor(
actionStringRes = R.string._import,
),
)
else -> list.toUi(mode)
else -> buildList(list.size + 1) {
add(createHeader(list, tags, order))
list.toUi(this, mode, this@LocalListViewModel)
}
}
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
@@ -63,14 +76,27 @@ class LocalListViewModel @Inject constructor(
watchDirectories()
}
override fun onUpdateFilter(tags: Set<MangaTag>) {
selectedTags.value = tags
onRefresh()
}
override fun onRefresh() {
launchLoadingJob(Dispatchers.Default) {
val prevJob = refreshJob
refreshJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
doRefresh()
}
}
override fun onRetry() = onRefresh()
fun setSortOrder(value: SortOrder) {
sortOrder.value = value
settings.localListOrder = value
onRefresh()
}
fun delete(ids: Set<Long>) {
launchLoadingJob {
withContext(Dispatchers.Default) {
@@ -93,7 +119,7 @@ class LocalListViewModel @Inject constructor(
private suspend fun doRefresh() {
try {
listError.value = null
mangaList.value = repository.getList(0, null, null)
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
} catch (e: Throwable) {
listError.value = e
}
@@ -119,4 +145,46 @@ class LocalListViewModel @Inject constructor(
}
}
}
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
val tags = HashMap<MangaTag, Int>()
for (item in mangaList) {
for (tag in item.tags) {
tags[tag] = tags[tag]?.plus(1) ?: 1
}
}
val topTags = tags.entries.sortedByDescending { it.value }.take(6)
val chips = LinkedList<ChipsView.ChipModel>()
for ((tag, _) in topTags) {
val model = ChipsView.ChipModel(
icon = 0,
title = tag.title,
isCheckable = true,
isChecked = tag in selectedTags,
data = tag,
)
if (model.isChecked) {
chips.addFirst(model)
} else {
chips.addLast(model)
}
}
return ListHeader2(
chips = chips,
sortOrder = order,
hasSelectedTags = selectedTags.isNotEmpty(),
)
}
override suspend fun getCounter(mangaId: Long): Int {
return trackingRepository.getNewChaptersCount(mangaId)
}
override suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
}

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.main.ui
import android.Manifest
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle
import android.util.SparseIntArray
import android.view.MenuItem
@@ -7,6 +10,7 @@ import android.view.View
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
@@ -24,7 +28,6 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
@@ -291,12 +294,11 @@ class MainActivity :
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
}
requestNotificationsPermission()
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
yield()
// TODO get<SyncController>().requestFullSyncAndGc(get())
}
}
@@ -347,6 +349,15 @@ class MainActivity :
showNav(!isOpened)
}
private fun requestNotificationsPermission() {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
}
}
private inner class VoiceInputCallback : ActivityResultCallback<String?> {
override fun onActivityResult(result: String?) {

View File

@@ -9,13 +9,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@HiltViewModel
@@ -23,13 +25,15 @@ class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
private val appUpdateRepository: AppUpdateRepository,
private val trackingRepository: TrackingRepository,
syncController: SyncController,
database: MangaDatabase,
) : BaseViewModel() {
val onOpenReader = SingleLiveEvent<Manga>()
val isResumeEnabled = historyRepository
.observeHasItems()
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val counters = combine(
appUpdateRepository.observeAvailableUpdate(),
@@ -45,6 +49,9 @@ class MainViewModel @Inject constructor(
launchJob {
appUpdateRepository.fetchUpdate()
}
launchJob {
syncController.requestFullSyncAndGc(database)
}
}
fun openLastReader() {

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import javax.inject.Inject
import javax.inject.Singleton
import org.acra.dialog.CrashReportDialog
import org.koitharu.kotatsu.core.prefs.AppSettings
@Singleton
@@ -14,7 +15,7 @@ class AppProtectHelper @Inject constructor(private val settings: AppSettings) :
private var isUnlocked = settings.appPassword.isNullOrEmpty()
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity !is ProtectActivity && !isUnlocked) {
if (!isUnlocked && activity !is ProtectActivity && activity !is CrashReportDialog) {
val sourceIntent = Intent(activity, activity.javaClass)
activity.intent?.let {
sourceIntent.putExtras(it)

View File

@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityProtectBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
@AndroidEntryPoint
class ProtectActivity :
@@ -44,7 +45,7 @@ class ProtectActivity :
viewModel.onError.observe(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onUnlockSuccess.observe(this) {
val intent = intent.getParcelableExtra<Intent>(EXTRA_INTENT)
val intent = intent.getParcelableExtraCompat<Intent>(EXTRA_INTENT)
startActivity(intent)
finishAfterTransition()
}

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

@@ -6,6 +6,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.roundToInt
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -17,9 +19,8 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemClickListener<ChapterListItem> {
@@ -33,7 +34,7 @@ class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val chapters = arguments?.getParcelable<ParcelableMangaChapters>(ARG_CHAPTERS)?.chapters
val chapters = arguments?.getParcelableCompat<ParcelableMangaChapters>(ARG_CHAPTERS)?.chapters
if (chapters.isNullOrEmpty()) {
dismissAllowingStateLoss()
return

View File

@@ -63,7 +63,7 @@ class ReaderActivity :
val viewModel by assistedViewModels {
viewModelFactory.create(
intent = MangaIntent(intent),
initialState = intent?.getParcelableExtra(EXTRA_STATE),
initialState = intent?.getParcelableExtraCompat(EXTRA_STATE),
preselectedBranch = intent?.getStringExtra(EXTRA_BRANCH),
)
}
@@ -74,6 +74,9 @@ class ReaderActivity :
pageSwitchTimer.delaySec = value
}
override val readerMode: ReaderMode?
get() = readerManager.currentMode
private lateinit var pageSwitchTimer: PageSwitchTimer
private lateinit var touchHelper: GridTouchHelper
private lateinit var controlDelegate: ReaderControlDelegate
@@ -88,7 +91,7 @@ class ReaderActivity :
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
pageSwitchTimer = PageSwitchTimer(this, this)
controlDelegate = ReaderControlDelegate(lifecycleScope, settings, this)
controlDelegate = ReaderControlDelegate(settings, this, this)
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
binding.slider.setLabelFormatter(PageLabelFormatter())
ReaderSliderListener(this, viewModel).attachToSlider(binding.slider)

View File

@@ -1,33 +1,39 @@
package org.koitharu.kotatsu.reader.ui
import android.content.SharedPreferences
import android.view.KeyEvent
import android.view.SoundEffectConstants
import android.view.View
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.utils.GridTouchHelper
class ReaderControlDelegate(
scope: LifecycleCoroutineScope,
settings: AppSettings,
private val settings: AppSettings,
private val listener: OnInteractionListener,
) {
owner: LifecycleOwner,
) : DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener {
private var isTapSwitchEnabled: Boolean = true
private var isVolumeKeysSwitchEnabled: Boolean = false
private var isReaderTapsAdaptive: Boolean = true
init {
settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch }
.flowOn(Dispatchers.Default)
.onEach {
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it
}.launchIn(scope)
owner.lifecycle.addObserver(this)
settings.subscribe(this)
updateSettings()
}
override fun onDestroy(owner: LifecycleOwner) {
settings.unsubscribe(this)
owner.lifecycle.removeObserver(this)
super.onDestroy(owner)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
updateSettings()
}
fun onGridTouch(area: Int, view: View) {
@@ -41,7 +47,7 @@ class ReaderControlDelegate(
view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP)
}
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
listener.switchPageBy(-1)
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT)
}
GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) {
@@ -49,7 +55,7 @@ class ReaderControlDelegate(
view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN)
}
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
listener.switchPageBy(1)
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT)
}
}
@@ -72,19 +78,25 @@ class ReaderControlDelegate(
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_RIGHT,
-> {
listener.switchPageBy(1)
true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
true
}
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_LEFT,
-> {
listener.switchPageBy(-1)
true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
true
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
listener.toggleUiVisibility()
true
@@ -99,8 +111,21 @@ class ReaderControlDelegate(
)
}
private fun updateSettings() {
val switch = settings.readerPageSwitch
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in switch
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in switch
isReaderTapsAdaptive = settings.isReaderTapsAdaptive
}
private fun isReaderTapsReversed(): Boolean {
return isReaderTapsAdaptive && listener.readerMode == ReaderMode.REVERSED
}
interface OnInteractionListener {
val readerMode: ReaderMode?
fun switchPageBy(delta: Int)
fun toggleUiVisibility()

View File

@@ -10,8 +10,11 @@ import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.R as materialR
import java.text.SimpleDateFormat
import java.util.*
@@ -36,6 +39,8 @@ class ReaderInfoBarView @JvmOverloads constructor(
private var insetLeft: Int = 0
private var insetRight: Int = 0
private var insetTop: Int = 0
private var cutoutInsetLeft = 0
private var cutoutInsetRight = 0
private val colorText = ColorUtils.setAlphaComponent(
context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK),
200,
@@ -78,19 +83,34 @@ class ReaderInfoBarView @JvmOverloads constructor(
super.onDraw(canvas)
val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom
paint.textAlign = Paint.Align.LEFT
canvas.drawTextOutline(text, (paddingLeft + insetLeft).toFloat(), paddingTop + insetTop + ty)
canvas.drawTextOutline(
text,
(paddingLeft + insetLeft + cutoutInsetLeft).toFloat(),
paddingTop + insetTop + ty,
)
paint.textAlign = Paint.Align.RIGHT
canvas.drawTextOutline(timeText, (width - paddingRight - insetRight).toFloat(), paddingTop + insetTop + ty)
canvas.drawTextOutline(
timeText,
(width - paddingRight - insetRight - cutoutInsetRight).toFloat(),
paddingTop + insetTop + ty,
)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateCutoutInsets(ViewCompat.getRootWindowInsets(this))
updateTextSize()
}
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
updateCutoutInsets(WindowInsetsCompat.toWindowInsetsCompat(insets))
return super.onApplyWindowInsets(insets)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
context.registerReceiver(timeReceiver, IntentFilter(Intent.ACTION_TIME_TICK))
updateCutoutInsets(ViewCompat.getRootWindowInsets(this))
}
override fun onDetachedFromWindow() {
@@ -137,6 +157,20 @@ class ReaderInfoBarView @JvmOverloads constructor(
drawText(text, x, y, paint)
}
private fun updateCutoutInsets(insetsCompat: WindowInsetsCompat?) {
val cutouts = (insetsCompat ?: return).displayCutout?.boundingRects.orEmpty()
cutoutInsetLeft = 0
cutoutInsetRight = 0
for (rect in cutouts) {
if (rect.left <= paddingLeft) {
cutoutInsetLeft += rect.width()
}
if (rect.right >= width - paddingRight) {
cutoutInsetRight += rect.width()
}
}
}
private inner class TimeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {

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.domain.ChaptersLoader
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.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
@@ -86,6 +87,14 @@ class ReaderViewModel @AssistedInject constructor(
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(
mangaData,
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
@@ -94,8 +103,6 @@ class ReaderViewModel @AssistedInject constructor(
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val onZoomChanged = SingleLiveEvent<Unit>()
val isBookmarkAdded: LiveData<Boolean> = currentState.flatMapLatest { state ->
val manga = mangaData.value
if (state == null || manga == null) {
@@ -108,7 +115,6 @@ class ReaderViewModel @AssistedInject constructor(
init {
loadImpl()
subscribeToSettings()
}
override fun onCleared() {
@@ -124,7 +130,7 @@ class ReaderViewModel @AssistedInject constructor(
fun switchMode(newMode: ReaderMode) {
launchJob {
val manga = checkNotNull(mangaData.value)
dataRepository.savePreferences(
dataRepository.saveReaderMode(
manga = manga,
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> {
val fromIndexBounded = fromIndex.coerceAtMost(lastIndex)
val toIndexBounded = toIndex.coerceIn(fromIndexBounded, lastIndex)
@@ -331,7 +330,7 @@ class ReaderViewModel @AssistedInject constructor(
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess {
dataRepository.savePreferences(manga, it)
dataRepository.saveReaderMode(manga, it)
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(defaultMode)

View File

@@ -0,0 +1,151 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
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.*
@AndroidEntryPoint
class ColorFilterConfigActivity :
BaseActivity<ActivityColorFilterBinding>(),
Slider.OnChangeListener,
View.OnClickListener {
@Inject
lateinit var coil: ImageLoader
@Inject
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?) {
super.onCreate(savedInstanceState)
setContentView(ActivityColorFilterBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
binding.sliderBrightness.addOnChangeListener(this)
binding.sliderContrast.addOnChangeListener(this)
val formatter = PercentLabelFormatter(resources)
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) {
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) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
binding.scrollView.updatePadding(
bottom = insets.bottom,
)
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) {
binding.sliderBrightness.value = readerColorFilter?.brightness ?: 0f
binding.sliderContrast.value = readerColorFilter?.contrast ?: 0f
binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
}
private fun onPreviewChanged(preview: MangaPage?) {
if (preview == null) return
ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(preview.url)
.referer(preview.referer)
.scale(Scale.FILL)
.decodeRegion()
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(binding.imageViewBefore))
.allowRgb565(false)
.target(ShadowViewTarget(binding.imageViewBefore, binding.imageViewAfter))
.enqueueWith(coil)
}
private fun onLoadingChanged(isLoading: Boolean) {
binding.sliderContrast.isEnabled = !isLoading
binding.sliderBrightness.isEnabled = !isLoading
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 {
private const val EXTRA_PAGES = "pages"
private const val EXTRA_MANGA = "manga_id"
fun newIntent(context: Context, manga: Manga, page: MangaPage) =
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

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import android.graphics.drawable.Drawable
import android.widget.ImageView
import coil.target.ImageViewTarget
class ShadowViewTarget(
view: ImageView,
private val shadowView: ImageView,
) : ImageViewTarget(view) {
override var drawable: Drawable?
get() = super.drawable
set(value) {
super.drawable = value
shadowView.setImageDrawable(value?.constantState?.newDrawable())
}
}

View File

@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
import org.koitharu.kotatsu.reader.ui.PageSaveContract
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -59,6 +60,7 @@ class ReaderConfigBottomSheet :
binding.buttonSavePage.setOnClickListener(this)
binding.buttonScreenRotate.setOnClickListener(this)
binding.buttonSettings.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnSliderTouchListener(this)
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
@@ -80,6 +82,11 @@ class ReaderConfigBottomSheet :
R.id.button_screen_rotate -> {
orientationHelper?.toggleOrientation()
}
R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return
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.viewbinding.ViewBinding
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.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
@@ -37,8 +37,16 @@ abstract class BasePageHolder<B : ViewBinding>(
protected abstract fun onBind(data: ReaderPage)
fun onAttachedToWindow() {
delegate.onAttachedToWindow()
}
fun onDetachedFromWindow() {
delegate.onDetachedFromWindow()
}
@CallSuper
open fun onRecycled() {
delegate.onRecycle()
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
private const val KEY_STATE = "state"
@@ -18,7 +19,7 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var restoredState = savedInstanceState?.getParcelable<ReaderState?>(KEY_STATE)
var restoredState = savedInstanceState?.getParcelableCompat<ReaderState>(KEY_STATE)
viewModel.content.observe(viewLifecycleOwner) {
onPagesChanged(it.pages, restoredState ?: it.state)

View File

@@ -4,17 +4,17 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
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.utils.ext.resetTransformations
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.utils.ext.resetTransformations
@Suppress("LeakingThis")
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
private val loader: PageLoader,
private val settings: AppSettings,
private val readerSettings: ReaderSettings,
private val exceptionResolver: ExceptionResolver,
) : RecyclerView.Adapter<H>() {
@@ -35,6 +35,16 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
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 getItemOrNull(position: Int) = differ.currentList.getOrNull(position)
@@ -45,8 +55,8 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
final override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
viewType: Int,
): H = onCreateViewHolder(parent, loader, readerSettings, exceptionResolver)
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
differ.submitList(items) {
@@ -57,8 +67,8 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
protected abstract fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
): H
private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() {
@@ -70,6 +80,5 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
return oldItem == newItem
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,26 +3,26 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.LayoutInflater
import android.view.ViewGroup
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.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter(
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) = PageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
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.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter(
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) = WebtoonHolder(
binding = ItemPageWebtoonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
false,
),
loader = loader,
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 org.koitharu.kotatsu.R
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.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.ReaderPage
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
@@ -19,7 +18,7 @@ import org.koitharu.kotatsu.utils.ext.*
class WebtoonHolder(
binding: ItemPageWebtoonBinding,
loader: PageLoader,
settings: AppSettings,
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
View.OnClickListener {
@@ -60,7 +59,8 @@ class WebtoonHolder(
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) {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
minScale = width / sWidth.toFloat()
@@ -70,7 +70,7 @@ class WebtoonHolder(
scrollToRestore != 0 -> scrollToRestore
itemView.top < 0 -> getScrollRange()
else -> 0
}
},
)
scrollToRestore = 0
}
@@ -89,7 +89,7 @@ class WebtoonHolder(
override fun onError(e: Throwable) {
bindingInfo.textViewError.text = e.getDisplayMessage(context.resources)
bindingInfo.buttonRetry.setText(
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again }
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
)
bindingInfo.layoutError.isVisible = true
bindingInfo.progressBar.hideCompat()
@@ -104,4 +104,4 @@ class WebtoonHolder(
scrollToRestore = scroll
}
}
}
}

View File

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

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -52,7 +53,7 @@ class PagesThumbnailsSheet :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pages = arguments?.getParcelable<ParcelableMangaPages>(ARG_PAGES)?.pages
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
if (pages.isNullOrEmpty()) {
dismissAllowingStateLoss()
return

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
@@ -30,7 +31,7 @@ fun pageThumbnailAD(
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = Size(
width = gridWidth,
height = (gridWidth * 13f / 18f).toInt(),
height = (gridWidth / 13f * 18f).toInt(),
)
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
@@ -52,6 +53,7 @@ fun pageThumbnailAD(
ImageRequest.Builder(context)
.data(file)
.size(thumbSize)
.decodeRegion()
.allowRgb565(isLowRamDevice(context))
.build(),
).drawable

View File

@@ -99,7 +99,7 @@ class RemoteListFragment : MangaListFragment() {
override fun onQueryTextChange(newText: String?): Boolean = false
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
return true
}

View File

@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikiMangaSelectionDe
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikimoriSelectorAdapter
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.requireParcelable
import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
@@ -47,7 +48,7 @@ class ScrobblingSelectorBottomSheet :
private val viewModel by assistedViewModels {
viewModelFactory.create(
requireNotNull(requireArguments().getParcelable<ParcelableManga>(MangaIntent.KEY_MANGA)).manga,
requireArguments().requireParcelable<ParcelableManga>(MangaIntent.KEY_MANGA).manga,
)
}
@@ -84,7 +85,7 @@ class ScrobblingSelectorBottomSheet :
dismiss()
}
viewModel.searchQuery.observe(viewLifecycleOwner) {
binding.headerBar.toolbar.subtitle = it
binding.headerBar.subtitle = it
}
}
@@ -102,7 +103,7 @@ class ScrobblingSelectorBottomSheet :
viewModel.loadList(append = true)
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
return true
}

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
@AndroidEntryPoint
class MangaListActivity :
@@ -29,7 +30,7 @@ class MangaListActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtra<ParcelableMangaTags>(EXTRA_TAGS)?.tags
val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val source = intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: tags?.firstOrNull()?.source
if (source == null) {

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -41,8 +42,13 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
@Inject
lateinit var cookieJar: AndroidCookieJar
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history)
findPreference<Preference>(AppSettings.KEY_SHORTCUTS)?.isVisible =
shortcutsUpdater.isDynamicShortcutsAvailable()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -53,7 +55,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(KEY_AUTH)?.run {
if (isVisible) {
loadUsername(this)
loadUsername(viewLifecycleOwner, this)
}
}
}
@@ -68,7 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
}
}
private fun loadUsername(preference: Preference) = viewLifecycleScope.launch {
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
runCatching {
preference.summary = null
withContext(Dispatchers.Default) {
@@ -99,7 +101,8 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
val pref = findPreference<Preference>(KEY_AUTH) ?: return@launch
loadUsername(pref)
val lifecycleOwner = awaitViewLifecycle()
loadUsername(lifecycleOwner, pref)
}
}
}

View File

@@ -120,7 +120,7 @@ class SourcesSettingsFragment :
else -> false
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
return true
}

View File

@@ -7,7 +7,7 @@ import kotlin.math.roundToInt
class GridTouchHelper(
context: Context,
private val listener: OnGridTouchListener
private val listener: OnGridTouchListener,
) : GestureDetector.SimpleOnGestureListener() {
private val detector = GestureDetector(context, this)
@@ -16,7 +16,7 @@ class GridTouchHelper(
private var isDispatching = false
init {
detector.setIsLongpressEnabled(false)
detector.setIsLongpressEnabled(true)
detector.setOnDoubleTapListener(this)
}
@@ -46,7 +46,7 @@ class GridTouchHelper(
}
2 -> AREA_RIGHT
else -> return false
}
},
)
return true
}
@@ -66,4 +66,4 @@ class GridTouchHelper(
fun onProcessTouch(rawX: Int, rawY: Int): Boolean
}
}
}

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.utils.ext
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import java.io.Serializable
@Suppress("DEPRECATION")
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, T::class.java)
} else {
getParcelable(key) as? T
}
}
@Suppress("DEPRECATION")
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableExtra(key, T::class.java)
} else {
getParcelableExtra(key) as? T
}
}
@Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializable(key, T::class.java)
} else {
getSerializable(key) as? T
}
}
inline fun <reified T : Serializable> Bundle.requireSerializable(key: String): T {
return checkNotNull(getSerializableCompat(key)) {
"Serializable of type \"${T::class.java.name}\" not found at \"$key\""
}
}
inline fun <reified T : Parcelable> Bundle.requireParcelable(key: String): T {
return checkNotNull(getParcelableCompat(key)) {
"Parcelable of type \"${T::class.java.name}\" not found at \"$key\""
}
}

View File

@@ -10,8 +10,10 @@ import coil.request.ImageResult
import coil.request.SuccessResult
import coil.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.image.RegionBitmapDecoder
import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener
fun ImageView.newImageRequest(url: Any?): ImageRequest.Builder? {
@@ -47,13 +49,28 @@ fun ImageResult.toBitmapOrNull() = when (this) {
}
fun ImageRequest.Builder.referer(referer: String): ImageRequest.Builder {
return setHeader(CommonHeaders.REFERER, referer)
if (referer.isEmpty()) {
return this
}
try {
setHeader(CommonHeaders.REFERER, referer)
} catch (e: IllegalArgumentException) {
val baseUrl = referer.baseUrl()
if (baseUrl != null) {
setHeader(CommonHeaders.REFERER, baseUrl)
}
}
return this
}
fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder {
return listener(ImageRequestIndicatorListener(indicator))
}
fun ImageRequest.Builder.decodeRegion(): ImageRequest.Builder {
return decoderFactory(RegionBitmapDecoder.Factory())
}
@Suppress("SpellCheckingInspection")
fun ImageRequest.Builder.crossfade(context: Context?): ImageRequest.Builder {
if (context == null) {
@@ -63,3 +80,11 @@ fun ImageRequest.Builder.crossfade(context: Context?): ImageRequest.Builder {
val duration = context.resources.getInteger(R.integer.config_defaultAnimTime) * context.animatorDurationScale
return crossfade(duration.toInt())
}
private fun String.baseUrl(): String? {
return (this.toHttpUrlOrNull()?.newBuilder("/") ?: return null)
.username("")
.password("")
.build()
.toString()
}

View File

@@ -1,14 +1,17 @@
package org.koitharu.kotatsu.utils.ext
import android.os.Bundle
import android.os.Parcelable
import androidx.core.view.MenuProvider
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.coroutineScope
import java.io.Serializable
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
val b = Bundle(size)
@@ -20,18 +23,10 @@ inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
val Fragment.viewLifecycleScope
inline get() = viewLifecycleOwner.lifecycle.coroutineScope
fun <T : Parcelable> Fragment.parcelableArgument(name: String): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
requireNotNull(arguments?.getParcelable(name)) {
"No argument $name passed into ${javaClass.simpleName}"
}
}
}
fun <T : Serializable> Fragment.serializableArgument(name: String): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
@Suppress("UNCHECKED_CAST")
requireNotNull(arguments?.getSerializable(name)) {
requireNotNull(arguments?.getSerializableCompat(name)) {
"No argument $name passed into ${javaClass.simpleName}"
} as T
}
@@ -50,3 +45,19 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) {
fun Fragment.addMenuProvider(provider: MenuProvider) {
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.STARTED)
}
suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner = suspendCancellableCoroutine { cont ->
val liveData = viewLifecycleOwnerLiveData
val observer = object : Observer<LifecycleOwner> {
override fun onChanged(result: LifecycleOwner?) {
if (result != null) {
liveData.removeObserver(this)
cont.resume(result)
}
}
}
liveData.observeForever(observer)
cont.invokeOnCancellation {
liveData.removeObserver(observer)
}
}

View File

@@ -0,0 +1,168 @@
package org.koitharu.kotatsu.utils.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DecodeResult
import coil.decode.DecodeUtils
import coil.decode.Decoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import coil.size.*
import kotlin.math.roundToInt
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
class RegionBitmapDecoder(
private val source: ImageSource,
private val options: Options,
private val parallelismLock: Semaphore,
) : Decoder {
override suspend fun decode() = parallelismLock.withPermit {
runInterruptible { BitmapFactory.Options().decode() }
}
private fun BitmapFactory.Options.decode(): DecodeResult {
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(source.source().inputStream())
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(source.source().inputStream(), false)
}
checkNotNull(regionDecoder)
try {
val rect = configureScale(regionDecoder.width, regionDecoder.height)
configureConfig()
val bitmap = regionDecoder.decodeRegion(rect, this)
bitmap.density = options.context.resources.displayMetrics.densityDpi
return DecodeResult(
drawable = bitmap.toDrawable(options.context.resources),
isSampled = true,
)
} finally {
regionDecoder.recycle()
}
}
private fun BitmapFactory.Options.configureConfig() {
var config = options.config
inMutable = false
if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) {
inPreferredColorSpace = options.colorSpace
}
inPremultiplied = options.premultipliedAlpha
// Decode the image as RGB_565 as an optimization if allowed.
if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") {
config = Bitmap.Config.RGB_565
}
// High color depth images must be decoded as either RGBA_F16 or HARDWARE.
if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) {
config = Bitmap.Config.RGBA_F16
}
inPreferredConfig = config
}
/** Compute and set the scaling properties for [BitmapFactory.Options]. */
private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect {
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
val srcRatio = srcWidth / srcHeight.toDouble()
val dstRatio = dstWidth / dstHeight.toDouble()
val rect = if (srcRatio < dstRatio) {
// probably manga
Rect(0, 0, srcWidth, (srcWidth / dstRatio).toInt())
} else {
Rect(0, 0, (srcHeight / dstRatio).toInt(), srcHeight)
}
rect.offsetTo(
(srcWidth - rect.width()) / 2,
(srcHeight - rect.height()) / 2,
)
// Calculate the image's sample size.
inSampleSize = DecodeUtils.calculateInSampleSize(
srcWidth = rect.width(),
srcHeight = rect.height(),
dstWidth = dstWidth,
dstHeight = dstHeight,
scale = options.scale,
)
// Calculate the image's density scaling multiple.
var scale = DecodeUtils.computeSizeMultiplier(
srcWidth = rect.width() / inSampleSize.toDouble(),
srcHeight = rect.height() / inSampleSize.toDouble(),
dstWidth = dstWidth.toDouble(),
dstHeight = dstHeight.toDouble(),
scale = options.scale,
)
// Only upscale the image if the options require an exact size.
if (options.allowInexactSize) {
scale = scale.coerceAtMost(1.0)
}
inScaled = scale != 1.0
if (inScaled) {
if (scale > 1) {
// Upscale
inDensity = (Int.MAX_VALUE / scale).roundToInt()
inTargetDensity = Int.MAX_VALUE
} else {
// Downscale
inDensity = Int.MAX_VALUE
inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
}
}
return rect
}
class Factory(
maxParallelism: Int = DEFAULT_MAX_PARALLELISM,
) : Decoder.Factory {
@Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
@SinceKotlin("999.9") // Only public in Java.
constructor() : this()
private val parallelismLock = Semaphore(maxParallelism)
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder {
return RegionBitmapDecoder(result.source, options, parallelismLock)
}
override fun equals(other: Any?) = other is Factory
override fun hashCode() = javaClass.hashCode()
}
}
private const val DEFAULT_MAX_PARALLELISM = 4
private inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
}
private inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else height.toPx(scale)
}
private fun Dimension.toPx(scale: Scale) = pxOrElse {
when (scale) {
Scale.FILL -> Int.MIN_VALUE
Scale.FIT -> Int.MAX_VALUE
}
}

View File

@@ -1,5 +1,12 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
</vector>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
tools:navigationIcon="@drawable/abc_ic_clear_material"
tools:title="@string/color_correction">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
</com.google.android.material.appbar.MaterialToolbar>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:overScrollMode="ifContentScrolls">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_normal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_before"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="W,14:9"
app:layout_constraintEnd_toStartOf="@id/imageView_arrow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="?shapeAppearanceCornerLarge"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp"
tools:src="@sample/covers" />
<ImageView
android:id="@+id/imageView_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:padding="@dimen/margin_normal"
android:src="@drawable/ic_arrow_forward"
app:layout_constraintBottom_toBottomOf="@id/imageView_before"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/imageView_before" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_after"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="W,14:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_arrow"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="?shapeAppearanceCornerLarge"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp"
tools:src="@sample/covers" />
<TextView
android:id="@+id/textView_brightness"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_normal"
android:text="@string/brightness"
android:textAppearance="?textAppearanceTitleMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView_before" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_brightness"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:labelFor="@id/textView_brightness"
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_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>
</ScrollView>
</LinearLayout>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/textView"
android:padding="8dp"
android:textAppearance="@style/TextAppearance.AppCompat"
android:textSize="20sp"
app:drawableEndCompat="@drawable/ic_arrow_forward"
app:drawableTint="?attr/colorPrimary"
tools:text="History" />

View File

@@ -103,7 +103,7 @@
android:text="@string/reader_mode_hint"
android:textAppearance="?attr/textAppearanceBodySmall" />
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
@@ -115,23 +115,50 @@
android:paddingEnd="?android:listPreferredItemPaddingEnd">
<TextView
android:layout_width="wrap_content"
android:id="@+id/textView_timer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/automatic_scroll"
android:textAppearance="?attr/textAppearanceButton"
app:drawableStartCompat="@drawable/ic_timer" />
android:textColor="@color/list_item_text_color"
app:drawableStartCompat="@drawable/ic_timer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/slider_timer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_timer"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:contentDescription="@string/automatic_scroll"
android:labelFor="@id/textView_timer"
android:valueFrom="0"
android:valueTo="20"
app:labelBehavior="floating" />
app:labelBehavior="floating"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/textView_timer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="120dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
android:id="@+id/button_color_filter"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/color_correction"
android:textAppearance="?attr/textAppearanceButton"
app:drawableStartCompat="@drawable/ic_appearance" />
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
android:id="@+id/button_settings"

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_order_new"
android:title="@string/newest" />
<item
android:id="@+id/action_order_abs"
android:title="@string/by_name" />
<item
android:id="@+id/action_order_rating"
android:title="@string/by_rating" />
</menu>

View File

@@ -51,7 +51,7 @@
<string name="scale_mode">Mode mise à l\'échelle</string>
<string name="report_github">Signaler un problème sur GitHub</string>
<string name="create_category">Nouvelle catégorie</string>
<string name="right_to_left">De droite à gauche (←)</string>
<string name="right_to_left">De droite à gauche</string>
<string name="no_update_available">Aucune mise à jour disponible</string>
<string name="update_check_failed">Échec de la recherche de mise à jour</string>
<string name="checking_for_updates">Recherche de mises à jour…</string>
@@ -320,4 +320,58 @@
<string name="invalid_domain_message">Domaine invalide</string>
<string name="select_range">Sélectionner une plage</string>
<string name="not_found_404">Contenu non trouvé ou supprimé</string>
<string name="manga_error_description_pattern">Détails de l\'erreur :&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Essayez de &lt;a href=%2$s&gt;ouvrir le manga dans un navigateur web&lt;/a&gt; pour vous assurer qu\'il est disponible sur sa source&lt;br&gt;2. S\'il est disponible, envoyez un rapport d\'erreur aux développeurs.</string>
<string name="confirm_exit">Rappuyez sur Retour pour quitter</string>
<string name="categories_delete_confirm">Êtes-vous sûr·e de vouloir supprimer les catégories de favoris sélectionnées \?
\nTous les mangas qui s\'y trouvent seront perdus et ceci ne peut pas être annulé.</string>
<string name="exit_confirmation_summary">Appuyez deux fois sur la touche Retour pour quitter l\'appli</string>
<string name="available">Disponible</string>
<string name="exit_confirmation">Confirmation de sortie</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="feed">Flux</string>
<string name="enter_email_text">Entrez votre courriel pour continuer</string>
<string name="app_update_available_s">Mise à jour de l\'application disponible : %s</string>
<string name="importing_manga">Importation de mangas</string>
<string name="removed_from_favourites">Retiré des favoris</string>
<string name="clear_all_history">Effacer tout l\'historique</string>
<string name="last_2_hours">Les 2 dernières heures</string>
<string name="history_cleared">Historique effacé</string>
<string name="manage">Gérer</string>
<string name="no_bookmarks_yet">Aucun marque-page</string>
<string name="no_bookmarks_summary">Vous pouvez créer un marque-page pendant la lecture d\'un manga</string>
<string name="bookmarks_removed">Marque-pages supprimés</string>
<string name="no_manga_sources">Aucune source de mangas</string>
<string name="no_manga_sources_text">Autoriser les sources de mangas de lire des mangas en ligne</string>
<string name="random">Aléatoire</string>
<string name="reorder">Réordonner</string>
<string name="empty">Vide</string>
<string name="changelog">Journal des modifications</string>
<string name="explore">Explorer</string>
<string name="text_shelf_holder_primary">Vos mangas seront affichés ici</string>
<string name="text_shelf_holder_secondary">Trouvez ce que vous voulez lire dans la section « Explorer »</string>
<string name="canceled">Annulé</string>
<string name="account_already_exists">Le compte existe déjà</string>
<string name="back">Retour</string>
<string name="sync">Synchronisation</string>
<string name="sync_title">Synchronisez vos données</string>
<string name="email_enter_hint">Entrez votre courriel pour continuer</string>
<string name="saved_manga">Mangas sauvegardés</string>
<string name="pages_cache">Cache des pages</string>
<string name="other_cache">Autre cache</string>
<string name="storage_usage">Utilisation du stockage</string>
<string name="removed_from_s">Retiré de « %s »</string>
<string name="options">Options</string>
<string name="downloading_manga">Télécharger des mangas</string>
<string name="incognito_mode">Mode incognito</string>
<string name="no_chapters">Aucun chapitre</string>
<string name="automatic_scroll">Défilement automatique</string>
<string name="off_short">Désactivé</string>
<string name="seconds_pattern">%s s</string>
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
<string name="reader_info_bar">Afficher la barre d\'infos dans le lecteur</string>
<string name="comics_archive">Archives des BD</string>
<string name="folder_with_images">Dossier avec des images</string>
<string name="import_completed">Importation terminée</string>
<string name="import_completed_hint">Vous pouvez supprimer le fichier original du stockage pour gagner de l\'espace</string>
<string name="import_will_start_soon">L\'importation va bientôt commencer</string>
</resources>

View File

@@ -326,4 +326,46 @@
<string name="removed_from_favourites">Dihapus dari favorit</string>
<string name="import_will_start_soon">Impor akan segera dimulai</string>
<string name="not_found_404">Konten tidak ditemukan atau dihapus</string>
<string name="exit_confirmation_summary">Tekan Kembali dua kali untuk keluar dari aplikasi</string>
<string name="exit_confirmation">Konfirmasi keluar</string>
<string name="pages_cache">Tembolok halaman</string>
<string name="other_cache">Tembolok lainnya</string>
<string name="storage_usage">Penggunaan penyimpanan</string>
<string name="available">Tersedia</string>
<string name="incognito_mode">Mode Incognito</string>
<string name="app_update_available_s">Pembaruan aplikasi tersedia: %s</string>
<string name="automatic_scroll">Gulir otomatis</string>
<string name="comics_archive">Arsip komik</string>
<string name="folder_with_images">Folder dengan gambar</string>
<string name="import_completed_hint">Anda bisa menghapus berkas asli dari penyimpanan untuk menghemat ruang</string>
<string name="feed">Umpan</string>
<string name="manga_error_description_pattern">Detail galat:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Coba untuk &lt;a href=%2$s&gt;membuka manga di peramban web&lt;/a&gt; untuk memastikan bahwa itu tersedia di sumbernya&lt;br&gt;2. Jika tersedia, kirim laporan galat ke pengembang.</string>
<string name="brightness">Kecerahan</string>
<string name="contrast">Kontras</string>
<string name="reset">Atur Ulang</string>
<string name="color_correction_hint">Pengaturan warna yang dipilih akan diingat untuk manga ini</string>
<string name="text_unsaved_changes_prompt">Anda memiliki perubahan belum tersimpan, apakah Anda ingin menyimpannya atau membuangnya\?</string>
<string name="discard">Buang</string>
<string name="use_fingerprint">Gunakan sidik jari jika tersedia</string>
<string name="appwidget_shelf_description">Manga dari favorit Anda</string>
<string name="appwidget_recent_description">Manga yang baru-baru ini Anda baca</string>
<string name="data_deletion">Penghapusan data</string>
<string name="account_already_exists">Akun sudah ada</string>
<string name="back">Kembali</string>
<string name="disable_battery_optimization_summary">Membantu pemeriksaan pembaruan di latar belakang</string>
<string name="crash_text">Ada sesuatu yang salah. Mohon untuk mengirim laporan kutu (bug) ke pengembang untuk membantu kami memperbaikinya.</string>
<string name="report">Lapor</string>
<string name="exclude_nsfw_from_history_summary">Manga yang ditandai sebagai NSFW tidak akan ditambahkan ke riwayat dan progres Anda tidak akan disimpan</string>
<string name="clear_cookies_summary">Bisa membantu dalam beberapa masalah. Seluruh otorisasi akan menjadi tidak valid.</string>
<string name="manage">Kelola</string>
<string name="no_manga_sources_text">Aktifkan sumber manga untuk membaca manga daring</string>
<string name="categories_delete_confirm">Apakah Anda yakin ingin menghapus kategori favorit yang dipilih\?
\n Semua manga di sana akan hilang dan ini tidak bisa diurungkan.</string>
<string name="reorder">Atur Ulang</string>
<string name="saved_manga">Manga tersimpan</string>
<string name="confirm_exit">Tekan Kembali lagi untuk keluar</string>
<string name="no_chapters">Tidak ada bab</string>
<string name="history_shortcuts">Tampilkan pintasan manga baru-baru ini</string>
<string name="history_shortcuts_summary">Buat manga baru-baru ini tersedia dengan menekan panjang pada ikon aplikasi</string>
<string name="select_range">Pilih jangkauan</string>
</resources>

View File

@@ -374,4 +374,5 @@
<string name="comics_archive">コミックアーカイブ</string>
<string name="downloading_manga">漫画をダウンロードする</string>
<string name="feed">フィード</string>
<string name="manga_error_description_pattern">エラーの詳細:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. &lt;a href=%2$s&gt;Webブラウザで漫画を開いてみて&lt;/a&gt;、そのソースで利用できるか確認する&lt;br&gt;2. 利用できる場合は、開発者にエラーレポートを送信する&lt;/a&gt;。</string>
</resources>

View File

@@ -374,4 +374,16 @@
<string name="import_will_start_soon">Импорт скоро начнётся</string>
<string name="feed">Лента</string>
<string name="downloading_manga">Загрузка манги</string>
<string name="manga_error_description_pattern">Информация об ошибке:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Попробуйте &lt;a href=%2$s&gt;открыть мангу в веб-браузере&lt;/a&gt;, чтобы убедиться, что она доступна в источнике&lt;br&gt;2. Если она доступна, отправьте отчёт об ошибке разработчикам.</string>
<string name="history_shortcuts">Показывать ярлыки последней прочитанной манги</string>
<string name="history_shortcuts_summary">Сделать недавно прочитанную доступной по долгому нажатию на иконку приложения</string>
<string name="reader_control_ltr_summary">Нажатие на правый край или нажатие правой клавиши всегда переключает на следующую страницу</string>
<string name="reader_control_ltr">Эргономичное управление режимом чтения</string>
<string name="reset">Сбросить</string>
<string name="discard">Отменить</string>
<string name="text_unsaved_changes_prompt">У вас есть несохраненные изменения, вы хотите сохранить или отменить их\?</string>
<string name="contrast">Контрастность</string>
<string name="brightness">Яркость</string>
<string name="color_correction">Цветокоррекция</string>
<string name="color_correction_hint">Выбранные настройки цвета запомнятся для этой манги</string>
</resources>

View File

@@ -16,7 +16,7 @@
<string name="try_again">Tekrar dene</string>
<string name="clear_history">Geçmişi temizle</string>
<string name="nothing_found">Hiçbir şey bulunamadı</string>
<string name="history_is_empty">Sen geçmişiniz yok</string>
<string name="history_is_empty">Geçmiş yok</string>
<string name="read">Oku</string>
<string name="you_have_not_favourites_yet">Henüz favorileriniz yok</string>
<string name="add_to_favourites">Favoriniz</string>
@@ -209,7 +209,7 @@
<string name="new_version_s">Yeni sürüm: %s</string>
<string name="protect_application">Uygulamayı koru</string>
<string name="passwords_mismatch">Parolalar eşleşmiyor</string>
<string name="right_to_left">Sağdan sola (←)</string>
<string name="right_to_left">Sağdan-sola</string>
<string name="create_category">Yeni kategori</string>
<string name="file_not_found">Dosya bulunamadı</string>
<string name="data_restored_success">Tüm veriler geri yüklendi</string>
@@ -320,4 +320,25 @@
<string name="invalid_domain_message">Geçersiz etki alanı</string>
<string name="select_range">Aralık seç</string>
<string name="not_found_404">İçerik bulunamadı veya kaldırıldı</string>
<string name="text_shelf_holder_primary">Manganız burada görüntülenecek</string>
<string name="canceled">İptal edilmiş</string>
<string name="account_already_exists">Hesap zaten var</string>
<string name="back">Geri</string>
<string name="sync">Senkronizasyon</string>
<string name="sync_title">Verini yedekle</string>
<string name="email_enter_hint">Devam etmek için E-Postanızı girin</string>
<string name="clear_all_history">Tüm gecmişi temizle</string>
<string name="last_2_hours">Son 2 saat</string>
<string name="history_cleared">Geçmiş temizlendi</string>
<string name="manage">Yönet</string>
<string name="no_bookmarks_yet">Yer işareti yok</string>
<string name="no_bookmarks_summary">Manga okurken yer işareti oluşturabilirsiniz</string>
<string name="bookmarks_removed">Yer işaretleri kaldırıldı</string>
<string name="no_manga_sources">Manga kaynağı yok</string>
<string name="no_manga_sources_text">Çevrimiçi manga okumak için manga kaynaklarını aktif edin</string>
<string name="random">Rastgele</string>
<string name="empty">Boş</string>
<string name="explore">Keşfet</string>
<string name="confirm_exit">Çıkmak için tekrar Geri tıkla</string>
<string name="exit_confirmation_summary">Çıkmak için iki defa Geri tıkla</string>
</resources>

View File

@@ -373,4 +373,5 @@
<string name="removed_from_favourites">Видалено з уподобань</string>
<string name="other_cache">Інший кеш</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="manga_error_description_pattern">Деталі помилки:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Спробуйте &lt;a href=%2$s&gt;відкрити манґу у веб-браузері&lt;/a&gt;, щоб переконатися, що вона доступна в джерелі&lt;br&gt;2. Якщо вона доступна, то надішліть звіт про помилку розробникам.</string>
</resources>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="items">
<item quantity="other">%1$d 个项目</item>
</plurals>
<plurals name="new_chapters">
<item quantity="other">%1$d 个新章节</item>
</plurals>
<plurals name="chapters">
<item quantity="other">%1$d 章节</item>
</plurals>
<plurals name="chapters_from_x">
<item quantity="other">来自 %2$d 中的 %1$d 个章节</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="other">%1$d 分钟前</item>
</plurals>
<plurals name="hours_ago">
<item quantity="other">%1$d 小时前</item>
</plurals>
<plurals name="days_ago">
<item quantity="other">%1$d 天前</item>
</plurals>
<plurals name="pages">
<item quantity="other">共 %1$d 页</item>
</plurals>
</resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="settings">设置</string>
<string name="local_storage">本地存储</string>
<string name="favourites">喜欢</string>
@@ -78,7 +78,7 @@
<string name="sort_order">排序顺序</string>
<string name="filter">过滤器</string>
<string name="theme">主题</string>
<string name="dark">黑暗</string>
<string name="dark">深色</string>
<string name="light">浅色</string>
<string name="automatic">跟随系统</string>
<string name="pages">页数</string>
@@ -101,7 +101,7 @@
<string name="cache">缓存</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">标准</string>
<string name="webtoon">Webtoon</string>
<string name="webtoon">条漫</string>
<string name="read_mode">阅读模式</string>
<string name="grid_size">网格大小</string>
<string name="search_on_s">在%s上搜索</string>
@@ -188,7 +188,7 @@
<string name="checking_for_updates">检查更新…</string>
<string name="update_check_failed">无法查找更新</string>
<string name="no_update_available">没有更新</string>
<string name="right_to_left">从右到左 (←)</string>
<string name="right_to_left">从右到左</string>
<string name="create_category">新类别</string>
<string name="scale_mode">缩放模式</string>
<string name="zoom_mode_fit_center">适应中心</string>
@@ -249,7 +249,7 @@
<string name="screenshots_block_all">始终阻止</string>
<string name="suggestions">建议</string>
<string name="suggestions_enable">启用建议</string>
<string name="text_suggestion_holder">开始阅读漫画,你会得到个性化的建议</string>
<string name="text_suggestion_holder">开始阅读漫画,你会得到个性化的建议</string>
<string name="exclude_nsfw_from_suggestions">请勿推荐18+漫画</string>
<string name="enabled">启用</string>
<string name="disabled">禁用</string>
@@ -290,7 +290,7 @@
<string name="dns_over_https">DNS over HTTPS</string>
<string name="default_mode">默认模式</string>
<string name="detect_reader_mode">自动检测阅读器模式</string>
<string name="detect_reader_mode_summary">自动检测漫画是否为网络漫画</string>
<string name="detect_reader_mode_summary">自动检测漫画是否为条漫</string>
<string name="disable_battery_optimization">禁用电池优化</string>
<string name="disable_battery_optimization_summary">帮助进行背景更新检查</string>
<string name="crash_text">出了点问题. 请向开发人员提交一份错误报告以帮助我们修复它.</string>
@@ -313,4 +313,65 @@
<string name="exclude_nsfw_from_history_summary">标记为NSFW的漫画将永远不会被添加到历史中你的进度也不会被保存</string>
<string name="clear_cookies_summary">可以在出现一些问题时提供帮助. 所有授权将被视为无效</string>
<string name="show_all">显示全部</string>
<string name="manga_error_description_pattern">错误详情:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1.尝试&lt;a href=%2$s&gt;在网络浏览器中打开漫画&lt;/a&gt;以确保在其来源中可用&lt;br&gt;2.如果可用,请向开发人员发送错误报告。</string>
<string name="invalid_domain_message">无效域名</string>
<string name="text_shelf_holder_primary">此处将显示你的漫画</string>
<string name="text_shelf_holder_secondary">在【浏览】页面搜索想要阅读的漫画</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="canceled">已取消</string>
<string name="account_already_exists">账号已存在</string>
<string name="back">返回</string>
<string name="sync">同步</string>
<string name="sync_title">同步您的数据</string>
<string name="email_enter_hint">输入您的邮箱以继续</string>
<string name="status_dropped">已放弃</string>
<string name="select_range">选择范围</string>
<string name="clear_all_history">清除所有历史</string>
<string name="last_2_hours">过去2小时</string>
<string name="bookmarks_removed">书签已移除</string>
<string name="history_cleared">历史已清除</string>
<string name="manage">管理</string>
<string name="no_bookmarks_yet">还没有书签</string>
<string name="no_bookmarks_summary">您可以在阅读漫画时创建书签</string>
<string name="no_manga_sources">无漫画源</string>
<string name="no_manga_sources_text">启用漫画源以在线阅读漫画</string>
<string name="random">随机</string>
<string name="categories_delete_confirm">您确定要删除选定的收藏夹吗?
\n所有收藏夹中的漫画将丢失且无法恢复。</string>
<string name="reorder">重新排序</string>
<string name="empty">留空</string>
<string name="changelog">变更日志</string>
<string name="explore">浏览</string>
<string name="automatic_scroll">自动滚动</string>
<string name="off_short">关闭</string>
<string name="reader_info_bar">在阅读器中显示信息栏</string>
<string name="comics_archive">漫画压缩包</string>
<string name="folder_with_images">图片文件夹</string>
<string name="importing_manga">漫画导入中</string>
<string name="seconds_pattern">%ss</string>
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d 的 %2$d 启用</string>
<string name="size_s">大小:%s</string>
<string name="confirm_exit">再按一次返回键退出</string>
<string name="exit_confirmation_summary">按两次返回键退出应用</string>
<string name="exit_confirmation">退出确认</string>
<string name="saved_manga">已保存漫画</string>
<string name="pages_cache">页面缓存</string>
<string name="other_cache">其他缓存</string>
<string name="storage_usage">存储占用</string>
<string name="available">可用</string>
<string name="enter_email_text">输入您的邮箱以继续</string>
<string name="removed_from_favourites">从收藏中移除</string>
<string name="removed_from_s">从 \"%s\" 中移除</string>
<string name="options">选项</string>
<string name="downloading_manga">下载中的漫画</string>
<string name="incognito_mode">隐身模式</string>
<string name="app_update_available_s">应用更新可用: %s</string>
<string name="no_chapters">无章节</string>
<string name="import_completed">导入完毕</string>
<string name="import_completed_hint">您可以从存储中删除原文件以节省空间</string>
<string name="import_will_start_soon">即将开始导入</string>
<string name="feed">订阅源</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="not_found_404">内容未找到或已移除</string>
</resources>

View File

@@ -377,4 +377,15 @@
<string name="import_will_start_soon">Import will start soon</string>
<string name="feed">Feed</string>
<string name="manga_error_description_pattern">Error details:&lt;br>&lt;tt>%1$s&lt;/tt>&lt;br>&lt;br>1. Try to &lt;a href="%2$s">open manga in a web browser&lt;/a> to ensure it is available on its source&lt;br>2. If it is available, send an error report to the developers.</string>
<string name="history_shortcuts">Show recent manga shortcuts</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">Ergonomic reader control</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>

View File

@@ -13,6 +13,12 @@
android:summary="@string/exclude_nsfw_from_history_summary"
android:title="@string/exclude_nsfw_from_history" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="dynamic_shortcuts"
android:summary="@string/history_shortcuts_summary"
android:title="@string/history_shortcuts" />
<PreferenceCategory android:title="@string/tracking">
<PreferenceScreen
@@ -56,4 +62,4 @@
</PreferenceCategory>
</PreferenceScreen>
</PreferenceScreen>

View File

@@ -29,6 +29,12 @@
android:title="@string/switch_pages"
app:allowDividerAbove="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="reader_taps_ltr"
android:summary="@string/reader_control_ltr_summary"
android:title="@string/reader_control_ltr" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="reader_animation"

View File

@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'

View File

@@ -1,7 +1,7 @@
#Sun May 15 16:26:09 EEST 2022
#Wed Aug 24 10:43:54 EEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4