Compare commits

..

48 Commits

Author SHA1 Message Date
734765dbdd Fix suggestions worker processing broken tags 2022-09-14 14:09:02 +03:00
Marcos Souza
117c4c5978 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 87.3% (339 of 388 strings)

Co-authored-by: Marcos Souza <marcossouzacg2004@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
kyoya
13e33a6614 Translated using Weblate (Turkish)
Currently translated at 99.2% (385 of 388 strings)

Co-authored-by: kyoya <thelol9181@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
Dpper
affb495136 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
demo_101
9b2dafd668 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.6% (379 of 388 strings)

Co-authored-by: demo_101 <sleepy@nixnet.email>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
5f32c0401f Update ssiv 2022-09-14 13:51:11 +03:00
f35f40ed27 Merge branch 'master' into devel 2022-09-14 12:58:11 +03:00
46f0d3ef74 Fix onboarding dialog 2022-09-14 12:34:33 +03:00
c27c785ac2 Use Coil for empty states images 2022-09-14 12:30:07 +03:00
6e844e8c3b Fix library selection #224 2022-09-12 16:24:14 +03:00
4186c36f30 Prevent GoneOnInvisibleListener leak 2022-09-12 15:45:34 +03:00
2d727a0da8 Prevent GoneOnInvisibleListener leak 2022-09-12 15:43:42 +03:00
39e574e9dc Update dependencies 2022-09-12 15:40:37 +03:00
efb94cbd67 Fix unwanted errors reporting 2022-09-12 14:34:07 +03:00
757e33dfb4 Fix unwanted errors reporting 2022-09-12 14:32:42 +03:00
ab9bdf9f07 Fixes 2022-09-11 09:11:35 +03:00
a68632a888 Update SubsamplingScaleImageView 2022-09-10 16:47:50 +03:00
9b0dc8b413 Fix hiding FastScroller 2022-09-08 15:53:16 +03:00
f43fe5830e Fix crashes 2022-09-08 15:39:12 +03:00
ce616b328c Update dependencies 2022-09-08 14:46:27 +03:00
824a8ff97a Merge branch 'master' into devel 2022-09-08 14:42:01 +03:00
no
ef9018b92f Translated using Weblate (Turkish)
Currently translated at 90.2% (350 of 388 strings)

Co-authored-by: no <cbasroyalasroyal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-09-08 14:38:29 +03:00
J. Lavoie
a088d10935 Translated using Weblate (French)
Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2022-09-08 14:38:29 +03:00
2e561697ac Update parsers 2022-09-07 14:07:48 +03:00
d242acd502 Sort local list by manga name 2022-09-03 15:57:48 +03:00
d37b44d3f6 Update parsers 2022-09-03 15:56:16 +03:00
6243fc88c9 Sorting local manga #219 #217 2022-09-03 15:45:06 +03:00
Neko Nekowazarashi
f74e865b06 Translated using Weblate (Indonesian)
Currently translated at 91.7% (356 of 388 strings)

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

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

Translated using Weblate (Russian)

Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (377 of 377 strings)

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

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Added translation using Weblate (Chinese (Simplified))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

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

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

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

6
.idea/ktlint.xml generated
View File

@@ -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>

View File

@@ -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'

View File

@@ -13,3 +13,4 @@
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment

View File

@@ -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"

View File

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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -33,6 +33,7 @@ class ScrollbarAnimator(
.animate()
.translationX(0f)
.alpha(1f)
.setListener(null)
.setDuration(animationDuration)
}

View File

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

View File

@@ -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(
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}
}

View File

@@ -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,

View File

@@ -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) },

View File

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

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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(
}
}
}
}
}

View File

@@ -14,6 +14,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.R as materialR
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
@@ -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)

View File

@@ -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
}
}

View File

@@ -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()
}

View File

