Compare commits
48 Commits
v4.0-a3
...
v4.0-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
| 734765dbdd | |||
|
|
117c4c5978 | ||
|
|
13e33a6614 | ||
|
|
affb495136 | ||
|
|
9b2dafd668 | ||
| 5f32c0401f | |||
| f35f40ed27 | |||
| 46f0d3ef74 | |||
| c27c785ac2 | |||
| 6e844e8c3b | |||
| 4186c36f30 | |||
| 2d727a0da8 | |||
| 39e574e9dc | |||
| efb94cbd67 | |||
| 757e33dfb4 | |||
| ab9bdf9f07 | |||
| a68632a888 | |||
| 9b0dc8b413 | |||
| f43fe5830e | |||
| ce616b328c | |||
| 824a8ff97a | |||
|
|
ef9018b92f | ||
|
|
a088d10935 | ||
| 2e561697ac | |||
| d242acd502 | |||
| d37b44d3f6 | |||
| 6243fc88c9 | |||
|
|
f74e865b06 | ||
|
|
1a2bc02188 | ||
| b0153e9f61 | |||
|
|
7f3fc1b88a | ||
|
|
40bf0c75bf | ||
|
|
9407bd205c | ||
|
|
3317c8dddc | ||
| fc4b6eb1af | |||
| 2aaaf2f4a2 | |||
| 92aa96a644 | |||
| 6a0a4023ad | |||
| 8569610e52 | |||
| 51ffa4d469 | |||
| e4c4d2bbf0 | |||
| 35d6f1fb34 | |||
| edac5fda8c | |||
| 040d3e4433 | |||
| 011dd4c069 | |||
| b4f93fc0a5 | |||
| e0d74ba2a9 | |||
| d853bb2c62 |
6
.idea/ktlint.xml
generated
6
.idea/ktlint.xml
generated
@@ -1,7 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KtlintProjectConfiguration">
|
||||
<enableKtlint>false</enableKtlint>
|
||||
<androidMode>true</androidMode>
|
||||
<treatAsErrors>false</treatAsErrors>
|
||||
<disabledRules>
|
||||
<list>
|
||||
<option value="no-empty-first-line-in-method-block" />
|
||||
</list>
|
||||
</disabledRules>
|
||||
</component>
|
||||
</project>
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 492
|
||||
versionName '4.0-a3'
|
||||
versionCode 497
|
||||
versionName '4.0-beta1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -82,13 +82,13 @@ afterEvaluate {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:b3a9c5fcda') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
@@ -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'
|
||||
|
||||
@@ -122,10 +122,9 @@ dependencies {
|
||||
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.2.0'
|
||||
// implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2942b797a2'
|
||||
implementation 'io.coil-kt:coil-base:2.2.1'
|
||||
implementation 'io.coil-kt:coil-svg:2.2.1'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:a9ab74c8b4'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
|
||||
implementation 'ch.acra:acra-http:5.9.6'
|
||||
@@ -145,7 +144,7 @@ dependencies {
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
androidTestImplementation 'androidx.room:room-testing:2.4.3'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
||||
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2'
|
||||
|
||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -13,3 +13,4 @@
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
|
||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||
|
||||
@@ -133,6 +133,9 @@
|
||||
<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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
@@ -92,7 +91,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
||||
ActivityCompat.recreate(this)
|
||||
// ActivityCompat.recreate(this)
|
||||
TODO("Test error")
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
|
||||
@@ -60,7 +60,19 @@ class SectionedSelectionController<T : Any>(
|
||||
startActionMode()
|
||||
notifySelectionChanged()
|
||||
}
|
||||
recyclerView.addItemDecoration(decoration)
|
||||
var shouldAddDecoration = true
|
||||
for (i in (0 until recyclerView.itemDecorationCount).reversed()) {
|
||||
val decor = recyclerView.getItemDecorationAt(i)
|
||||
if (decor === decoration) {
|
||||
shouldAddDecoration = false
|
||||
break
|
||||
} else if (decor.javaClass == decoration.javaClass) {
|
||||
recyclerView.removeItemDecorationAt(i)
|
||||
}
|
||||
}
|
||||
if (shouldAddDecoration) {
|
||||
recyclerView.addItemDecoration(decoration)
|
||||
}
|
||||
if (pendingData?.isEmpty() == true) {
|
||||
pendingData = null
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ScrollbarAnimator(
|
||||
.animate()
|
||||
.translationX(0f)
|
||||
.alpha(1f)
|
||||
.setListener(null)
|
||||
.setDuration(animationDuration)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
@@ -12,7 +13,6 @@ import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||
import org.koitharu.kotatsu.list.ui.adapter.*
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class BookmarksGroupAdapter(
|
||||
coil: ImageLoader,
|
||||
@@ -34,11 +34,11 @@ class BookmarksGroupAdapter(
|
||||
selectionController = selectionController,
|
||||
bookmarkClickListener = bookmarkClickListener,
|
||||
groupClickListener = groupClickListener,
|
||||
)
|
||||
),
|
||||
)
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyStateListAD(listener))
|
||||
.addDelegate(emptyStateListAD(coil, listener))
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
@@ -64,4 +64,4 @@ class BookmarksGroupAdapter(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
@@ -17,9 +20,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.utils.isSuccess
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class ExceptionResolver private constructor(
|
||||
private val activity: FragmentActivity?,
|
||||
@@ -65,7 +65,7 @@ class ExceptionResolver private constructor(
|
||||
cont.invokeOnCancellation {
|
||||
continuations.remove(CloudFlareDialog.TAG, cont)
|
||||
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
|
||||
dialog.dismiss()
|
||||
dialog.dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,4 +94,4 @@ class ExceptionResolver private constructor(
|
||||
|
||||
fun canResolve(e: Throwable) = getResolveStringId(e) != 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
// Limits to avoid TransactionTooLargeException
|
||||
private const val MAX_SAFE_SIZE = 1024 * 512 // Assume that 512 kb is safe parcel size
|
||||
private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe
|
||||
private const val MAX_SAFE_CHAPTERS_COUNT = 32 // this is 100% safe
|
||||
|
||||
class ParcelableManga(
|
||||
val manga: Manga,
|
||||
|
||||
@@ -2,10 +2,11 @@ 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
|
||||
@@ -13,6 +14,8 @@ import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.room.InvalidationTracker
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Precision
|
||||
import coil.size.Scale
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -22,6 +25,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 +39,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 +60,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 +83,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,15 +102,25 @@ 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(
|
||||
coil.execute(
|
||||
ImageRequest.Builder(context)
|
||||
.data(manga.coverUrl)
|
||||
.size(iconSize.width, iconSize.height)
|
||||
.precision(Precision.EXACT)
|
||||
.scale(Scale.FILL)
|
||||
.build(),
|
||||
).requireBitmap()
|
||||
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
|
||||
}.fold(
|
||||
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
||||
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,8 +6,6 @@ import androidx.appcompat.view.ActionMode
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -202,18 +200,4 @@ class ChaptersFragment :
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
private fun findBottomSheetBehavior(): BottomSheetBehavior<*>? {
|
||||
val v = view ?: return null
|
||||
for (p in v.parents) {
|
||||
val layoutParams = (p as? View)?.layoutParams
|
||||
if (layoutParams is CoordinatorLayout.LayoutParams) {
|
||||
val behavior = layoutParams.behavior
|
||||
if (behavior is BottomSheetBehavior<*>) {
|
||||
return behavior
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ class ChaptersMenuProvider(
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
|
||||
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
|
||||
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
|
||||
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
|
||||
@@ -155,7 +155,7 @@ class DownloadManager @AssistedInject constructor(
|
||||
}
|
||||
outState.value = DownloadState.PostProcessing(startId, data, cover)
|
||||
output.mergeWithExisting()
|
||||
output.finalize()
|
||||
output.finish()
|
||||
val localManga = localMangaRepository.getFromFile(output.file)
|
||||
outState.value = DownloadState.Done(startId, data, cover, localManga)
|
||||
} catch (e: CancellationException) {
|
||||
|
||||
@@ -4,12 +4,12 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class CategoriesAdapter(
|
||||
coil: ImageLoader,
|
||||
@@ -20,7 +20,7 @@ class CategoriesAdapter(
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener))
|
||||
.addDelegate(emptyStateListAD(listListener))
|
||||
.addDelegate(emptyStateListAD(coil, listListener))
|
||||
.addDelegate(loadingStateAD())
|
||||
}
|
||||
|
||||
@@ -56,4 +56,4 @@ class CategoriesAdapter(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -47,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)
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class HistoryListMenuProvider(
|
||||
private val context: Context,
|
||||
@@ -38,6 +38,6 @@ class HistoryListMenuProvider(
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_history_grouping).isChecked = viewModel.isGroupingEnabled.value == true
|
||||
menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
|
||||
|
||||
private fun setDrawable(drawable: Drawable?) {
|
||||
if (drawable != null) {
|
||||
view.setImage(ImageSource.bitmap(drawable.toBitmap()))
|
||||
view.setImage(ImageSource.Bitmap(drawable.toBitmap()))
|
||||
} else {
|
||||
view.recycle()
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class LibraryAdapter(
|
||||
)
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyStateListAD(listener))
|
||||
.addDelegate(emptyStateListAD(coil, listener))
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
|
||||
@@ -55,11 +55,8 @@ fun libraryGroupAD(
|
||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||
binding.buttonMore.setOnClickListener(listenerAdapter)
|
||||
|
||||
bind { payloads ->
|
||||
if (payloads.isEmpty()) {
|
||||
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
|
||||
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
||||
}
|
||||
bind {
|
||||
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
||||
binding.textViewTitle.text = item.getTitle(context.resources)
|
||||
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
|
||||
adapter.items = item.items
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
@file:SuppressLint("UnsafeOptInUsageError")
|
||||
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.google.android.material.badge.BadgeUtils
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
@CheckResult
|
||||
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
|
||||
return if (counter > 0) {
|
||||
val badgeDrawable = badge ?: initBadge(this)
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||
|
||||
fun emptyStateListAD(
|
||||
coil: ImageLoader,
|
||||
listener: ListStateHolderListener,
|
||||
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
|
||||
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) }
|
||||
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||
|
||||
bind {
|
||||
binding.icon.setImageResource(item.icon)
|
||||
binding.icon.newImageRequest(item.icon)?.enqueueWith(coil)
|
||||
binding.textPrimary.setText(item.textPrimary)
|
||||
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
||||
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||
}
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
binding.icon.disposeImageRequest()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
open class MangaListAdapter(
|
||||
coil: ImageLoader,
|
||||
@@ -24,7 +24,7 @@ open class MangaListAdapter(
|
||||
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
|
||||
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
|
||||
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, listener))
|
||||
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
|
||||
.addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener))
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ fun mangaListDetailedItemAD(
|
||||
}
|
||||
binding.textViewRating.textAndVisible = item.rating
|
||||
binding.textViewTags.text = item.tags
|
||||
itemView.bindBadge(badge, item.counter)
|
||||
badge = itemView.bindBadge(badge, item.counter)
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
|
||||
@@ -40,7 +40,7 @@ fun mangaListItemAD(
|
||||
lifecycle(lifecycleOwner)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
itemView.bindBadge(badge, item.counter)
|
||||
badge = itemView.bindBadge(badge, item.counter)
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
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>()
|
||||
|
||||
@@ -32,7 +36,7 @@ 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()
|
||||
@@ -70,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)
|
||||
|
||||
@@ -62,7 +62,7 @@ class CbzMangaOutput(
|
||||
index.addChapter(chapter)
|
||||
}
|
||||
|
||||
suspend fun finalize() {
|
||||
suspend fun finish() {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
output.put(ENTRY_NAME_INDEX, index.toString())
|
||||
output.finish()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -43,7 +43,7 @@ class DirMangaImporter(
|
||||
)
|
||||
it.sortChaptersByName()
|
||||
it.mergeWithExisting()
|
||||
it.finalize()
|
||||
it.finish()
|
||||
it.file
|
||||
}
|
||||
return localMangaRepository.getFromFile(dest)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,4 +23,4 @@ class LocalListMenuProvider(
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
@@ -289,16 +290,16 @@ class MainActivity :
|
||||
}
|
||||
|
||||
private fun onFirstStart() {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
|
||||
when {
|
||||
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
withContext(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
}
|
||||
requestNotificationsPermission()
|
||||
when {
|
||||
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
@@ -32,7 +33,7 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
val isResumeEnabled = historyRepository
|
||||
.observeHasItems()
|
||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val counters = combine(
|
||||
appUpdateRepository.observeAvailableUpdate(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -202,7 +205,7 @@ class ReaderActivity :
|
||||
val resolveTextId = ExceptionResolver.getResolveStringId(e)
|
||||
if (resolveTextId != 0) {
|
||||
dialog.setPositiveButton(resolveTextId, listener)
|
||||
} else {
|
||||
} else if (e.isReportable()) {
|
||||
dialog.setPositiveButton(R.string.report, listener)
|
||||
}
|
||||
dialog.show()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,18 @@ abstract class BasePageHolder<B : ViewBinding>(
|
||||
|
||||
protected abstract fun onBind(data: ReaderPage)
|
||||
|
||||
@CallSuper
|
||||
open fun onAttachedToWindow() {
|
||||
delegate.onAttachedToWindow()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onDetachedFromWindow() {
|
||||
delegate.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onRecycled() {
|
||||
delegate.onRecycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,26 @@ package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import androidx.lifecycle.Observer
|
||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||
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,
|
||||
) : 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() {
|
||||
@@ -68,7 +76,7 @@ class PageHolderDelegate(
|
||||
callback.onImageShown()
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Exception) {
|
||||
override fun onImageLoadError(e: Throwable) {
|
||||
val file = this.file
|
||||
error = e
|
||||
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,41 +23,42 @@ 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)
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
resetScaleAndCenter()
|
||||
}
|
||||
ZoomMode.FIT_HEIGHT -> {
|
||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
minScale = height / sHeight.toFloat()
|
||||
setScaleAndCenter(
|
||||
minScale,
|
||||
PointF(sWidth.toFloat(), sHeight / 2f)
|
||||
PointF(sWidth.toFloat(), sHeight / 2f),
|
||||
)
|
||||
}
|
||||
ZoomMode.FIT_WIDTH -> {
|
||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
minScale = width / sWidth.toFloat()
|
||||
setScaleAndCenter(
|
||||
minScale,
|
||||
PointF(sWidth / 2f, 0f)
|
||||
PointF(sWidth / 2f, 0f),
|
||||
)
|
||||
}
|
||||
ZoomMode.KEEP_START -> {
|
||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
setScaleAndCenter(
|
||||
maxScale,
|
||||
PointF(sWidth.toFloat(), 0f)
|
||||
PointF(sWidth.toFloat(), 0f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -7,14 +7,12 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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,15 +20,14 @@ 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 {
|
||||
|
||||
init {
|
||||
binding.ssiv.setExecutor(Dispatchers.Default.asExecutor())
|
||||
binding.ssiv.setEagerLoadingEnabled(!isLowRamDevice(context))
|
||||
binding.ssiv.setOnImageEventListener(delegate)
|
||||
binding.ssiv.isEagerLoadingEnabled = !isLowRamDevice(context)
|
||||
binding.ssiv.addOnImageEventListener(delegate)
|
||||
@Suppress("LeakingThis")
|
||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
|
||||
@@ -63,21 +60,22 @@ open class PageHolder(
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri) {
|
||||
binding.ssiv.setImage(ImageSource.uri(uri))
|
||||
binding.ssiv.setImage(ImageSource.Uri(uri))
|
||||
}
|
||||
|
||||
override fun onImageShowing(zoom: ZoomMode) {
|
||||
override fun onImageShowing(settings: ReaderSettings) {
|
||||
binding.ssiv.maxScale = 2f * maxOf(
|
||||
binding.ssiv.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.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
binding.ssiv.resetScaleAndCenter()
|
||||
}
|
||||
ZoomMode.FIT_HEIGHT -> {
|
||||
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
|
||||
binding.ssiv.setScaleAndCenter(
|
||||
binding.ssiv.minScale,
|
||||
@@ -85,7 +83,7 @@ open class PageHolder(
|
||||
)
|
||||
}
|
||||
ZoomMode.FIT_WIDTH -> {
|
||||
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
|
||||
binding.ssiv.setScaleAndCenter(
|
||||
binding.ssiv.minScale,
|
||||
@@ -93,7 +91,7 @@ open class PageHolder(
|
||||
)
|
||||
}
|
||||
ZoomMode.KEEP_START -> {
|
||||
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
binding.ssiv.setScaleAndCenter(
|
||||
binding.ssiv.maxScale,
|
||||
PointF(0f, 0f),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
||||
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,17 +19,18 @@ 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 {
|
||||
|
||||
private var scrollToRestore = 0
|
||||
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
|
||||
|
||||
init {
|
||||
binding.ssiv.setOnImageEventListener(delegate)
|
||||
binding.ssiv.regionDecoderFactory = SkiaPooledImageRegionDecoder.Factory()
|
||||
binding.ssiv.addOnImageEventListener(delegate)
|
||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||
GoneOnInvisibleListener(bindingInfo.progressBar).attach()
|
||||
}
|
||||
|
||||
override fun onBind(data: ReaderPage) {
|
||||
@@ -41,6 +42,16 @@ class WebtoonHolder(
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
goneOnInvisibleListener.attach()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
goneOnInvisibleListener.detach()
|
||||
}
|
||||
|
||||
override fun onLoadingStarted() {
|
||||
bindingInfo.layoutError.isVisible = false
|
||||
bindingInfo.progressBar.showCompat()
|
||||
@@ -57,12 +68,13 @@ class WebtoonHolder(
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri) {
|
||||
binding.ssiv.setImage(ImageSource.uri(uri))
|
||||
binding.ssiv.setImage(ImageSource.Uri(uri))
|
||||
}
|
||||
|
||||
override fun onImageShowing(zoom: ZoomMode) {
|
||||
override fun onImageShowing(settings: ReaderSettings) {
|
||||
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
||||
with(binding.ssiv) {
|
||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
minScale = width / sWidth.toFloat()
|
||||
maxScale = minScale
|
||||
scrollTo(
|
||||
@@ -70,7 +82,7 @@ class WebtoonHolder(
|
||||
scrollToRestore != 0 -> scrollToRestore
|
||||
itemView.top < 0 -> getScrollRange()
|
||||
else -> 0
|
||||
}
|
||||
},
|
||||
)
|
||||
scrollToRestore = 0
|
||||
}
|
||||
@@ -89,7 +101,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 +116,4 @@ class WebtoonHolder(
|
||||
scrollToRestore = scroll
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.utils.ext.parents
|
||||
|
||||
private const val SCROLL_UNKNOWN = -1
|
||||
@@ -23,11 +20,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
private var scrollPos = 0
|
||||
private var scrollRange = SCROLL_UNKNOWN
|
||||
|
||||
init {
|
||||
setExecutor(Dispatchers.Default.asExecutor())
|
||||
setEagerLoadingEnabled(!isLowRamDevice(context))
|
||||
}
|
||||
|
||||
fun scrollBy(delta: Int) {
|
||||
val maxScroll = getScrollRange()
|
||||
if (maxScroll == 0) {
|
||||
@@ -113,4 +105,4 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
private fun parentHeight(): Int {
|
||||
return parents.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,13 +5,13 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.list.ui.adapter.*
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class MultiSearchAdapter(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
@@ -34,11 +34,11 @@ class MultiSearchAdapter(
|
||||
selectionDecoration = selectionDecoration,
|
||||
listener = listener,
|
||||
itemClickListener = itemClickListener,
|
||||
)
|
||||
),
|
||||
)
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyStateListAD(listener))
|
||||
.addDelegate(emptyStateListAD(coil, listener))
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class OnboardDialogFragment :
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
super.onBuildDialog(builder)
|
||||
.setPositiveButton(R.string.done, this)
|
||||
.setCancelable(true)
|
||||
.setCancelable(false)
|
||||
if (isWelcome) {
|
||||
builder.setTitle(R.string.welcome)
|
||||
} else {
|
||||
|
||||
@@ -11,8 +11,6 @@ import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.*
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.pow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -21,12 +19,16 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.utils.ext.asArrayList
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.trySetForeground
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.pow
|
||||
|
||||
@HiltWorker
|
||||
class SuggestionsWorker @AssistedInject constructor(
|
||||
@@ -94,14 +96,10 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
||||
val rawResults = coroutineScope {
|
||||
tagsBySources.flatMap { (source, tags) ->
|
||||
val repo = mangaRepositoryFactory.create(source)
|
||||
val repo = mangaRepositoryFactory.tryCreate(source) ?: return@flatMap emptyList()
|
||||
tags.map { tag ->
|
||||
async(dispatcher) {
|
||||
repo.getList(
|
||||
offset = 0,
|
||||
sortOrder = SortOrder.UPDATED,
|
||||
tags = setOf(tag),
|
||||
)
|
||||
repo.getListSafe(tag)
|
||||
}
|
||||
}
|
||||
}.awaitAll().flatten().asArrayList()
|
||||
@@ -139,6 +137,18 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
return (weight / maxWeight).pow(2.0).toFloat()
|
||||
}
|
||||
|
||||
private suspend fun MangaRepository.getListSafe(tag: MangaTag) = runCatching {
|
||||
getList(offset = 0, sortOrder = SortOrder.UPDATED, tags = setOf(tag))
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.getOrDefault(emptyList())
|
||||
|
||||
private fun MangaRepository.Factory.tryCreate(source: MangaSource) = runCatching {
|
||||
create(source)
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "suggestions"
|
||||
|
||||
@@ -4,11 +4,11 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.list.ui.adapter.*
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class FeedAdapter(
|
||||
coil: ImageLoader,
|
||||
@@ -23,7 +23,7 @@ class FeedAdapter(
|
||||
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
|
||||
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
|
||||
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, listener))
|
||||
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,10 @@ class GoneOnInvisibleListener(
|
||||
|
||||
fun attach() {
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||||
onGlobalLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import java.io.Serializable
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? {
|
||||
if (!containsKey(key)) {
|
||||
return null
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelable(key, T::class.java)
|
||||
} else {
|
||||
@@ -17,6 +20,9 @@ inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T?
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? {
|
||||
if (!hasExtra(key)) {
|
||||
return null
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(key, T::class.java)
|
||||
} else {
|
||||
@@ -26,6 +32,9 @@ inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String): T? {
|
||||
if (!containsKey(key)) {
|
||||
return null
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getSerializable(key, T::class.java)
|
||||
} else {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ 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)
|
||||
@@ -22,7 +26,7 @@ val Fragment.viewLifecycleScope
|
||||
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
|
||||
}
|
||||
@@ -41,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.res.Resources
|
||||
import androidx.collection.arraySetOf
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import okio.FileNotFoundException
|
||||
@@ -23,24 +24,31 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
||||
is FileNotFoundException -> resources.getString(R.string.file_not_found)
|
||||
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
|
||||
is SyncApiException,
|
||||
is ContentUnavailableException, -> message
|
||||
is ContentUnavailableException,
|
||||
-> message
|
||||
is ParseException -> shortMessage
|
||||
is UnknownHostException,
|
||||
is SocketTimeoutException, -> resources.getString(R.string.network_error)
|
||||
is SocketTimeoutException,
|
||||
-> resources.getString(R.string.network_error)
|
||||
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
||||
is NotFoundException -> resources.getString(R.string.not_found_404)
|
||||
else -> localizedMessage
|
||||
} ?: resources.getString(R.string.error_occurred)
|
||||
|
||||
fun Throwable.isReportable(): Boolean {
|
||||
if (this !is Exception) {
|
||||
return true
|
||||
}
|
||||
return this is ParseException || this is IllegalArgumentException ||
|
||||
this is IllegalStateException || this.javaClass == RuntimeException::class.java
|
||||
return this is Error || this.javaClass in reportableExceptions
|
||||
}
|
||||
|
||||
fun Throwable.report(message: String?) {
|
||||
val exception = CaughtException(this, message)
|
||||
exception.sendWithAcra()
|
||||
}
|
||||
|
||||
private val reportableExceptions = arraySetOf<Class<*>>(
|
||||
ParseException::class.java,
|
||||
RuntimeException::class.java,
|
||||
IllegalStateException::class.java,
|
||||
IllegalArgumentException::class.java,
|
||||
ConcurrentModificationException::class.java,
|
||||
UnsupportedOperationException::class.java,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
@@ -22,6 +23,7 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter
|
||||
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShelfConfigActivity :
|
||||
BaseActivity<ActivityCategoriesBinding>(),
|
||||
OnListItemClickListener<CategoryItem>,
|
||||
|
||||
@@ -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>
|
||||
|
||||
154
app/src/main/res/layout/activity_color_filter.xml
Normal file
154
app/src/main/res/layout/activity_color_filter.xml
Normal 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>
|
||||
@@ -23,6 +23,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:scrollIndicators="top|bottom"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_source_locale" />
|
||||
|
||||
@@ -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" />
|
||||
@@ -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"
|
||||
|
||||
17
app/src/main/res/menu/popup_order.xml
Normal file
17
app/src/main/res/menu/popup_order.xml
Normal 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>
|
||||
@@ -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,69 @@
|
||||
<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 :<br><tt>%1$s</tt><br><br>1. Essayez de <a href=%2$s>ouvrir le manga dans un navigateur web</a> pour vous assurer qu\'il est disponible sur sa source<br>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>
|
||||
<string name="text_unsaved_changes_prompt">Vous avez des modifications non sauvegardées, voulez-vous les sauvegarder ou les abandonner \?</string>
|
||||
<string name="discard">Abandonner</string>
|
||||
<string name="history_shortcuts_summary">Rendre les mangas récents disponibles en appuyant longuement sur l\'icône de l\'application</string>
|
||||
<string name="reader_control_ltr_summary">Taper sur le bord droit ou appuyer sur la touche droite permet toujours de passer à la page suivante</string>
|
||||
<string name="reader_control_ltr">Contrôle ergonomique du lecteur</string>
|
||||
<string name="history_shortcuts">Afficher les raccourcis des mangas récents</string>
|
||||
<string name="color_correction">Correction des couleurs</string>
|
||||
<string name="brightness">Luminosité</string>
|
||||
<string name="contrast">Contraste</string>
|
||||
<string name="reset">Réinitialiser</string>
|
||||
<string name="color_correction_hint">Les paramètres de couleurs choisis seront sauvegardés pour ce manga</string>
|
||||
</resources>
|
||||
@@ -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:<br><tt>%1$s</tt><br><br>1. Coba untuk <a href=%2$s>membuka manga di peramban web</a> untuk memastikan bahwa itu tersedia di sumbernya<br>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>
|
||||
@@ -374,4 +374,5 @@
|
||||
<string name="comics_archive">コミックアーカイブ</string>
|
||||
<string name="downloading_manga">漫画をダウンロードする</string>
|
||||
<string name="feed">フィード</string>
|
||||
<string name="manga_error_description_pattern">エラーの詳細:<br><tt>%1$s</tt><br><br>1. <a href=%2$s>Webブラウザで漫画を開いてみて</a>、そのソースで利用できるか確認する<br>2. 利用できる場合は、開発者にエラーレポートを送信する</a>。</string>
|
||||
</resources>
|
||||
@@ -247,7 +247,7 @@
|
||||
<string name="data_restored_success">Todos os dados foram restaurados</string>
|
||||
<string name="protect_application">Proteja o aplicativo</string>
|
||||
<string name="updates_feed_cleared">Limpo</string>
|
||||
<string name="right_to_left">Da direita para a esquerda (←)</string>
|
||||
<string name="right_to_left">Da direita para a esquerda</string>
|
||||
<string name="scale_mode">Modo de escala</string>
|
||||
<string name="zoom_mode_fit_height">Ajustar à altura</string>
|
||||
<string name="black_dark_theme">Preto</string>
|
||||
@@ -296,4 +296,93 @@
|
||||
<string name="detect_reader_mode_summary">Detectar automaticamente se o mangá é webtoon</string>
|
||||
<string name="disable_battery_optimization">Desative a otimização da bateria</string>
|
||||
<string name="disable_battery_optimization_summary">Ajuda com verificações de atualizações em segundo plano</string>
|
||||
<string name="invalid_domain_message">Endereço inválido</string>
|
||||
<string name="use_fingerprint">Usar impressão digital, se disponível</string>
|
||||
<string name="appwidget_shelf_description">Mangás dos seus favoritos</string>
|
||||
<string name="appwidget_recent_description">Seus mangás recentemente lidos</string>
|
||||
<string name="status_completed">Concluído</string>
|
||||
<string name="status_on_hold">Em espera</string>
|
||||
<string name="status_dropped">Desistido</string>
|
||||
<string name="text_shelf_holder_primary">Seus mangás serão exibidos aqui</string>
|
||||
<string name="text_shelf_holder_secondary">Encontre o que ler na seção <«Explorar»</string>
|
||||
<string name="canceled">Cancelado</string>
|
||||
<string name="account_already_exists">Essa conta já existe</string>
|
||||
<string name="back">Voltar</string>
|
||||
<string name="sync">Sincronização</string>
|
||||
<string name="sync_title">Sincronize seus dados</string>
|
||||
<string name="email_enter_hint">Digite seu e-mail para continuar</string>
|
||||
<string name="tracking">Monitoramento</string>
|
||||
<string name="logout">Sair</string>
|
||||
<string name="crash_text">Algo deu errado. Por favor, envie um relatório de bug para ajudar os desenvolvedores a consertarem isso.</string>
|
||||
<string name="send">Enviar</string>
|
||||
<string name="status_planned">Planejado</string>
|
||||
<string name="status_reading">Lendo</string>
|
||||
<string name="status_re_reading">Relendo</string>
|
||||
<string name="report">Reportar</string>
|
||||
<string name="clear_cookies_summary">Pode ajudar no caso de alguns problemas. Todas as autorizações serão invalidadas</string>
|
||||
<string name="show_reading_indicators">Mostrar indicadores de progresso de leitura</string>
|
||||
<string name="data_deletion">Exclusão de dados</string>
|
||||
<string name="show_reading_indicators_summary">Mostrar percentual de leitura no histórico e nos favoritos</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Mangás marcados como +18 nunca serão adicionados ao histórico e seu progresso não sera salvo</string>
|
||||
<string name="show_all">Mostrar tudo</string>
|
||||
<string name="clear_all_history">Limpar todo o histórico</string>
|
||||
<string name="last_2_hours">Ultimas 2 horas</string>
|
||||
<string name="history_cleared">Histórico apagado</string>
|
||||
<string name="manage">Gerenciar</string>
|
||||
<string name="no_bookmarks_yet">Sem páginas marcadas ainda</string>
|
||||
<string name="no_bookmarks_summary">Você pode criar um marcador de página enquanto lé o mangá</string>
|
||||
<string name="bookmarks_removed">Marcadores de página removidos</string>
|
||||
<string name="no_manga_sources">Sem fontes de mangás</string>
|
||||
<string name="select_range">Selecionar intervalo</string>
|
||||
<string name="disable_all">Desabilitar tudo</string>
|
||||
<string name="no_chapters">Sem capítulos</string>
|
||||
<string name="manga_error_description_pattern">Detalhes do erro:<br><tt>%1$s</tt><br><br>1. Tente <a href=%2$s>abrir o mangá em um navegador de internet</a>para garantir que ele está disponível na fonte<br>2. Se estiver disponível, envie um relatório de erro para os desenvolvedores.</string>
|
||||
<string name="feed">Feed</string>
|
||||
<string name="history_shortcuts_summary">Torne os mangás recentes visíveis pressionando o ícone do aplicativo</string>
|
||||
<string name="no_manga_sources_text">Habilite as fontes de maná para ler online</string>
|
||||
<string name="random">Aleatório</string>
|
||||
<string name="categories_delete_confirm">Tem certeza que deseja excluir as categorias favoritas selecionadas\?
|
||||
\nTodos os mangás serão perdidos e isso não pode ser desfeito.</string>
|
||||
<string name="reorder">Reordenar</string>
|
||||
<string name="empty">Vazio</string>
|
||||
<string name="changelog">Registro de alterações</string>
|
||||
<string name="explore">Explorar</string>
|
||||
<string name="confirm_exit">Pressione Voltar novamente para sair</string>
|
||||
<string name="exit_confirmation_summary">Pressione Voltar duas vezes para sair do app</string>
|
||||
<string name="exit_confirmation">Confirmação de saída</string>
|
||||
<string name="saved_manga">Mangás salvos</string>
|
||||
<string name="pages_cache">Cache de páginas</string>
|
||||
<string name="other_cache">Outro cache</string>
|
||||
<string name="storage_usage">Uso de armazenamento</string>
|
||||
<string name="available">Disponível</string>
|
||||
<string name="enter_email_text">Digite seu e-mail para continuar</string>
|
||||
<string name="removed_from_favourites">Removido dos favoritos</string>
|
||||
<string name="removed_from_s">Removido de \"%s\"</string>
|
||||
<string name="options">Opções</string>
|
||||
<string name="downloading_manga">Baixando mangá</string>
|
||||
<string name="incognito_mode">Modo anônimo</string>
|
||||
<string name="app_update_available_s">Atualização do aplicativo disponível: %s</string>
|
||||
<string name="automatic_scroll">Rolagem automática</string>
|
||||
<string name="off_short">Desligado</string>
|
||||
<string name="reader_info_pattern">Cap. %1$d/%2$d Pág. %3$d/%4$d</string>
|
||||
<string name="reader_info_bar">Mostrar barra de informações no leitor</string>
|
||||
<string name="folder_with_images">Pasta com imagens</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
<string name="seconds_pattern">%ss</string>
|
||||
<string name="comics_archive">Arquivo de quadrinhos</string>
|
||||
<string name="importing_manga">Importando mangá(s)</string>
|
||||
<string name="import_completed">Importação completa</string>
|
||||
<string name="import_completed_hint">Você pode excluir o arquivo original do dispositivo para salvar espaço</string>
|
||||
<string name="import_will_start_soon">A importação começará em breve</string>
|
||||
<string name="history_shortcuts">Mostrar atalhos de mangás recentes</string>
|
||||
<string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita sempre passa para a próxima página</string>
|
||||
<string name="reader_control_ltr">Controle de leitura ergonômico</string>
|
||||
<string name="color_correction">Correção de cor</string>
|
||||
<string name="brightness">Luminosidade</string>
|
||||
<string name="contrast">Contraste</string>
|
||||
<string name="reset">Redefinir</string>
|
||||
<string name="color_correction_hint">As configurações de cor escolhidas serão lembradas para esse mangá</string>
|
||||
<string name="text_unsaved_changes_prompt">Você tem alterações não salvas, deseja salvá-las ou descartá-las\?</string>
|
||||
<string name="discard">Descartar</string>
|
||||
<string name="not_found_404">Conteúdo não encontrado ou removido</string>
|
||||
</resources>
|
||||
@@ -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">Информация об ошибке:<br><tt>%1$s</tt><br><br>1. Попробуйте <a href=%2$s>открыть мангу в веб-браузере</a>, чтобы убедиться, что она доступна в источнике<br>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>
|
||||
@@ -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,66 @@
|
||||
<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>
|
||||
<string name="removed_from_favourites">Favorilerden kaldırıldı</string>
|
||||
<string name="downloading_manga">Manga indiriliyor</string>
|
||||
<string name="changelog">Değişiklik günlüğü</string>
|
||||
<string name="exit_confirmation">Çıkış doğrulaması</string>
|
||||
<string name="enter_email_text">Devam etmek için e-postanızı girin</string>
|
||||
<string name="removed_from_s">\"%s\"dan kaldırıldı</string>
|
||||
<string name="comics_archive">Çizgi roman arşivi</string>
|
||||
<string name="manga_error_description_pattern">Hata ayrıntıları:<br><tt>%1$s</tt><br><br>1.Mangayı <a href=%2$s>kaynağında mevcut olduğuna emin olmak için</a> bir web tarayıcısında açın<br>2. Mevcutsa, geliştiricilere bir hata reporu gönderin.</string>
|
||||
<string name="text_shelf_holder_secondary">«Keşfet» kısmında neler okuyacağınızı bulun</string>
|
||||
<string name="categories_delete_confirm">Seçilen favori kategorileri silmek istediğinizden emin misiniz\?
|
||||
\nİçindeki tüm mangalar kaybolur ve bu işlem geri alınamaz.</string>
|
||||
<string name="reorder">Yeniden sırala</string>
|
||||
<string name="pages_cache">Sayfa önbelleği</string>
|
||||
<string name="other_cache">Diğer önbellekler</string>
|
||||
<string name="storage_usage">Depolama kullanımı</string>
|
||||
<string name="available">Mevcut</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
<string name="options">Seçenekler</string>
|
||||
<string name="incognito_mode">Gizli mod</string>
|
||||
<string name="app_update_available_s">Uygulama güncellemesi mevcut: %s</string>
|
||||
<string name="no_chapters">Bölüm yok</string>
|
||||
<string name="automatic_scroll">Otomatik kaydır</string>
|
||||
<string name="off_short">Kapalı</string>
|
||||
<string name="seconds_pattern">%ss</string>
|
||||
<string name="reader_info_pattern">Böl. %1$d/%2$d Sayf. %3$d/%4$d</string>
|
||||
<string name="reader_info_bar">Okuyucuda bilgi çubuğu göster</string>
|
||||
<string name="folder_with_images">Resimlerle klasör</string>
|
||||
<string name="importing_manga">Manga içe aktarılıyor</string>
|
||||
<string name="import_completed">İçe aktarım tamamlandı</string>
|
||||
<string name="import_completed_hint">Yer açmak için orijinal dosyayı depolamadan silebilirsiniz</string>
|
||||
<string name="import_will_start_soon">İçe aktarım birazdan başlayacak</string>
|
||||
<string name="feed">Feed</string>
|
||||
<string name="history_shortcuts">En son manga kısayollarını göster</string>
|
||||
<string name="reader_control_ltr">Ergonomik okuyucu kontrol</string>
|
||||
<string name="color_correction">Renk düzeltme</string>
|
||||
<string name="brightness">Parlaklık</string>
|
||||
<string name="contrast">Kontrast</string>
|
||||
<string name="reset">Sıfırla</string>
|
||||
<string name="color_correction_hint">Seçilen renk ayarları bu manga için hatırlanacaktır</string>
|
||||
<string name="text_unsaved_changes_prompt">Kaydedilmemiş değişiklikleriniz var, kaydetmek mi istersiniz yoksa, yoksaymak mı istersiniz\?</string>
|
||||
<string name="discard">Yoksay</string>
|
||||
</resources>
|
||||
@@ -373,4 +373,16 @@
|
||||
<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">Деталі помилки:<br><tt>%1$s</tt><br><br>1. Спробуйте <a href=%2$s>відкрити манґу у веб-браузері</a>, щоб переконатися, що вона доступна в джерелі<br>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="brightness">Яскравість</string>
|
||||
<string name="color_correction">Корекція кольору</string>
|
||||
<string name="contrast">Контрастність</string>
|
||||
<string name="reset">Скинути</string>
|
||||
<string name="color_correction_hint">Вибрані параметри кольору будуть запам\'ятовані для цієї манґи</string>
|
||||
<string name="text_unsaved_changes_prompt">У вас є незбережені зміни, ви хочете зберегти чи скасувати їх\?</string>
|
||||
<string name="discard">Скасувати</string>
|
||||
</resources>
|
||||
27
app/src/main/res/values-zh-rCN/plurals.xml
Normal file
27
app/src/main/res/values-zh-rCN/plurals.xml
Normal 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>
|
||||
@@ -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,67 @@
|
||||
<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">错误详情:<br><tt>%1$s</tt><br><br>1.尝试<a href=%2$s>在网络浏览器中打开漫画</a>以确保在其来源中可用<br>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>
|
||||
<string name="reader_control_ltr_summary">点击屏幕右侧或按下右键翻到下一页</string>
|
||||
<string name="reader_control_ltr">高效阅读器控制</string>
|
||||
</resources>
|
||||
@@ -6,7 +6,7 @@
|
||||
<string name="url_twitter" translatable="false">https://twitter.com/kotatsuapp</string>
|
||||
<string name="url_reddit" translatable="false">https://reddit.com/user/kotatsuapp</string>
|
||||
<string name="url_weblate" translatable="false">https://hosted.weblate.org/engage/kotatsu</string>
|
||||
<string name="url_error_report" translatable="false">http://86.57.183.214:8082/report</string>
|
||||
<string name="url_error_report" translatable="false">https://acra.rumblur.space/report</string>
|
||||
<string name="account_type_sync" translatable="false">org.kotatsu.sync</string>
|
||||
<string name="url_sync_server" translatable="false">http://86.57.183.214:8081</string>
|
||||
<string-array name="values_theme" translatable="false">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user