@@ -40,7 +40,7 @@ class LibraryAdapter(
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyStateListAD(listener))
.addDelegate(emptyStateListAD(coil, listener))
.addDelegate(errorStateListAD(listener))
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -40,7 +40,7 @@ fun mangaListItemAD(
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
itemView.bindBadge(badge, item.counter)
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {

View File

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

View File

@@ -6,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)

View File

@@ -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()

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ class DirMangaImporter(
)
it.sortChaptersByName()
it.mergeWithExisting()
it.finalize()
it.finish()
it.file
}
return localMangaRepository.getFromFile(dest)

View File

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

View File

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

View File

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

View File

@@ -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)
}
}
}

View File

@@ -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(),

View File

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

View File

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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,151 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.ViewSizeResolver
import com.google.android.material.R as materialR
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.utils.ext.*
@AndroidEntryPoint
class ColorFilterConfigActivity :
BaseActivity<ActivityColorFilterBinding>(),
Slider.OnChangeListener,
View.OnClickListener {
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var viewModelFactory: ColorFilterConfigViewModel.Factory
private val viewModel: ColorFilterConfigViewModel by assistedViewModels {
viewModelFactory.create(
manga = checkNotNull(intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga),
page = checkNotNull(intent.getParcelableExtraCompat<ParcelableMangaPages>(EXTRA_PAGES)?.pages?.firstOrNull()),
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityColorFilterBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
binding.sliderBrightness.addOnChangeListener(this)
binding.sliderContrast.addOnChangeListener(this)
val formatter = PercentLabelFormatter(resources)
binding.sliderContrast.setLabelFormatter(formatter)
binding.sliderBrightness.setLabelFormatter(formatter)
binding.buttonDone.setOnClickListener(this)
binding.buttonReset.setOnClickListener(this)
onBackPressedDispatcher.addCallback(ColorFilterConfigBackPressedDispatcher(this, viewModel))
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
viewModel.isLoading.observe(this, this::onLoadingChanged)
viewModel.preview.observe(this, this::onPreviewChanged)
viewModel.onDismiss.observe(this) {
finishAfterTransition()
}
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
when (slider.id) {
R.id.slider_brightness -> viewModel.setBrightness(value)
R.id.slider_contrast -> viewModel.setContrast(value)
}
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> viewModel.save()
R.id.button_reset -> viewModel.reset()
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
binding.scrollView.updatePadding(
bottom = insets.bottom,
)
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) {
binding.sliderBrightness.value = readerColorFilter?.brightness ?: 0f
binding.sliderContrast.value = readerColorFilter?.contrast ?: 0f
binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
}
private fun onPreviewChanged(preview: MangaPage?) {
if (preview == null) return
ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(preview.url)
.referer(preview.referer)
.scale(Scale.FILL)
.decodeRegion()
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(binding.imageViewBefore))
.allowRgb565(false)
.target(ShadowViewTarget(binding.imageViewBefore, binding.imageViewAfter))
.enqueueWith(coil)
}
private fun onLoadingChanged(isLoading: Boolean) {
binding.sliderContrast.isEnabled = !isLoading
binding.sliderBrightness.isEnabled = !isLoading
binding.buttonDone.isEnabled = !isLoading
}
private class PercentLabelFormatter(resources: Resources) : LabelFormatter {
private val pattern = resources.getString(R.string.percent_string_pattern)
override fun getFormattedValue(value: Float): String {
val percent = ((value + 1f) * 100).format(0)
return pattern.format(percent)
}
}
companion object {
private const val EXTRA_PAGES = "pages"
private const val EXTRA_MANGA = "manga_id"
fun newIntent(context: Context, manga: Manga, page: MangaPage) =
Intent(context, ColorFilterConfigActivity::class.java)
.putExtra(EXTRA_MANGA, ParcelableManga(manga, false))
.putExtra(EXTRA_PAGES, ParcelableMangaPages(listOf(page)))
}
}

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
import org.koitharu.kotatsu.reader.ui.PageSaveContract
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -59,6 +60,7 @@ class ReaderConfigBottomSheet :
binding.buttonSavePage.setOnClickListener(this)
binding.buttonScreenRotate.setOnClickListener(this)
binding.buttonSettings.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnSliderTouchListener(this)
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
@@ -80,6 +82,11 @@ class ReaderConfigBottomSheet :
R.id.button_screen_rotate -> {
orientationHelper?.toggleOrientation()
}
R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return
val manga = viewModel.manga ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
}
}
}

View File

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

View File

@@ -5,15 +5,15 @@ import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
@@ -37,8 +37,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()
}
}
}

View File

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

View File

@@ -2,26 +2,26 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.net.Uri
import androidx.core.net.toUri
import 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)
}
}
}

View File

@@ -6,16 +6,16 @@ import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
class ReversedPageHolder(
binding: ItemPageBinding,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
settings: ReaderSettings,
exceptionResolver: ExceptionResolver,
) : PageHolder(binding, loader, settings, exceptionResolver) {
init {
@@ -23,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),
)
}
}
}
}
}
}

View File

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

View File

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

View File

@@ -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),

View File

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

View File

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

View File

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

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}

View File

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

View File

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

View File

@@ -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))
}

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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())
}

View File

@@ -19,5 +19,10 @@ class GoneOnInvisibleListener(
fun attach() {
view.viewTreeObserver.addOnGlobalLayoutListener(this)
onGlobalLayout()
}
}
fun detach() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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)
}
}

View File

@@ -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,
)

View File

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

View File

@@ -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>,

View File

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

View File

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

View File

@@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &lt;«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:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Tente &lt;a href=%2$s&gt;abrir o mangá em um navegador de internet&lt;/a&gt;para garantir que ele está disponível na fonte&lt;br&gt;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>

View File

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

View File

@@ -16,7 +16,7 @@
<string name="try_again">Tekrar dene</string>
<string name="clear_history">Geçmişi temizle</string>
<string name="nothing_found">Hiçbir şey bulunamadı</string>
<string name="history_is_empty">Sen geçmişiniz yok</string>
<string name="history_is_empty">Geçmiş yok</string>
<string name="read">Oku</string>
<string name="you_have_not_favourites_yet">Henüz favorileriniz yok</string>
<string name="add_to_favourites">Favoriniz</string>
@@ -209,7 +209,7 @@
<string name="new_version_s">Yeni sürüm: %s</string>
<string name="protect_application">Uygulamayı koru</string>
<string name="passwords_mismatch">Parolalar eşleşmiyor</string>
<string name="right_to_left">Sağdan sola (←)</string>
<string name="right_to_left">Sağdan-sola</string>
<string name="create_category">Yeni kategori</string>
<string name="file_not_found">Dosya bulunamadı</string>
<string name="data_restored_success">Tüm veriler geri yüklendi</string>
@@ -320,4 +320,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ı:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1.Mangayı &lt;a href=%2$s&gt;kaynağında mevcut olduğuna emin olmak için&lt;/a&gt; bir web tarayıcısında açın&lt;br&gt;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>

View File

@@ -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">Деталі помилки:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Спробуйте &lt;a href=%2$s&gt;відкрити манґу у веб-браузері&lt;/a&gt;, щоб переконатися, що вона доступна в джерелі&lt;br&gt;2. Якщо вона доступна, то надішліть звіт про помилку розробникам.</string>
<string name="history_shortcuts">Показувати ярлики останньої прочитаної манґи</string>
<string name="history_shortcuts_summary">Зробити нещодавно прочитану манґу доступною за довгим натисканням на іконку програми</string>
<string name="reader_control_ltr_summary">Натискання на правий край або натискання правої клавіші завжди переходить на наступну сторінку</string>
<string name="reader_control_ltr">Ергономічне керування режимом читання</string>
<string name="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>

View File

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

View File

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

View File

@@ -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