Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be012f631a | ||
|
|
0165f43603 | ||
|
|
55801a1488 | ||
|
|
77103f016f | ||
|
|
6b6719a259 | ||
|
|
822642abb0 | ||
|
|
260745fb95 | ||
|
|
024ec0388f | ||
|
|
5345998eec | ||
|
|
3d56190e71 | ||
|
|
954431d0a5 | ||
|
|
afec63b443 | ||
|
|
ac5b29c35a | ||
|
|
59f5578b66 | ||
|
|
391dbb4237 | ||
|
|
7d4505eb78 | ||
|
|
e6ceb20cf7 | ||
|
|
8004f8c093 | ||
|
|
61bf2abb6c | ||
|
|
d9612f3427 | ||
|
|
435c3824f7 | ||
|
|
c846693570 | ||
|
|
123937cd01 | ||
|
|
9f56554313 | ||
|
|
f8687bb697 | ||
|
|
43d3a2cc6a | ||
|
|
a95db6ed21 | ||
|
|
fd0bb57338 | ||
|
|
6b94bc2632 | ||
|
|
c8b91599c6 | ||
|
|
3a8b0f9e93 | ||
|
|
17a0725666 | ||
|
|
3be7848ad9 | ||
|
|
08202c11a3 | ||
|
|
5ef907d046 | ||
|
|
c3776ea3c6 | ||
|
|
a624bffea3 | ||
|
|
8f38b4fe30 | ||
|
|
71a2de5358 | ||
|
|
5478f8fb59 | ||
|
|
5155c9a33d | ||
|
|
f7a461a9d8 | ||
|
|
3a02d22e02 | ||
|
|
2b8a29e2a6 | ||
|
|
bc68441585 | ||
|
|
1cc51b6a88 | ||
|
|
fd5aca7252 | ||
|
|
e447245fac | ||
|
|
5af0ee1c69 | ||
|
|
c02d1641ab | ||
|
|
f55c525c8a | ||
|
|
a42fc87a9a | ||
|
|
6b6905fd71 | ||
|
|
b7f57856db | ||
|
|
1d6d626b62 | ||
|
|
d93ff92cc9 | ||
|
|
8eda113f3b | ||
|
|
3916c2619e | ||
|
|
1d3e8e55ca | ||
|
|
2c3b4f29eb | ||
|
|
ee530002b6 | ||
|
|
59d530e0dc | ||
|
|
52a132caed | ||
|
|
379d2dd8d4 | ||
|
|
f8cefa3e8d | ||
|
|
5e1eda850c | ||
|
|
18cc0ad0fb | ||
|
|
11dd49c626 | ||
|
|
2ad8ab0258 | ||
|
|
4f8c5325a4 | ||
|
|
6e181a59a3 | ||
|
|
7a7d20dbf4 | ||
|
|
83d5f8e378 | ||
|
|
5ac9bad728 |
2
.idea/.gitignore
generated
vendored
2
.idea/.gitignore
generated
vendored
@@ -3,3 +3,5 @@
|
||||
/workspace.xml
|
||||
/migrations.xml
|
||||
/runConfigurations.xml
|
||||
/appInsightsSettings.xml
|
||||
/kotlinCodeInsightSettings.xml
|
||||
|
||||
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="PLACEHOLDER" />
|
||||
<option name="mobileSdkAppId" value="" />
|
||||
<option name="projectId" value="" />
|
||||
<option name="projectNumber" value="" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
</InsightsFilterSettings>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in online content sources.**
|
||||
|
||||
   [](https://github.com/KotatsuApp/kotatsu-parsers) [](https://hosted.weblate.org/engage/kotatsu/) [](https://discord.gg/NNJ5RgVBC5) [](https://t.me/kotatsuapp) [](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
|
||||
   [](https://github.com/KotatsuApp/kotatsu-parsers) [](https://hosted.weblate.org/engage/kotatsu/) [](https://discord.gg/NNJ5RgVBC5) [](https://t.me/kotatsuapp) [](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
|
||||
|
||||
### Download
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
versionCode = 1024
|
||||
versionName = '9.1'
|
||||
versionCode = 1030
|
||||
versionName = '9.2'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -87,6 +87,7 @@ android {
|
||||
'-opt-in=coil3.annotation.InternalCoilApi',
|
||||
'-opt-in=kotlinx.serialization.ExperimentalSerializationApi',
|
||||
'-Xjspecify-annotations=strict',
|
||||
'-Xannotation-default-target=first-only',
|
||||
'-Xtype-enhancement-improvements-strict-mode'
|
||||
]
|
||||
}
|
||||
|
||||
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@@ -8,8 +8,7 @@
|
||||
public static void checkParameterIsNotNull(...);
|
||||
public static void checkNotNullParameter(...);
|
||||
}
|
||||
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
@@ -17,8 +16,10 @@
|
||||
-dontwarn com.google.j2objc.annotations.**
|
||||
-dontwarn coil3.PlatformContext
|
||||
|
||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||
-keep class org.koitharu.kotatsu.settings.about.changelog.ChangelogFragment
|
||||
|
||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||
-keep class org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment { *; }
|
||||
-keep class org.jsoup.parser.Tag
|
||||
|
||||
@@ -41,8 +41,8 @@ class KotatsuApp : BaseApp() {
|
||||
detectNetwork()
|
||||
detectDiskWrites()
|
||||
detectCustomSlowCalls()
|
||||
detectResourceMismatches()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectUnbufferedIo()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) detectResourceMismatches()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) detectExplicitGc()
|
||||
penaltyLog()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.model.TestMangaSource
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.util.EnumSet
|
||||
|
||||
/*
|
||||
This class is for parser development and testing purposes
|
||||
You can open it in the app via Settings -> Debug
|
||||
*/
|
||||
class TestMangaRepository(
|
||||
@Suppress("unused") private val loaderContext: MangaLoaderContext,
|
||||
cache: MemoryContentCache
|
||||
) : CachingMangaRepository(cache) {
|
||||
|
||||
override val source = TestMangaSource
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = sortOrders.first()
|
||||
set(value) = Unit
|
||||
|
||||
override val filterCapabilities = MangaListFilterCapabilities()
|
||||
|
||||
override suspend fun getFilterOptions() = MangaListFilterOptions()
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
order: SortOrder?,
|
||||
filter: MangaListFilter?
|
||||
): List<Manga> = TODO("Get manga list by filter")
|
||||
|
||||
override suspend fun getDetailsImpl(
|
||||
manga: Manga
|
||||
): Manga = TODO("Fetch manga details")
|
||||
|
||||
override suspend fun getPagesImpl(
|
||||
chapter: MangaChapter
|
||||
): List<MangaPage> = TODO("Get pages for specific chapter")
|
||||
|
||||
override suspend fun getPageUrl(
|
||||
page: MangaPage
|
||||
): String = TODO("Return direct url of page image or page.url if it is already a direct url")
|
||||
|
||||
override suspend fun getRelatedMangaImpl(
|
||||
seed: Manga
|
||||
): List<Manga> = TODO("Get list of related manga. This method is optional and parser library has a default implementation")
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import androidx.preference.Preference
|
||||
import leakcanary.LeakCanary
|
||||
import org.koitharu.kotatsu.KotatsuApp
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.TestMangaSource
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
|
||||
import org.koitharu.workinspector.WorkInspector
|
||||
@@ -35,6 +37,11 @@ class DebugSettingsFragment : BasePreferenceFragment(R.string.debug), Preference
|
||||
true
|
||||
}
|
||||
|
||||
KEY_TEST_PARSER -> {
|
||||
router.openList(TestMangaSource, null, null)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
@@ -60,5 +67,6 @@ class DebugSettingsFragment : BasePreferenceFragment(R.string.debug), Preference
|
||||
|
||||
const val KEY_LEAK_CANARY = "leak_canary"
|
||||
const val KEY_WORK_INSPECTOR = "work_inspector"
|
||||
const val KEY_TEST_PARSER = "test_parser"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
|
||||
android:id="@+id/action_leakcanary"
|
||||
android:key="leak_canary"
|
||||
android:persistent="false"
|
||||
android:title="LeakCanary" />
|
||||
|
||||
<Preference
|
||||
android:id="@+id/action_works"
|
||||
android:key="work_inspector"
|
||||
android:persistent="false"
|
||||
android:title="@string/wi_lib_name" />
|
||||
|
||||
<Preference
|
||||
android:key="test_parser"
|
||||
android:persistent="false"
|
||||
android:title="@string/test_parser"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
android:backupAgent="org.koitharu.kotatsu.backups.domain.AppBackupAgent"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:enableOnBackInvokedCallback="@bool/is_predictive_back_enabled"
|
||||
android:extractNativeLibs="true"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:fullBackupOnly="true"
|
||||
android:hasFragileUserData="true"
|
||||
|
||||
@@ -17,6 +17,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase
|
||||
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase.NoAlternativesException
|
||||
import org.koitharu.kotatsu.core.ErrorReporterReceiver
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
@@ -47,7 +48,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
}
|
||||
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
@@ -58,7 +59,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
val result = runCatchingCancellable {
|
||||
autoFixUseCase.invoke(mangaId)
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
if (checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(startId, result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
@@ -67,7 +68,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
}
|
||||
|
||||
override fun IntentJobContext.onError(error: Throwable) {
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
if (checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = runBlocking { buildNotification(startId, Result.failure(error)) }
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
@@ -75,7 +76,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun startForeground(jobContext: IntentJobContext) {
|
||||
val title = applicationContext.getString(R.string.fixing_manga)
|
||||
val title = getString(R.string.fixing_manga)
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_MIN)
|
||||
.setName(title)
|
||||
.setShowBadge(false)
|
||||
@@ -85,7 +86,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
.build()
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setDefaults(0)
|
||||
@@ -97,7 +98,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.addAction(
|
||||
appcompatR.drawable.abc_ic_clear_material,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
getString(android.R.string.cancel),
|
||||
jobContext.getCancelIntent(),
|
||||
)
|
||||
.build()
|
||||
@@ -110,7 +111,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
}
|
||||
|
||||
private suspend fun buildNotification(startId: Int, result: Result<Pair<Manga, Manga?>>): Notification {
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setDefaults(0)
|
||||
.setSilent(true)
|
||||
@@ -119,17 +120,17 @@ class AutoFixService : CoroutineIntentService() {
|
||||
if (replacement != null) {
|
||||
notification.setLargeIcon(
|
||||
coil.execute(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
ImageRequest.Builder(this)
|
||||
.data(replacement.coverUrl)
|
||||
.mangaSourceExtra(replacement.source)
|
||||
.build(),
|
||||
).toBitmapOrNull(),
|
||||
)
|
||||
notification.setSubText(replacement.title)
|
||||
val intent = AppRouter.detailsIntent(applicationContext, replacement)
|
||||
val intent = AppRouter.detailsIntent(this, replacement)
|
||||
notification.setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
this,
|
||||
replacement.id.toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
@@ -143,35 +144,35 @@ class AutoFixService : CoroutineIntentService() {
|
||||
},
|
||||
)
|
||||
notification
|
||||
.setContentTitle(applicationContext.getString(R.string.fixed))
|
||||
.setContentTitle(getString(R.string.fixed))
|
||||
.setContentText(
|
||||
applicationContext.getString(
|
||||
getString(
|
||||
R.string.manga_replaced,
|
||||
seed.title,
|
||||
seed.source.getTitle(applicationContext),
|
||||
seed.source.getTitle(this),
|
||||
replacement.title,
|
||||
replacement.source.getTitle(applicationContext),
|
||||
replacement.source.getTitle(this),
|
||||
),
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_stat_done)
|
||||
} else {
|
||||
notification
|
||||
.setContentTitle(applicationContext.getString(R.string.fixing_manga))
|
||||
.setContentText(applicationContext.getString(R.string.no_fix_required, seed.title))
|
||||
.setContentTitle(getString(R.string.fixing_manga))
|
||||
.setContentText(getString(R.string.no_fix_required, seed.title))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
notification
|
||||
.setContentTitle(applicationContext.getString(R.string.error_occurred))
|
||||
.setContentTitle(getString(R.string.error_occurred))
|
||||
.setContentText(
|
||||
if (error is AutoFixUseCase.NoAlternativesException) {
|
||||
applicationContext.getString(R.string.no_alternatives_found, error.seed.manga.title)
|
||||
if (error is NoAlternativesException) {
|
||||
getString(R.string.no_alternatives_found, error.seed.manga.title)
|
||||
} else {
|
||||
error.getDisplayMessage(applicationContext.resources)
|
||||
error.getDisplayMessage(resources)
|
||||
},
|
||||
).setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
ErrorReporterReceiver.getNotificationAction(
|
||||
context = applicationContext,
|
||||
context = this,
|
||||
e = error,
|
||||
notificationId = startId,
|
||||
notificationTag = TAG,
|
||||
|
||||
@@ -13,6 +13,7 @@ import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.platform.PlatformRegistry
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.dialog
|
||||
@@ -79,6 +80,7 @@ open class BaseApp : Application(), Configuration.Provider {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
PlatformRegistry.applicationContext = this // TODO replace with OkHttp.initialize
|
||||
if (ACRA.isACRASenderServiceProcess()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.BadParcelableException
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
@@ -65,7 +64,7 @@ class ErrorReporterReceiver : BroadcastReceiver() {
|
||||
e: Throwable,
|
||||
notificationId: Int,
|
||||
notificationTag: String?,
|
||||
): PendingIntent? = try {
|
||||
): PendingIntent? = runCatching {
|
||||
val intent = Intent(context, ErrorReporterReceiver::class.java)
|
||||
intent.setAction(ACTION_REPORT)
|
||||
intent.setData("err://${e.hashCode()}".toUri())
|
||||
@@ -73,9 +72,9 @@ class ErrorReporterReceiver : BroadcastReceiver() {
|
||||
intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
intent.putExtra(EXTRA_NOTIFICATION_TAG, notificationTag)
|
||||
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
|
||||
} catch (e: BadParcelableException) {
|
||||
}.onFailure { e ->
|
||||
// probably cannot write exception as serializable
|
||||
e.printStackTraceDebug()
|
||||
null
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.collection.MutableScatterMap
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
@@ -43,6 +44,7 @@ import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.webview.WebViewExecutor
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
@@ -54,6 +56,7 @@ import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
import org.koitharu.kotatsu.parsers.util.mapToArray
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@@ -64,11 +67,13 @@ class CaptchaHandler @Inject constructor(
|
||||
@LocalizedAppContext private val context: Context,
|
||||
private val databaseProvider: Provider<MangaDatabase>,
|
||||
private val coilProvider: Provider<ImageLoader>,
|
||||
private val webViewExecutor: WebViewExecutor,
|
||||
) : EventListener() {
|
||||
|
||||
private val exceptionMap = MutableScatterMap<MangaSource, CloudFlareProtectedException>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
@CheckResult
|
||||
suspend fun handle(exception: CloudFlareException): Boolean = handleException(exception.source, exception, true)
|
||||
|
||||
suspend fun discard(source: MangaSource) {
|
||||
@@ -78,10 +83,18 @@ class CaptchaHandler @Inject constructor(
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
super.onError(request, result)
|
||||
val e = result.throwable
|
||||
if (e is CloudFlareException && request.extras[ignoreCaptchaKey] != true) {
|
||||
if (e is CloudFlareException) {
|
||||
val scope = request.lifecycle?.coroutineScope ?: processLifecycleScope
|
||||
scope.launch {
|
||||
handleException(e.source, e, true)
|
||||
if (
|
||||
handleException(
|
||||
source = e.source,
|
||||
exception = e,
|
||||
notify = request.extras[suppressCaptchaKey] != true,
|
||||
)
|
||||
) {
|
||||
coilProvider.get().enqueue(request) // TODO check if ok
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,11 +102,14 @@ class CaptchaHandler @Inject constructor(
|
||||
private suspend fun handleException(
|
||||
source: MangaSource,
|
||||
exception: CloudFlareException?,
|
||||
notify: Boolean
|
||||
notify: Boolean,
|
||||
): Boolean = withContext(Dispatchers.Default) {
|
||||
if (source == UnknownMangaSource) {
|
||||
return@withContext false
|
||||
}
|
||||
if (exception != null && webViewExecutor.tryResolveCaptcha(exception, RESOLVE_TIMEOUT)) {
|
||||
return@withContext true
|
||||
}
|
||||
mutex.withLock {
|
||||
var removedException: CloudFlareProtectedException? = null
|
||||
if (exception is CloudFlareProtectedException) {
|
||||
@@ -118,7 +134,7 @@ class CaptchaHandler @Inject constructor(
|
||||
notify(exceptions)
|
||||
}
|
||||
}
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
@@ -152,6 +168,15 @@ class CaptchaHandler @Inject constructor(
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(R.drawable.ic_bot)
|
||||
.setGroup(GROUP_CAPTCHA)
|
||||
.setContentIntent(
|
||||
PendingIntentCompat.getActivities(
|
||||
context, GROUP_NOTIFICATION_ID,
|
||||
exceptions.mapToArray { e ->
|
||||
AppRouter.cloudFlareResolveIntent(context, e)
|
||||
},
|
||||
0, false,
|
||||
),
|
||||
)
|
||||
.setContentText(
|
||||
context.getString(
|
||||
R.string.captcha_required_summary, context.getString(R.string.app_name),
|
||||
@@ -172,7 +197,6 @@ class CaptchaHandler @Inject constructor(
|
||||
|
||||
private suspend fun buildNotification(exception: CloudFlareProtectedException): Notification {
|
||||
val intent = AppRouter.cloudFlareResolveIntent(context, exception)
|
||||
.setData(exception.url.toUri())
|
||||
val discardIntent = Intent(ACTION_DISCARD)
|
||||
.putExtra(AppRouter.KEY_SOURCE, exception.source.name)
|
||||
.setData("source://${exception.source.name}".toUri())
|
||||
@@ -225,7 +249,7 @@ class CaptchaHandler @Inject constructor(
|
||||
.data(source.faviconUri())
|
||||
.allowHardware(false)
|
||||
.allowConversionToBitmap(true)
|
||||
.ignoreCaptchaErrors()
|
||||
.suppressCaptchaErrors()
|
||||
.mangaSourceExtra(source)
|
||||
.size(context.resources.getNotificationIconSize())
|
||||
.scale(Scale.FILL)
|
||||
@@ -251,11 +275,11 @@ class CaptchaHandler @Inject constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
fun ImageRequest.Builder.ignoreCaptchaErrors() = apply {
|
||||
extras[ignoreCaptchaKey] = true
|
||||
fun ImageRequest.Builder.suppressCaptchaErrors() = apply {
|
||||
extras[suppressCaptchaKey] = true
|
||||
}
|
||||
|
||||
val ignoreCaptchaKey = Extras.Key(false)
|
||||
private val suppressCaptchaKey = Extras.Key(false)
|
||||
|
||||
private const val CHANNEL_ID = "captcha"
|
||||
private const val TAG = CHANNEL_ID
|
||||
@@ -263,5 +287,6 @@ class CaptchaHandler @Inject constructor(
|
||||
private const val GROUP_NOTIFICATION_ID = 34
|
||||
private const val SETTINGS_ACTION_CODE = 3
|
||||
private const val ACTION_DISCARD = "org.koitharu.kotatsu.CAPTCHA_DISCARD"
|
||||
private const val RESOLVE_TIMEOUT = 20_000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,15 @@ data object UnknownMangaSource : MangaSource {
|
||||
override val name = "UNKNOWN"
|
||||
}
|
||||
|
||||
data object TestMangaSource : MangaSource {
|
||||
override val name = "TEST"
|
||||
}
|
||||
|
||||
fun MangaSource(name: String?): MangaSource {
|
||||
when (name ?: return UnknownMangaSource) {
|
||||
UnknownMangaSource.name -> return UnknownMangaSource
|
||||
|
||||
LocalMangaSource.name -> return LocalMangaSource
|
||||
TestMangaSource.name -> return TestMangaSource
|
||||
}
|
||||
if (name.startsWith("content:")) {
|
||||
val parts = name.substringAfter(':').splitTwoParts('/') ?: return UnknownMangaSource
|
||||
@@ -92,6 +96,7 @@ fun MangaSource.getSummary(context: Context): String? = when (val source = unwra
|
||||
fun MangaSource.getTitle(context: Context): String = when (val source = unwrap()) {
|
||||
is MangaParserSource -> source.title
|
||||
LocalMangaSource -> context.getString(R.string.local_storage)
|
||||
TestMangaSource -> context.getString(R.string.test_parser)
|
||||
is ExternalMangaSource -> source.resolveName(context)
|
||||
else -> context.getString(R.string.unknown)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.annotation.WorkerThread
|
||||
import androidx.core.util.Predicate
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import org.koitharu.kotatsu.parsers.util.newBuilder
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.core.network.webview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
import kotlin.coroutines.Continuation
|
||||
|
||||
class CaptchaContinuationClient(
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val targetUrl: String,
|
||||
continuation: Continuation<Unit>,
|
||||
) : ContinuationResumeWebViewClient(continuation) {
|
||||
|
||||
private val oldClearance = CloudFlareHelper.getClearanceCookie(cookieJar, targetUrl)
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) = Unit
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
checkClearance(view)
|
||||
}
|
||||
|
||||
private fun checkClearance(view: WebView?) {
|
||||
val clearance = CloudFlareHelper.getClearanceCookie(cookieJar, targetUrl)
|
||||
if (clearance != null && clearance != oldClearance) {
|
||||
resumeContinuation(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,22 @@ package org.koitharu.kotatsu.core.network.webview
|
||||
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class ContinuationResumeWebViewClient(
|
||||
open class ContinuationResumeWebViewClient(
|
||||
private val continuation: Continuation<Unit>,
|
||||
) : WebViewClient() {
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
view?.webViewClient = WebViewClient() // reset to default
|
||||
continuation.resume(Unit)
|
||||
resumeContinuation(view)
|
||||
}
|
||||
|
||||
protected fun resumeContinuation(view: WebView?) {
|
||||
if (continuation !is CancellableContinuation || continuation.isActive) {
|
||||
view?.webViewClient = WebViewClient() // reset to default
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.koitharu.kotatsu.core.network.webview
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.annotation.MainThread
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareException
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Singleton
|
||||
class WebViewExecutor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val proxyProvider: ProxyProvider,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val mangaRepositoryFactoryProvider: Provider<MangaRepository.Factory>,
|
||||
) {
|
||||
|
||||
private var webViewCached: WeakReference<WebView>? = null
|
||||
private val mutex = Mutex()
|
||||
|
||||
val defaultUserAgent: String? by lazy {
|
||||
WebSettings.getDefaultUserAgent(context)
|
||||
}
|
||||
|
||||
suspend fun evaluateJs(baseUrl: String?, script: String): String? = mutex.withLock {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val webView = obtainWebView()
|
||||
try {
|
||||
if (!baseUrl.isNullOrEmpty()) {
|
||||
suspendCoroutine { cont ->
|
||||
webView.webViewClient = ContinuationResumeWebViewClient(cont)
|
||||
webView.loadDataWithBaseURL(baseUrl, " ", "text/html", null, null)
|
||||
}
|
||||
}
|
||||
suspendCoroutine { cont ->
|
||||
webView.evaluateJavascript(script) { result ->
|
||||
cont.resume(result?.takeUnless { it == "null" })
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
webView.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun tryResolveCaptcha(exception: CloudFlareException, timeout: Long): Boolean = mutex.withLock {
|
||||
runCatchingCancellable {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val webView = obtainWebView()
|
||||
try {
|
||||
exception.source.getUserAgent()?.let {
|
||||
webView.settings.userAgentString = it
|
||||
}
|
||||
withTimeout(timeout) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
webView.webViewClient = CaptchaContinuationClient(
|
||||
cookieJar = cookieJar,
|
||||
targetUrl = exception.url,
|
||||
continuation = cont,
|
||||
)
|
||||
webView.loadUrl(exception.url)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
webView.reset()
|
||||
}
|
||||
}
|
||||
}.onFailure { e ->
|
||||
exception.addSuppressed(e)
|
||||
e.printStackTraceDebug()
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
private suspend fun obtainWebView(): WebView {
|
||||
webViewCached?.get()?.let {
|
||||
return it
|
||||
}
|
||||
return withContext(Dispatchers.Main.immediate) {
|
||||
webViewCached?.get()?.let {
|
||||
return@withContext it
|
||||
}
|
||||
WebView(context).also {
|
||||
it.configureForParser(null)
|
||||
webViewCached = WeakReference(it)
|
||||
proxyProvider.applyWebViewConfig()
|
||||
it.onResume()
|
||||
it.resumeTimers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaSource.getUserAgent(): String? {
|
||||
val repository = mangaRepositoryFactoryProvider.get().create(this) as? ParserMangaRepository
|
||||
return repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun WebView.reset() {
|
||||
stopLoading()
|
||||
webViewClient = WebViewClient()
|
||||
settings.userAgentString = defaultUserAgent
|
||||
loadDataWithBaseURL(null, " ", "text/html", null, null)
|
||||
clearHistory()
|
||||
}
|
||||
}
|
||||
@@ -80,12 +80,7 @@ class NetworkState(
|
||||
if (settings.isOfflineCheckDisabled) {
|
||||
return true
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
activeNetwork?.let { isOnline(it) } == true
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activeNetworkInfo?.isConnected == true
|
||||
}
|
||||
return activeNetwork?.let { isOnline(it) } == true
|
||||
}
|
||||
|
||||
private fun ConnectivityManager.isOnline(network: Network): Boolean {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* This parser is just for parser development, it should not be used in releases
|
||||
*/
|
||||
class DummyParser(context: MangaLoaderContext) : AbstractMangaParser(context, MangaParserSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("localhost")
|
||||
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities()
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
order: SortOrder,
|
||||
filter: MangaListFilter
|
||||
): List<Manga> = stub(null)
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
||||
|
||||
private fun stub(manga: Manga?): Nothing {
|
||||
throw UnsupportedSourceException("Usage of Dummy parser", manga)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.util.EnumSet
|
||||
|
||||
class EmptyMangaRepository(override val source: MangaSource) : MangaRepository {
|
||||
open class EmptyMangaRepository(override val source: MangaSource) : MangaRepository {
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
@@ -9,6 +9,8 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_PREFERENCES
|
||||
import org.koitharu.kotatsu.core.db.entity.ContentRating
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
@@ -189,6 +191,11 @@ class MangaDataRepository @Inject constructor(
|
||||
emitInitialState = emitInitialState,
|
||||
)
|
||||
|
||||
fun observeFavoritesTrigger(emitInitialState: Boolean) = db.invalidationTracker.createFlow(
|
||||
tables = arrayOf(TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES),
|
||||
emitInitialState = emitInitialState,
|
||||
)
|
||||
|
||||
private suspend fun Manga.withCachedChaptersIfNeeded(flag: Boolean): Manga = if (flag && !isLocal && chapters.isNullOrEmpty()) {
|
||||
val cachedChapters = db.getChaptersDao().findAll(id)
|
||||
if (cachedChapters.isEmpty()) {
|
||||
|
||||
@@ -3,15 +3,8 @@ package org.koitharu.kotatsu.core.parser
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -22,11 +15,8 @@ import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
|
||||
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.network.webview.ContinuationResumeWebViewClient
|
||||
import org.koitharu.kotatsu.core.network.webview.WebViewExecutor
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.use
|
||||
@@ -37,25 +27,19 @@ import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||
import org.koitharu.kotatsu.parsers.util.map
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Singleton
|
||||
class MangaLoaderContextImpl @Inject constructor(
|
||||
@MangaHttpClient override val httpClient: OkHttpClient,
|
||||
override val cookieJar: MutableCookieJar,
|
||||
@ApplicationContext private val androidContext: Context,
|
||||
private val webViewExecutor: WebViewExecutor,
|
||||
) : MangaLoaderContext() {
|
||||
|
||||
private var webViewCached: WeakReference<WebView>? = null
|
||||
private val webViewUserAgent by lazy { obtainWebViewUserAgent() }
|
||||
private val jsMutex = Mutex()
|
||||
private val jsTimeout = TimeUnit.SECONDS.toMillis(4)
|
||||
|
||||
@Deprecated("Provide a base url")
|
||||
@@ -63,25 +47,10 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
override suspend fun evaluateJs(script: String): String? = evaluateJs("", script)
|
||||
|
||||
override suspend fun evaluateJs(baseUrl: String, script: String): String? = withTimeout(jsTimeout) {
|
||||
jsMutex.withLock {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val webView = obtainWebView()
|
||||
if (baseUrl.isNotEmpty()) {
|
||||
suspendCoroutine { cont ->
|
||||
webView.webViewClient = ContinuationResumeWebViewClient(cont)
|
||||
webView.loadDataWithBaseURL(baseUrl, " ", "text/html", null, null)
|
||||
}
|
||||
}
|
||||
suspendCoroutine { cont ->
|
||||
webView.evaluateJavascript(script) { result ->
|
||||
cont.resume(result?.takeUnless { it == "null" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
webViewExecutor.evaluateJs(baseUrl, script)
|
||||
}
|
||||
|
||||
override fun getDefaultUserAgent(): String = webViewUserAgent
|
||||
override fun getDefaultUserAgent(): String = webViewExecutor.defaultUserAgent ?: UserAgents.FIREFOX_MOBILE
|
||||
|
||||
override fun getConfig(source: MangaSource): MangaSourceConfig {
|
||||
return SourceSettings(androidContext, source)
|
||||
@@ -118,28 +87,4 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
}
|
||||
|
||||
override fun createBitmap(width: Int, height: Int): Bitmap = BitmapWrapper.create(width, height)
|
||||
|
||||
@MainThread
|
||||
private fun obtainWebView(): WebView = webViewCached?.get() ?: WebView(androidContext).also {
|
||||
it.configureForParser(null)
|
||||
webViewCached = WeakReference(it)
|
||||
}
|
||||
|
||||
private fun obtainWebViewUserAgent(): String {
|
||||
val mainDispatcher = Dispatchers.Main.immediate
|
||||
return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
||||
obtainWebViewUserAgentImpl()
|
||||
} else {
|
||||
runBlocking(mainDispatcher) {
|
||||
obtainWebViewUserAgentImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun obtainWebViewUserAgentImpl() = runCatching {
|
||||
obtainWebView().settings.userAgentString.sanitizeHeaderValue()
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
|
||||
fun MangaParser(source: MangaParserSource, loaderContext: MangaLoaderContext): MangaParser {
|
||||
return when (source) {
|
||||
MangaParserSource.DUMMY -> DummyParser(loaderContext)
|
||||
else -> loaderContext.newParserInstance(source)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaSourceInfo
|
||||
import org.koitharu.kotatsu.core.model.TestMangaSource
|
||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
||||
@@ -85,11 +86,16 @@ interface MangaRepository {
|
||||
|
||||
private fun createRepository(source: MangaSource): MangaRepository? = when (source) {
|
||||
is MangaParserSource -> ParserMangaRepository(
|
||||
parser = MangaParser(source, loaderContext),
|
||||
parser = loaderContext.newParserInstance(source),
|
||||
cache = contentCache,
|
||||
mirrorSwitcher = mirrorSwitcher,
|
||||
)
|
||||
|
||||
TestMangaSource -> TestMangaRepository(
|
||||
loaderContext = loaderContext,
|
||||
cache = contentCache,
|
||||
)
|
||||
|
||||
is ExternalMangaSource -> if (source.isAvailable(context)) {
|
||||
ExternalMangaRepository(
|
||||
contentResolver = context.contentResolver,
|
||||
|
||||
@@ -53,6 +53,9 @@ class ExternalPluginContentSource(
|
||||
filter.states.forEach { uri.appendQueryParameter("state", it.name) }
|
||||
filter.locale?.let { uri.appendQueryParameter("locale", it.language) }
|
||||
filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) }
|
||||
if (!filter.author.isNullOrEmpty()) {
|
||||
uri.appendQueryParameter("author", filter.author)
|
||||
}
|
||||
if (!filter.query.isNullOrEmpty()) {
|
||||
uri.appendQueryParameter("query", filter.query)
|
||||
}
|
||||
@@ -196,6 +199,7 @@ class ExternalPluginContentSource(
|
||||
isYearSupported = cursor.getBooleanOrDefault(COLUMN_YEAR, false),
|
||||
isYearRangeSupported = cursor.getBooleanOrDefault(COLUMN_YEAR_RANGE, false),
|
||||
isOriginalLocaleSupported = cursor.getBooleanOrDefault(COLUMN_ORIGINAL_LOCALE, false),
|
||||
isAuthorSearchSupported = cursor.getBooleanOrDefault(COLUMN_AUTHOR, false),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -488,6 +488,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
|
||||
|
||||
var isWebtoonPullGestureEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_PULL_GESTURE, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_PULL_GESTURE, value) }
|
||||
|
||||
@get:FloatRange(from = 0.0, to = 0.5)
|
||||
val defaultWebtoonZoomOut: Float
|
||||
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
|
||||
@@ -748,6 +752,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_WEBTOON_GAPS = "webtoon_gaps"
|
||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
|
||||
const val KEY_WEBTOON_PULL_GESTURE = "webtoon_pull_gesture"
|
||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||
const val KEY_APP_LOCALE = "app_locale"
|
||||
const val KEY_SOURCES_GRID = "sources_grid"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.view.View
|
||||
|
||||
fun interface OnContextClickListenerCompat {
|
||||
|
||||
fun onContextClick(v: View): Boolean
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import coil3.asImage
|
||||
import coil3.request.Disposable
|
||||
import coil3.request.ImageRequest
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.CaptchaHandler.Companion.ignoreCaptchaErrors
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.CaptchaHandler.Companion.suppressCaptchaErrors
|
||||
import org.koitharu.kotatsu.core.image.CoilImageView
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
@@ -57,7 +57,7 @@ class FaviconView @JvmOverloads constructor(
|
||||
.fallback(fallbackFactory)
|
||||
.placeholder(placeholderFactory)
|
||||
.mangaSourceExtra(mangaSource)
|
||||
.ignoreCaptchaErrors()
|
||||
.suppressCaptchaErrors()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ package org.koitharu.kotatsu.core.ui.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnContextClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import androidx.core.util.Function
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
||||
import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
|
||||
class AdapterDelegateClickListenerAdapter<I, O>(
|
||||
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<out I, *>,
|
||||
private val clickListener: OnListItemClickListener<O>,
|
||||
private val itemMapper: Function<I, O>,
|
||||
) : OnClickListener, OnLongClickListener, OnContextClickListenerCompat {
|
||||
) : OnClickListener, OnLongClickListener, OnContextClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
clickListener.onItemClick(mappedItem(), v)
|
||||
@@ -33,7 +32,7 @@ class AdapterDelegateClickListenerAdapter<I, O>(
|
||||
fun attach(itemView: View) {
|
||||
itemView.setOnClickListener(this)
|
||||
itemView.setOnLongClickListener(this)
|
||||
itemView.setOnContextClickListenerCompat(this)
|
||||
itemView.setOnContextClickListener(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -186,6 +186,7 @@ class ListSelectionController(
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
if (event == Lifecycle.Event.ON_CREATE) {
|
||||
source.lifecycle.removeObserver(this)
|
||||
val registry = registryOwner.savedStateRegistry
|
||||
registry.registerSavedStateProvider(PROVIDER_NAME, this@ListSelectionController)
|
||||
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
@@ -14,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -37,14 +35,10 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
listeners?.forEach { it.onActionModeStarted(mode) }
|
||||
if (window != null) {
|
||||
val ctx = window.context
|
||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ColorUtils.compositeColors(
|
||||
ContextCompat.getColor(ctx, materialR.color.m3_appbar_overlay_color),
|
||||
ctx.getThemeColor(materialR.attr.colorSurface),
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
|
||||
}
|
||||
val actionModeColor = ColorUtils.compositeColors(
|
||||
ContextCompat.getColor(ctx, materialR.color.m3_appbar_overlay_color),
|
||||
ctx.getThemeColor(materialR.attr.colorSurface),
|
||||
)
|
||||
defaultStatusBarColor = window.statusBarColor
|
||||
window.statusBarColor = actionModeColor
|
||||
val insets = ViewCompat.getRootWindowInsets(window.decorView)
|
||||
|
||||
@@ -4,12 +4,10 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
|
||||
class PopupMenuMediator(
|
||||
private val provider: MenuProvider,
|
||||
) : View.OnLongClickListener, OnContextClickListenerCompat, PopupMenu.OnMenuItemClickListener,
|
||||
) : View.OnLongClickListener, View.OnContextClickListener, PopupMenu.OnMenuItemClickListener,
|
||||
PopupMenu.OnDismissListener {
|
||||
|
||||
override fun onContextClick(v: View): Boolean = onLongClick(v)
|
||||
@@ -37,6 +35,6 @@ class PopupMenuMediator(
|
||||
|
||||
fun attach(view: View) {
|
||||
view.setOnLongClickListener(this)
|
||||
view.setOnContextClickListenerCompat(this)
|
||||
view.setOnContextClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ open class StackLayout @JvmOverloads constructor(
|
||||
val h = b - t - paddingTop - paddingBottom
|
||||
visibleChildren.clear()
|
||||
children.filterNotTo(visibleChildren) { it.isGone }
|
||||
if (w <= 0 || h <= 0 || visibleChildren.isEmpty) {
|
||||
if (w <= 0 || h <= 0 || visibleChildren.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val xStep = w / (visibleChildren.size + 1)
|
||||
|
||||
@@ -31,8 +31,8 @@ open class MultiMutex<T : Any> {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
lock(element)
|
||||
return try {
|
||||
lock(element)
|
||||
block()
|
||||
} finally {
|
||||
unlock(element)
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
|
||||
@@ -169,12 +168,6 @@ fun BaseProgressIndicator<*>.showOrHide(value: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setOnContextClickListenerCompat(listener: OnContextClickListenerCompat) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setOnContextClickListener(listener::onContextClick)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setTooltipCompat(tooltip: CharSequence?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
tooltipText = tooltip
|
||||
@@ -183,6 +176,8 @@ fun View.setTooltipCompat(tooltip: CharSequence?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setTooltipCompat(@StringRes tooltipResId: Int) = setTooltipCompat(context.getString(tooltipResId))
|
||||
|
||||
val Toolbar.menuView: ActionMenuView?
|
||||
get() {
|
||||
menu // to call ensureMenu()
|
||||
|
||||
@@ -209,9 +209,7 @@ class DetailsActivity :
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
viewModel.getMangaOrNull()?.publicUrl?.toUriOrNull()?.let { outContent.webUri = it }
|
||||
}
|
||||
viewModel.getMangaOrNull()?.publicUrl?.toUriOrNull()?.let { outContent.webUri = it }
|
||||
}
|
||||
|
||||
override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.contentRating == ContentRating.ADULT }
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -25,6 +26,8 @@ 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.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -35,7 +38,8 @@ class RelatedListViewModel @Inject constructor(
|
||||
settings: AppSettings,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository) {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges) {
|
||||
|
||||
private val seed = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||
private val repository = mangaRepositoryFactory.create(seed.source)
|
||||
|
||||
@@ -202,7 +202,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
?: error("Cannot obtain remote manga instance")
|
||||
}
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
||||
val mangaDetails = if (manga.chapters.isNullOrEmpty() || manga.description.isNullOrEmpty()) repo.getDetails(manga) else manga
|
||||
output = LocalMangaOutput.getOrCreate(
|
||||
root = destination,
|
||||
manga = mangaDetails,
|
||||
|
||||
@@ -53,11 +53,7 @@ class MangaSourcesRepository @Inject constructor(
|
||||
get() = db.getSourcesDao()
|
||||
|
||||
val allMangaSources: Set<MangaParserSource> = Collections.unmodifiableSet(
|
||||
EnumSet.allOf(MangaParserSource::class.java).apply {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
remove(MangaParserSource.DUMMY)
|
||||
}
|
||||
},
|
||||
EnumSet.allOf(MangaParserSource::class.java)
|
||||
)
|
||||
|
||||
suspend fun getEnabledSources(): List<MangaSource> {
|
||||
|
||||
@@ -35,7 +35,7 @@ class ExploreRepository @Inject constructor(
|
||||
val details = runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
||||
}.getOrNull() ?: continue
|
||||
if ((settings.isSuggestionsExcludeNsfw && details.isNsfw) || details in tagsBlacklist) {
|
||||
if ((settings.isSuggestionsExcludeNsfw && details.isNsfw()) || details in tagsBlacklist) {
|
||||
continue
|
||||
}
|
||||
return details
|
||||
@@ -55,7 +55,7 @@ class ExploreRepository @Inject constructor(
|
||||
val details = runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
||||
}.getOrNull() ?: continue
|
||||
if ((skipNsfw && details.isNsfw) || details in tagsBlacklist) {
|
||||
if ((skipNsfw && details.isNsfw()) || details in tagsBlacklist) {
|
||||
continue
|
||||
}
|
||||
return details
|
||||
@@ -80,7 +80,7 @@ class ExploreRepository @Inject constructor(
|
||||
filter = MangaListFilter(tags = setOfNotNull(tag)),
|
||||
).asArrayList()
|
||||
if (settings.isSuggestionsExcludeNsfw) {
|
||||
list.removeAll { it.isNsfw }
|
||||
list.removeAll { it.isNsfw() }
|
||||
}
|
||||
if (blacklist.isNotEmpty()) {
|
||||
list.removeAll { manga -> manga in blacklist }
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.core.view.isVisible
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
|
||||
import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||
@@ -91,6 +92,13 @@ fun allCategoriesAD(
|
||||
R.drawable.ic_eye_off
|
||||
},
|
||||
)
|
||||
binding.imageViewVisible.setTooltipCompat(
|
||||
if (item.isVisible) {
|
||||
R.string.hide
|
||||
} else {
|
||||
R.string.show
|
||||
},
|
||||
)
|
||||
binding.coversView.setCoversAsync(item.covers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
private const val PAGE_SIZE = 16
|
||||
|
||||
@@ -52,7 +55,8 @@ class FavouritesListViewModel @Inject constructor(
|
||||
quickFilterFactory: FavoritesListQuickFilter.Factory,
|
||||
settings: AppSettings,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges), QuickFilterListener {
|
||||
|
||||
val categoryId: Long = savedStateHandle[AppRouter.KEY_ID] ?: NO_ID
|
||||
private val quickFilter = quickFilterFactory.create(categoryId)
|
||||
|
||||
@@ -51,9 +51,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
|
||||
@@ -43,6 +43,9 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
private const val PAGE_SIZE = 16
|
||||
|
||||
@@ -54,7 +57,8 @@ class HistoryListViewModel @Inject constructor(
|
||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||
private val quickFilter: HistoryListQuickFilter,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener by quickFilter {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges), QuickFilterListener by quickFilter {
|
||||
|
||||
private val sortOrder: StateFlow<ListSortOrder> = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.IO,
|
||||
|
||||
@@ -83,9 +83,7 @@ class CoverImageView @JvmOverloads constructor(
|
||||
if (fallbackDrawable == null) {
|
||||
fallbackDrawable = context.getThemeColor(materialR.attr.colorSurfaceContainer).toDrawable()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
addImageRequestListener(ErrorForegroundListener())
|
||||
}
|
||||
addImageRequestListener(ErrorForegroundListener())
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
@@ -169,7 +167,6 @@ class CoverImageView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private inner class ErrorForegroundListener : ImageRequest.Listener {
|
||||
|
||||
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
|
||||
|
||||
@@ -3,13 +3,16 @@ package org.koitharu.kotatsu.list.ui
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -21,10 +24,13 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
|
||||
abstract class MangaListViewModel(
|
||||
private val settings: AppSettings,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
@param:LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : BaseViewModel() {
|
||||
|
||||
abstract val content: StateFlow<List<ListModel>>
|
||||
@@ -45,7 +51,7 @@ abstract class MangaListViewModel(
|
||||
abstract fun onRetry()
|
||||
|
||||
protected fun List<Manga>.skipNsfwIfNeeded() = if (settings.isNsfwContentDisabled) {
|
||||
filterNot { it.isNsfw }
|
||||
filterNot { it.isNsfw() }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@@ -62,7 +68,11 @@ abstract class MangaListViewModel(
|
||||
|
||||
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
|
||||
listMode,
|
||||
mangaDataRepository.observeOverridesTrigger(emitInitialState = true),
|
||||
merge(
|
||||
mangaDataRepository.observeOverridesTrigger(emitInitialState = true),
|
||||
mangaDataRepository.observeFavoritesTrigger(emitInitialState = true),
|
||||
localStorageChanges.onStart { emit(null) },
|
||||
),
|
||||
settings.observeChanges().filter { key ->
|
||||
key == AppSettings.KEY_PROGRESS_INDICATORS
|
||||
|| key == AppSettings.KEY_TRACKER_ENABLED
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
@@ -94,7 +95,7 @@ class LocalMangaRepository @Inject constructor(
|
||||
}
|
||||
val list = getRawList()
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
list.removeAll { it.manga.isNsfw }
|
||||
list.removeAll { it.manga.isNsfw() }
|
||||
}
|
||||
if (filter != null) {
|
||||
val query = filter.query
|
||||
@@ -109,7 +110,7 @@ class LocalMangaRepository @Inject constructor(
|
||||
}
|
||||
filter.contentRating.singleOrNull()?.let { contentRating ->
|
||||
val isNsfw = contentRating == ContentRating.ADULT
|
||||
list.retainAll { it.manga.isNsfw == isNsfw }
|
||||
list.retainAll { it.manga.isNsfw() == isNsfw }
|
||||
}
|
||||
if (!query.isNullOrEmpty() && order == SortOrder.RELEVANCE) {
|
||||
list.sortBy { it.manga.title.levenshteinDistance(query) }
|
||||
|
||||
@@ -45,7 +45,7 @@ class LocalListViewModel @Inject constructor(
|
||||
mangaListMapper: MangaListMapper,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
exploreRepository: ExploreRepository,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
@param:LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
private val localStorageManager: LocalStorageManager,
|
||||
sourcesRepository: MangaSourcesRepository,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
@@ -58,6 +58,7 @@ class LocalListViewModel @Inject constructor(
|
||||
exploreRepository = exploreRepository,
|
||||
sourcesRepository = sourcesRepository,
|
||||
mangaDataRepository = mangaDataRepository,
|
||||
localStorageChanges = localStorageChanges,
|
||||
), SharedPreferences.OnSharedPreferenceChangeListener, QuickFilterListener {
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Unit>()
|
||||
|
||||
@@ -131,7 +131,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
onBackPressedDispatcher.addCallback(exitCallback)
|
||||
onBackPressedDispatcher.addCallback(navigationDelegate)
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || !resources.getBoolean(R.bool.is_predictive_back_enabled)) {
|
||||
val legacySearchCallback = SearchViewLegacyBackCallback(viewBinding.searchView)
|
||||
viewBinding.searchView.addTransitionListener(legacySearchCallback)
|
||||
onBackPressedDispatcher.addCallback(legacySearchCallback)
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package org.koitharu.kotatsu.main.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.isEmpty
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.iterator
|
||||
import androidx.core.view.size
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -66,6 +69,9 @@ class MainNavigationDelegate(
|
||||
navBar.setOnItemSelectedListener(this)
|
||||
navBar.setOnItemReselectedListener(this)
|
||||
navRailHeader?.run {
|
||||
root.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = Gravity.TOP or Gravity.CENTER
|
||||
}
|
||||
val horizontalPadding = (navBar as NavigationRailView).itemActiveIndicatorMarginHorizontal
|
||||
root.setPadding(horizontalPadding, 0, horizontalPadding, 0)
|
||||
buttonExpand.setOnClickListener(this@MainNavigationDelegate)
|
||||
@@ -295,6 +301,9 @@ class MainNavigationDelegate(
|
||||
if (value) {
|
||||
navBar.expand()
|
||||
navRailHeader?.run {
|
||||
root.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = Gravity.TOP or Gravity.START
|
||||
}
|
||||
railFab.extend()
|
||||
buttonExpand.setImageResource(R.drawable.ic_drawer_menu_open)
|
||||
buttonExpand.setContentDescriptionAndTooltip(R.string.collapse)
|
||||
@@ -304,6 +313,9 @@ class MainNavigationDelegate(
|
||||
} else {
|
||||
navBar.collapse()
|
||||
navRailHeader?.run {
|
||||
root.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = Gravity.TOP or Gravity.CENTER
|
||||
}
|
||||
railFab.shrink()
|
||||
buttonExpand.setImageResource(R.drawable.ic_drawer_menu)
|
||||
buttonExpand.setContentDescriptionAndTooltip(R.string.expand)
|
||||
|
||||
@@ -20,6 +20,9 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
|
||||
@HiltViewModel
|
||||
class MangaPickerViewModel @Inject constructor(
|
||||
@@ -28,7 +31,8 @@ class MangaPickerViewModel @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
) : MangaListViewModel(settings, mangaDataRepository) {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges) {
|
||||
|
||||
override val content: StateFlow<List<ListModel>>
|
||||
get() = flow {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.reader.ui
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
@@ -215,9 +214,7 @@ class ReaderActivity :
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
viewModel.getMangaOrNull()?.publicUrl?.toUriOrNull()?.let { outContent.webUri = it }
|
||||
}
|
||||
viewModel.getMangaOrNull()?.publicUrl?.toUriOrNull()?.let { outContent.webUri = it }
|
||||
}
|
||||
|
||||
override fun isNsfwContent(): Flow<Boolean> = viewModel.isMangaNsfw
|
||||
@@ -374,6 +371,7 @@ class ReaderActivity :
|
||||
viewBinding.infoBar.isTimeVisible = isFullscreen
|
||||
updateScrollTimerButton()
|
||||
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
|
||||
viewBinding.root.requestApplyInsets()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,8 +393,14 @@ class ReaderActivity :
|
||||
viewBinding.infoBar.updatePadding(
|
||||
top = systemBars.top,
|
||||
)
|
||||
val innerInsets = Insets.of(
|
||||
systemBars.left,
|
||||
if (viewBinding.appbarTop.isVisible) viewBinding.appbarTop.height else systemBars.top,
|
||||
systemBars.right,
|
||||
viewBinding.toolbarDocked?.takeIf { it.isVisible }?.height ?: systemBars.bottom,
|
||||
)
|
||||
return WindowInsetsCompat.Builder(insets)
|
||||
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
|
||||
.setInsets(WindowInsetsCompat.Type.systemBars(), innerInsets)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,12 @@ class ReaderViewModel @Inject constructor(
|
||||
valueProducer = { isWebtoonGapsEnabled },
|
||||
)
|
||||
|
||||
val isWebtoonPullGestureEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_WEBTOON_PULL_GESTURE,
|
||||
valueProducer = { isWebtoonPullGestureEnabled },
|
||||
)
|
||||
|
||||
val defaultWebtoonZoomOut = observeIsWebtoonZoomEnabled().flatMapLatest {
|
||||
if (it) {
|
||||
observeWebtoonZoomOut()
|
||||
@@ -345,11 +351,14 @@ class ReaderViewModel @Inject constructor(
|
||||
return@launchJob
|
||||
}
|
||||
ensureActive()
|
||||
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||
}
|
||||
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
val autoLoadAllowed = readerMode.value != ReaderMode.WEBTOON || !isWebtoonPullGestureEnabled.value
|
||||
if (autoLoadAllowed) {
|
||||
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||
}
|
||||
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
}
|
||||
}
|
||||
if (pageLoader.isPrefetchApplicable()) {
|
||||
pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
|
||||
|
||||
@@ -86,6 +86,8 @@ class ReaderConfigSheet :
|
||||
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
||||
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
|
||||
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED
|
||||
binding.switchPullGesture.isChecked = settings.isWebtoonPullGestureEnabled
|
||||
binding.switchPullGesture.isEnabled = mode == ReaderMode.WEBTOON
|
||||
|
||||
binding.checkableGroup.addOnButtonCheckedListener(this)
|
||||
binding.buttonSavePage.setOnClickListener(this)
|
||||
@@ -96,6 +98,7 @@ class ReaderConfigSheet :
|
||||
binding.buttonScrollTimer.setOnClickListener(this)
|
||||
binding.buttonBookmark.setOnClickListener(this)
|
||||
binding.switchDoubleReader.setOnCheckedChangeListener(this)
|
||||
binding.switchPullGesture.setOnCheckedChangeListener(this)
|
||||
|
||||
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
|
||||
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
|
||||
@@ -172,6 +175,10 @@ class ReaderConfigSheet :
|
||||
settings.isReaderDoubleOnLandscape = isChecked
|
||||
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
|
||||
}
|
||||
|
||||
R.id.switch_pull_gesture -> {
|
||||
settings.isWebtoonPullGestureEnabled = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +198,7 @@ class ReaderConfigSheet :
|
||||
else -> return
|
||||
}
|
||||
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
|
||||
viewBinding?.switchPullGesture?.isEnabled = newMode == ReaderMode.WEBTOON
|
||||
if (newMode == mode) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,9 +86,9 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
desiredWidth = sWidth
|
||||
desiredHeight = sHeight
|
||||
} else if (resizeHeight) {
|
||||
desiredHeight = (sHeight.toDouble() / sWidth.toDouble() * desiredWidth).roundToInt()
|
||||
desiredHeight = (sHeight.toDouble() / sWidth.toDouble() * desiredWidth).toInt()
|
||||
} else if (resizeWidth) {
|
||||
desiredWidth = (sWidth.toDouble() / sHeight.toDouble() * desiredHeight).roundToInt()
|
||||
desiredWidth = (sWidth.toDouble() / sHeight.toDouble() * desiredHeight).toInt()
|
||||
}
|
||||
}
|
||||
desiredWidth = desiredWidth.coerceAtLeast(suggestedMinimumWidth)
|
||||
|
||||
@@ -2,8 +2,13 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -27,7 +32,8 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>(),
|
||||
WebtoonRecyclerView.OnWebtoonScrollListener {
|
||||
WebtoonRecyclerView.OnWebtoonScrollListener,
|
||||
WebtoonRecyclerView.OnPullGestureListener {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
@@ -38,6 +44,8 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
private val scrollInterpolator = DecelerateInterpolator()
|
||||
|
||||
private var recyclerLifecycleDispatcher: RecyclerViewLifecycleDispatcher? = null
|
||||
private var canGoPrev = true
|
||||
private var canGoNext = true
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
@@ -53,6 +61,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
|
||||
addOnScrollListener(it)
|
||||
}
|
||||
setOnPullGestureListener(this@WebtoonReaderFragment)
|
||||
}
|
||||
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
|
||||
binding.frame.isZoomEnable = it
|
||||
@@ -70,6 +79,18 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
|
||||
it.applyBackground(binding.root)
|
||||
}
|
||||
viewModel.isWebtoonPullGestureEnabled.observe(viewLifecycleOwner) { enabled ->
|
||||
binding.recyclerView.isPullGestureEnabled = enabled
|
||||
}
|
||||
viewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||
if (state != null) {
|
||||
canGoPrev = state.chapterIndex > 0
|
||||
canGoNext = state.chapterIndex < state.chaptersTotal - 1
|
||||
} else {
|
||||
canGoPrev = true
|
||||
canGoNext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -78,6 +99,19 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val offsetInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
viewBinding?.apply {
|
||||
feedbackTop.updateLayoutParams<MarginLayoutParams> {
|
||||
topMargin = bottomMargin + offsetInsets.top
|
||||
}
|
||||
feedbackBottom.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = topMargin + offsetInsets.bottom
|
||||
}
|
||||
}
|
||||
return super.onApplyWindowInsets(v, insets)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter() = WebtoonAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = pageLoader,
|
||||
@@ -168,6 +202,47 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPullProgressTop(progress: Float) {
|
||||
val binding = viewBinding ?: return
|
||||
if (canGoPrev) {
|
||||
binding.feedbackTop.setFeedbackText(getString(R.string.pull_to_prev_chapter))
|
||||
} else {
|
||||
binding.feedbackTop.setFeedbackText(getString(R.string.pull_top_no_prev))
|
||||
}
|
||||
binding.feedbackTop.updateFeedback(progress)
|
||||
}
|
||||
|
||||
override fun onPullProgressBottom(progress: Float) {
|
||||
val binding = viewBinding ?: return
|
||||
if (canGoNext) {
|
||||
binding.feedbackBottom.setFeedbackText(getString(R.string.pull_to_next_chapter))
|
||||
} else {
|
||||
binding.feedbackBottom.setFeedbackText(getString(R.string.pull_bottom_no_next))
|
||||
}
|
||||
binding.feedbackBottom.updateFeedback(progress)
|
||||
}
|
||||
|
||||
override fun onPullTriggeredTop() {
|
||||
(viewBinding ?: return).feedbackTop.fadeOut()
|
||||
if (canGoPrev) {
|
||||
viewModel.switchChapterBy(-1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPullTriggeredBottom() {
|
||||
(viewBinding ?: return).feedbackBottom.fadeOut()
|
||||
if (canGoNext) {
|
||||
viewModel.switchChapterBy(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPullCancelled() {
|
||||
viewBinding?.apply {
|
||||
feedbackTop.fadeOut()
|
||||
feedbackBottom.fadeOut()
|
||||
}
|
||||
}
|
||||
|
||||
private fun RecyclerView.findCurrentPagePosition(): Int {
|
||||
val centerX = width / 2f
|
||||
val centerY = height - resources.getDimension(R.dimen.webtoon_pages_gap)
|
||||
@@ -177,4 +252,25 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION
|
||||
return getChildAdapterPosition(view)
|
||||
}
|
||||
|
||||
private fun TextView.updateFeedback(progress: Float) {
|
||||
val clamped = progress.coerceIn(0f, 1.2f)
|
||||
this.alpha = clamped.coerceAtMost(1f)
|
||||
this.scaleX = 0.9f + 0.1f * clamped.coerceAtMost(1f)
|
||||
this.scaleY = this.scaleX
|
||||
}
|
||||
|
||||
private fun TextView.fadeOut() {
|
||||
animate().alpha(0f).setDuration(150L).start()
|
||||
}
|
||||
|
||||
private fun TextView.setFeedbackText(text: CharSequence) {
|
||||
if (this.alpha <= 0f && text.isNotEmpty()) {
|
||||
this.alpha = 0f
|
||||
this.text = text
|
||||
animate().alpha(1f).setDuration(120L).start()
|
||||
} else {
|
||||
this.text = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.EdgeEffect
|
||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isEmpty
|
||||
@@ -10,6 +12,8 @@ import androidx.core.view.isNotEmpty
|
||||
import androidx.core.view.iterator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_BOTTOM
|
||||
import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_TOP
|
||||
import java.util.Collections
|
||||
import java.util.LinkedList
|
||||
import java.util.WeakHashMap
|
||||
@@ -23,6 +27,18 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
|
||||
private var isFixingScroll = false
|
||||
|
||||
var isPullGestureEnabled: Boolean = false
|
||||
var pullThreshold: Float = 0.3f
|
||||
private var pullListener: OnPullGestureListener? = null
|
||||
|
||||
init {
|
||||
setEdgeEffectFactory(PullEffect.Factory())
|
||||
}
|
||||
|
||||
fun setOnPullGestureListener(listener: OnPullGestureListener?) {
|
||||
pullListener = listener
|
||||
}
|
||||
|
||||
override fun onChildDetachedFromWindow(child: View) {
|
||||
super.onChildDetachedFromWindow(child)
|
||||
detachedViews.add(child)
|
||||
@@ -179,6 +195,68 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private class PullEffect(
|
||||
view: RecyclerView,
|
||||
private val direction: Int,
|
||||
private val pullThreshold: Float,
|
||||
private val pullListener: OnPullGestureListener,
|
||||
) : EdgeEffect(view.context) {
|
||||
|
||||
private var pullProgressTop: Float = 0f
|
||||
private var pullProgressBottom: Float = 0f
|
||||
|
||||
override fun onPull(deltaDistance: Float) {
|
||||
val sign = if (direction == DIRECTION_TOP) 1f else if (direction == DIRECTION_BOTTOM) 1f else 0f
|
||||
if (sign != 0f) onPull(deltaDistance, 0.5f)
|
||||
}
|
||||
|
||||
override fun onPull(deltaDistance: Float, displacement: Float) {
|
||||
if (direction == DIRECTION_TOP) {
|
||||
pullProgressTop = (pullProgressTop + deltaDistance).coerceAtLeast(0f)
|
||||
pullListener.onPullProgressTop(pullProgressTop / pullThreshold)
|
||||
} else if (direction == DIRECTION_BOTTOM) {
|
||||
pullProgressBottom = (pullProgressBottom + deltaDistance).coerceAtLeast(0f)
|
||||
pullListener.onPullProgressBottom(pullProgressBottom / pullThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRelease() {
|
||||
var triggered = false
|
||||
if (direction == DIRECTION_TOP) {
|
||||
if (pullProgressTop >= pullThreshold) {
|
||||
pullListener.onPullTriggeredTop()
|
||||
triggered = true
|
||||
}
|
||||
pullProgressTop = 0f
|
||||
pullListener.onPullProgressTop(0f)
|
||||
} else if (direction == DIRECTION_BOTTOM) {
|
||||
if (pullProgressBottom >= pullThreshold) {
|
||||
pullListener.onPullTriggeredBottom()
|
||||
triggered = true
|
||||
}
|
||||
pullProgressBottom = 0f
|
||||
pullListener.onPullProgressBottom(0f)
|
||||
}
|
||||
if (!triggered) {
|
||||
pullListener.onPullCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas?): Boolean = false
|
||||
|
||||
class Factory : EdgeEffectFactory() {
|
||||
|
||||
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||
val pullListener = (view as? WebtoonRecyclerView)?.pullListener
|
||||
return if (pullListener != null && view.isPullGestureEnabled) {
|
||||
PullEffect(view, direction, view.pullThreshold, pullListener)
|
||||
} else {
|
||||
super.createEdgeEffect(view, direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OnWebtoonScrollListener {
|
||||
|
||||
fun onScrollChanged(
|
||||
@@ -188,4 +266,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
lastVisiblePosition: Int,
|
||||
)
|
||||
}
|
||||
|
||||
interface OnPullGestureListener {
|
||||
fun onPullProgressTop(progress: Float)
|
||||
fun onPullProgressBottom(progress: Float)
|
||||
fun onPullTriggeredTop()
|
||||
fun onPullTriggeredBottom()
|
||||
fun onPullCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -40,6 +41,8 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.sizeOrZero
|
||||
import javax.inject.Inject
|
||||
@@ -55,8 +58,9 @@ open class RemoteListViewModel @Inject constructor(
|
||||
protected val mangaListMapper: MangaListMapper,
|
||||
private val exploreRepository: ExploreRepository,
|
||||
sourcesRepository: MangaSourcesRepository,
|
||||
mangaDataRepository: MangaDataRepository
|
||||
) : MangaListViewModel(settings, mangaDataRepository), FilterCoordinator.Owner {
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges), FilterCoordinator.Owner {
|
||||
|
||||
val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE])
|
||||
val isRandomLoading = MutableStateFlow(false)
|
||||
|
||||
@@ -76,7 +76,7 @@ class SearchV2Helper @AssistedInject constructor(
|
||||
|
||||
private fun MutableList<Manga>.postFilter(query: String, kind: SearchKind) {
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
removeAll { it.isNsfw }
|
||||
removeAll { it.isNsfw() }
|
||||
}
|
||||
when (kind) {
|
||||
SearchKind.TITLE -> retainAll { m ->
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.settings.protect
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.view.KeyEvent
|
||||
@@ -115,7 +114,6 @@ class ProtectSetupActivity :
|
||||
}
|
||||
|
||||
private fun isBiometricAvailable(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -33,10 +32,6 @@ class DozeHelper(
|
||||
}
|
||||
|
||||
fun startIgnoreDoseActivity(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Snackbar.make(fragment.listView ?: return false, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
val context = fragment.context ?: return false
|
||||
val packageName = context.packageName
|
||||
val powerManager = context.powerManager ?: return false
|
||||
@@ -58,9 +53,6 @@ class DozeHelper(
|
||||
}
|
||||
|
||||
private fun isDozeIgnoreAvailable(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return false
|
||||
}
|
||||
val context = fragment.context ?: return false
|
||||
val packageName = context.packageName
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
|
||||
@@ -24,6 +24,9 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionsListQuickFilter
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
@HiltViewModel
|
||||
class SuggestionsViewModel @Inject constructor(
|
||||
@@ -33,7 +36,8 @@ class SuggestionsViewModel @Inject constructor(
|
||||
private val quickFilter: SuggestionsListQuickFilter,
|
||||
private val suggestionsScheduler: SuggestionsWorker.Scheduler,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener by quickFilter {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges), QuickFilterListener by quickFilter {
|
||||
|
||||
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_SUGGESTIONS) { suggestionsListMode }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.suggestionsListMode)
|
||||
|
||||
@@ -229,7 +229,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
if (details.rating > 0 && details.rating < RATING_MIN) {
|
||||
continue
|
||||
}
|
||||
if (details.isNsfw && (appSettings.isSuggestionsExcludeNsfw || appSettings.isNsfwContentDisabled)) {
|
||||
if (details.isNsfw() && (appSettings.isSuggestionsExcludeNsfw || appSettings.isNsfwContentDisabled)) {
|
||||
continue
|
||||
}
|
||||
if (details in tagsBlacklist) {
|
||||
@@ -277,7 +277,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
filter = MangaListFilter(tags = setOfNotNull(tag)),
|
||||
).asArrayList()
|
||||
if (appSettings.isSuggestionsExcludeNsfw) {
|
||||
list.removeAll { it.isNsfw }
|
||||
list.removeAll { it.isNsfw() }
|
||||
}
|
||||
if (blacklist.isNotEmpty()) {
|
||||
list.removeAll { manga -> manga in blacklist }
|
||||
|
||||
@@ -5,18 +5,20 @@ import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
@@ -31,13 +33,20 @@ import org.koitharu.kotatsu.databinding.ActivitySyncAuthBinding
|
||||
import org.koitharu.kotatsu.sync.data.SyncSettings
|
||||
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
|
||||
|
||||
private const val PAGE_EMAIL = 0
|
||||
private const val PAGE_PASSWORD = 1
|
||||
private const val PASSWORD_MIN_LENGTH = 4
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickListener, FragmentResultListener {
|
||||
class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickListener, FragmentResultListener,
|
||||
DefaultTextWatcher {
|
||||
|
||||
private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
|
||||
private var resultBundle: Bundle? = null
|
||||
private val pageBackCallback = PageBackCallback()
|
||||
|
||||
private val regexEmail = Regex("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", RegexOption.IGNORE_CASE)
|
||||
|
||||
private val viewModel by viewModels<SyncAuthViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -46,14 +55,13 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
accountAuthenticatorResponse =
|
||||
intent.getParcelableExtraCompat(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
|
||||
accountAuthenticatorResponse?.onRequestContinued()
|
||||
viewBinding.buttonCancel.setOnClickListener(this)
|
||||
viewBinding.buttonNext.setOnClickListener(this)
|
||||
viewBinding.buttonBack.setOnClickListener(this)
|
||||
viewBinding.buttonCancel.setOnClickListener(this)
|
||||
viewBinding.buttonDone.setOnClickListener(this)
|
||||
viewBinding.layoutProgress.setOnClickListener(this)
|
||||
viewBinding.buttonSettings.setOnClickListener(this)
|
||||
viewBinding.editEmail.addTextChangedListener(EmailTextWatcher(viewBinding.buttonNext))
|
||||
viewBinding.editPassword.addTextChangedListener(PasswordTextWatcher(viewBinding.buttonDone))
|
||||
viewBinding.editEmail.addTextChangedListener(this)
|
||||
viewBinding.editPassword.addTextChangedListener(this)
|
||||
|
||||
onBackPressedDispatcher.addCallback(pageBackCallback)
|
||||
|
||||
@@ -65,17 +73,25 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
}
|
||||
|
||||
supportFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, this, this)
|
||||
pageBackCallback.update()
|
||||
if (savedInstanceState == null) {
|
||||
setPage(PAGE_EMAIL)
|
||||
} else {
|
||||
pageBackCallback.update()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val barsInsets = insets.systemBarsInsets
|
||||
val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
|
||||
viewBinding.root.setPadding(
|
||||
barsInsets.left + basePadding,
|
||||
barsInsets.top + basePadding,
|
||||
barsInsets.right + basePadding,
|
||||
barsInsets.bottom + basePadding,
|
||||
viewBinding.root.updatePadding(top = barsInsets.top)
|
||||
viewBinding.dockedToolbarChild.updateLayoutParams<MarginLayoutParams> {
|
||||
leftMargin = barsInsets.left
|
||||
rightMargin = barsInsets.right
|
||||
bottomMargin = barsInsets.bottom
|
||||
}
|
||||
val basePadding = viewBinding.layoutContent.paddingBottom
|
||||
viewBinding.layoutContent.updatePadding(
|
||||
left = barsInsets.left + basePadding,
|
||||
right = barsInsets.right + basePadding,
|
||||
)
|
||||
return insets.consumeAllSystemBarsInsets()
|
||||
}
|
||||
@@ -88,22 +104,18 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
}
|
||||
|
||||
R.id.button_next -> {
|
||||
viewBinding.groupLogin.isVisible = false
|
||||
viewBinding.groupPassword.isVisible = true
|
||||
pageBackCallback.update()
|
||||
setPage(PAGE_PASSWORD)
|
||||
viewBinding.editPassword.requestFocus()
|
||||
}
|
||||
|
||||
R.id.button_back -> {
|
||||
viewBinding.groupPassword.isVisible = false
|
||||
viewBinding.groupLogin.isVisible = true
|
||||
pageBackCallback.update()
|
||||
setPage(PAGE_EMAIL)
|
||||
viewBinding.editEmail.requestFocus()
|
||||
}
|
||||
|
||||
R.id.button_done -> {
|
||||
viewModel.obtainToken(
|
||||
email = viewBinding.editEmail.text.toString(),
|
||||
email = viewBinding.editEmail.text.toString().trim(),
|
||||
password = viewBinding.editPassword.text.toString(),
|
||||
)
|
||||
}
|
||||
@@ -128,12 +140,39 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
super.finish()
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val isLoading = viewModel.isLoading.value
|
||||
val email = viewBinding.editEmail.text?.trim()?.toString()
|
||||
val password = viewBinding.editPassword.text?.toString()
|
||||
viewBinding.buttonNext.isEnabled = !isLoading && !email.isNullOrEmpty() && regexEmail.matches(email)
|
||||
viewBinding.buttonDone.isEnabled = !isLoading && password != null && password.length >= PASSWORD_MIN_LENGTH
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
if (isLoading == viewBinding.layoutProgress.isVisible) {
|
||||
return
|
||||
with(viewBinding) {
|
||||
progressBar.isInvisible = !isLoading
|
||||
editEmail.isEnabled = !isLoading
|
||||
editPassword.isEnabled = !isLoading
|
||||
}
|
||||
afterTextChanged(null)
|
||||
pageBackCallback.update()
|
||||
}
|
||||
|
||||
private fun setPage(page: Int) {
|
||||
with(viewBinding) {
|
||||
val currentPage = if (layoutEmail.isVisible) PAGE_EMAIL else PAGE_PASSWORD
|
||||
if (currentPage != page) {
|
||||
val transition = MaterialSharedAxis(MaterialSharedAxis.X, page > currentPage)
|
||||
TransitionManager.beginDelayedTransition(layoutContent, transition)
|
||||
}
|
||||
buttonNext.isVisible = page == PAGE_EMAIL
|
||||
buttonBack.isVisible = page == PAGE_PASSWORD
|
||||
buttonSettings.isVisible = page == PAGE_EMAIL
|
||||
buttonDone.isVisible = page == PAGE_PASSWORD
|
||||
buttonCancel.isVisible = page == PAGE_EMAIL
|
||||
layoutEmail.isVisible = page == PAGE_EMAIL
|
||||
layoutPassword.isVisible = page == PAGE_PASSWORD
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(viewBinding.root, Fade())
|
||||
viewBinding.layoutProgress.isVisible = isLoading
|
||||
pageBackCallback.update()
|
||||
}
|
||||
|
||||
@@ -174,43 +213,16 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
super.finishAfterTransition()
|
||||
}
|
||||
|
||||
private class EmailTextWatcher(
|
||||
private val button: Button,
|
||||
) : TextWatcher {
|
||||
|
||||
private val regexEmail = Regex("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", RegexOption.IGNORE_CASE)
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val text = s?.toString()
|
||||
button.isEnabled = !text.isNullOrEmpty() && regexEmail.matches(text)
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordTextWatcher(
|
||||
private val button: Button,
|
||||
) : DefaultTextWatcher {
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val text = s?.toString()
|
||||
button.isEnabled = text != null && text.length >= 4
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PageBackCallback : OnBackPressedCallback(false) {
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
viewBinding.groupLogin.isVisible = true
|
||||
viewBinding.groupPassword.isVisible = false
|
||||
setPage(PAGE_EMAIL)
|
||||
viewBinding.editEmail.requestFocus()
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
isEnabled = !viewBinding.layoutProgress.isVisible && viewBinding.groupPassword.isVisible
|
||||
isEnabled = !viewBinding.progressBar.isVisible && viewBinding.editPassword.isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.UpdatesListQuickFilter
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
@HiltViewModel
|
||||
class UpdatesViewModel @Inject constructor(
|
||||
@@ -39,7 +42,8 @@ class UpdatesViewModel @Inject constructor(
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val quickFilter: UpdatesListQuickFilter,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener by quickFilter {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : MangaListViewModel(settings, mangaDataRepository, localStorageChanges), QuickFilterListener by quickFilter {
|
||||
|
||||
override val content = combine(
|
||||
quickFilter.appliedOptions.flatMapLatest { filterOptions ->
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.4" android:color="?android:colorBackground" />
|
||||
</selector>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.6" android:color="?colorSurfaceBright" />
|
||||
</selector>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorSurfaceContainerHigh" />
|
||||
</selector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.3" android:color="?colorPrimary" android:state_checked="true"/>
|
||||
<item android:color="@android:color/transparent"/>
|
||||
</selector>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.27" android:color="?attr/colorOnSurface" />
|
||||
</selector>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.4" android:color="@color/kotatsu_background" />
|
||||
<item android:alpha="0.4" android:color="?android:colorBackground" />
|
||||
</selector>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- https://stackoverflow.com/questions/54685474/theme-attributes-in-color-selector-for-api-22 -->
|
||||
<item android:alpha="0.6" android:color="@color/kotatsu_surfaceBright" />
|
||||
<item android:alpha="0.6" android:color="?colorSurfaceBright" />
|
||||
</selector>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/kotatsu_surfaceContainerHigh" />
|
||||
<item android:color="?attr/colorSurfaceContainerHigh" />
|
||||
</selector>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- https://stackoverflow.com/questions/54685474/theme-attributes-in-color-selector-for-api-22 -->
|
||||
<item android:alpha="0.3" android:color="@color/kotatsu_primary" android:state_checked="true"/>
|
||||
<item android:alpha="0.3" android:color="?colorPrimary" android:state_checked="true"/>
|
||||
<item android:color="@android:color/transparent"/>
|
||||
</selector>
|
||||
</selector>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- https://stackoverflow.com/questions/54685474/theme-attributes-in-color-selector-for-api-22 -->
|
||||
<item android:alpha="0.27" android:color="@color/kotatsu_onSurface" />
|
||||
<item android:alpha="0.27" android:color="?attr/colorOnSurface" />
|
||||
</selector>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?attr/colorSurface" />
|
||||
<corners android:radius="@dimen/m3_alert_dialog_corner_size" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/m3_popupmenu_overlay_color" />
|
||||
<corners android:radius="@dimen/m3_alert_dialog_corner_size" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?attr/colorSurfaceContainerHigh" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/m3_popupmenu_overlay_color" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Fills the entire area with the divider's color first... -->
|
||||
<item>
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?attr/colorSurfaceVariant"/>
|
||||
</shape>
|
||||
</item>
|
||||
<!-- ..., then draws a rectangle with the container color to cover the area not for the divider. -->
|
||||
<item
|
||||
android:bottom="1dp">
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?android:attr/colorBackground"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
18
app/src/main/res/drawable/ic_gesture.xml
Normal file
18
app/src/main/res/drawable/ic_gesture.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2A10,10 0 1,0 22,12A10,10 0 0,0 12,2Z"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="@android:color/transparent"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,7l-3,3h2v4h2v-4h2z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,17l3,-3h-2v-4h-2v4h-2z"/>
|
||||
</vector>
|
||||
@@ -2,13 +2,13 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/kotatsu_surface" />
|
||||
<solid android:color="?attr/colorSurface" />
|
||||
<corners android:radius="@dimen/m3_alert_dialog_corner_size" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/kotatsu_surface" />
|
||||
<solid android:color="@color/m3_popupmenu_overlay_color" />
|
||||
<corners android:radius="@dimen/m3_alert_dialog_corner_size" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/kotatsu_surfaceContainerHighest" />
|
||||
<solid android:color="?attr/colorSurfaceContainerHigh" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/kotatsu_surfaceContainerHighest" />
|
||||
<solid android:color="@color/m3_popupmenu_overlay_color" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<item>
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/kotatsu_surfaceVariant"/>
|
||||
<solid android:color="?attr/colorSurfaceVariant"/>
|
||||
</shape>
|
||||
</item>
|
||||
<!-- ..., then draws a rectangle with the container color to cover the area not for the divider. -->
|
||||
@@ -12,7 +12,7 @@
|
||||
android:bottom="1dp">
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/kotatsu_background"/>
|
||||
<solid android:color="?android:attr/colorBackground"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -1,227 +1,156 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<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"
|
||||
android:padding="@dimen/screen_padding">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="16dp"
|
||||
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||
android:layout_marginTop="24dp"
|
||||
android:drawablePadding="@dimen/screen_padding"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/sync_title"
|
||||
android:textAppearance="?textAppearanceHeadline5"
|
||||
app:drawableTint="?colorPrimary"
|
||||
app:drawableTopCompat="@drawable/ic_sync"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:drawableTopCompat="@drawable/ic_sync" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_settings"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/settings"
|
||||
android:tooltipText="@string/settings"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_settings" />
|
||||
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||
android:layout_marginTop="@dimen/screen_padding"
|
||||
android:max="100"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/email_enter_hint"
|
||||
android:textAppearance="?textAppearanceSubtitle1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title" />
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_email"
|
||||
style="?textInputOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
app:errorIconDrawable="@null"
|
||||
app:helperText="@string/sync_auth_hint"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_email"
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textEmailAddress"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="test@mail.com" />
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:paddingBottom="@dimen/screen_padding">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sync_auth_hint"
|
||||
android:textAppearance="?textAppearanceBodySmall" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_email"
|
||||
style="?textInputOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
app:errorIconDrawable="@null">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/email"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textEmailAddress"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_password"
|
||||
style="?textInputOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
app:endIconMode="password_toggle"
|
||||
app:errorIconDrawable="@null">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/password"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="24"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_settings"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/settings" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.dockedtoolbar.DockedToolbarLayout
|
||||
android:id="@+id/docked_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="@string/next"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/enter_password"
|
||||
android:textAppearance="?textAppearanceSubtitle1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_password"
|
||||
style="?textInputOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
app:endIconMode="password_toggle"
|
||||
app:errorIconDrawable="@null"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle_2">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_password"
|
||||
<FrameLayout
|
||||
android:id="@+id/docked_toolbar_child"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="24"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
tools:text="qwerty" />
|
||||
android:layout_height="@dimen/m3_comp_toolbar_docked_container_height">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:text="@android:string/cancel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:text="@string/back"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="@string/done"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<Button
|
||||
android:id="@+id/button_next"
|
||||
style="?materialButtonTonalStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:enabled="false"
|
||||
android:text="@string/next" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="textView_subtitle,button_cancel,button_next,layout_email" />
|
||||
<Button
|
||||
android:id="@+id/button_done"
|
||||
style="?materialButtonTonalStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:enabled="false"
|
||||
android:text="@string/done"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="textView_subtitle_2,button_back,button_done,layout_password" />
|
||||
</FrameLayout>
|
||||
</com.google.android.material.dockedtoolbar.DockedToolbarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="layout_email,layout_password" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view_line"
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/bg_chip"
|
||||
android:backgroundTint="?colorError"
|
||||
app:layout_constraintBottom_toBottomOf="@id/textView_hint_body"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/textView_hint_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_hint_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/unstable_feature"
|
||||
android:textAppearance="?textAppearanceTitleMedium"
|
||||
app:drawableStartCompat="@drawable/ic_alert_outline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/view_line"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_input" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_hint_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/unstable_feature_summary"
|
||||
android:textAppearance="?textAppearanceBodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/textView_hint_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_hint_title" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?android:windowBackground"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/circularProgressIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame
|
||||
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:id="@+id/frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -16,4 +17,36 @@
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/feedbackTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:layout_margin="@dimen/screen_padding"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
android:text="@string/pull_to_prev_chapter"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
tools:alpha="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/feedbackBottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_margin="@dimen/screen_padding"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
android:text="@string/pull_to_next_chapter"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
tools:alpha="1" />
|
||||
|
||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
||||
|
||||
@@ -129,6 +129,20 @@
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
app:drawableStartCompat="@drawable/ic_split_horizontal" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_pull_gesture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:minHeight="?android:listPreferredItemHeightSmall"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/enable_pull_gesture_title"
|
||||
android:textAppearance="?textAppearanceListItem"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
app:drawableStartCompat="@drawable/ic_gesture" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_screen_rotate"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -794,4 +794,17 @@
|
||||
<string name="global_search">البحث العالمي</string>
|
||||
<string name="search_everywhere">البحث في كل مكان</string>
|
||||
<string name="badges_in_lists">الشارات في القوائم</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="reader_navigation_inverted">عكس التنقل اثناء التصفح</string>
|
||||
<string name="reader_navigation_inverted_summary">عكس اتجاه زر التحكم في الصوت ومفتاح التنقل الاتجاهي في الأجهزة (يسار/أعلى/أسفل/يمين)</string>
|
||||
<string name="clear_browser_data">مسح بيانات المتصفح</string>
|
||||
<string name="clear_browser_data_summary">مسح بيانات المتصفح، مثل ذاكرة التخزين المؤقت وملفات تعريف الارتباط. تحذير: قد يصبح التفويض في مصادر المانجا غير صالح.</string>
|
||||
<string name="no_write_permission_to_file">ليس لديك صلاحية لكتابة ملف.</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">لن تظهر المانغا المخصصة للبالغين في الاقتراحات. قد لا يعمل هذا الخيار بدقة مع بعض المصادر.</string>
|
||||
<string name="include_disabled_sources">"شمل المصادر المعطلة."</string>
|
||||
<string name="suggestions_disabled_sources_summary">"اضهار الاقتراحات من كل مصادر المانقا شامل المعطلين."</string>
|
||||
<string name="tags_warnings">ابراز التصنيفات الخطيرة</string>
|
||||
<string name="tags_warnings_summary">ابراز التصنيفات التي قد تكون غير مناسبة لغالبية المستخدين.</string>
|
||||
<string name="error_non_file_uri">لا يمكن استخدام المسار المحدد لأنه لا يشير إلى ملف أو دليل.</string>
|
||||
<string name="manga_override_hint">ستؤثر هذه التغييرات على كيفية عرض المانجا في التطبيق.</string>
|
||||
</resources>
|
||||
|
||||
3
app/src/main/res/values-arz/plurals.xml
Normal file
3
app/src/main/res/values-arz/plurals.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
3
app/src/main/res/values-arz/strings.xml
Normal file
3
app/src/main/res/values-arz/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -835,4 +835,33 @@
|
||||
<string name="theme_name_itsuka">Іцука</string>
|
||||
<string name="theme_name_totoro">Тоторо</string>
|
||||
<string name="book_effect">Жаўтлявы фон (фільтр сіняга)</string>
|
||||
<string name="local_storage_cleanup">Ачыстка лакальнага сховішча</string>
|
||||
<string name="packup_creation_failed">Рэзервовае капіраванне не атрымалася</string>
|
||||
<string name="main_screen">Галоўны экран</string>
|
||||
<string name="main_screen_fab">Паказаць плаваючую кнопку «Працягнуць»</string>
|
||||
<string name="main_screen_fab_summary">Дазваляе працягнуць чытанне адным пстрычкай мышы. Гэтая кнопка не адлюстроўваецца ў рэжыме інкогніта або калі гісторыя пустая</string>
|
||||
<string name="error_corrupted_zip">Пашкоджаны ZIP-архіў (%s)</string>
|
||||
<string name="discord_rpc">Discord Rich Presence</string>
|
||||
<string name="discord_token">Токен Discord</string>
|
||||
<string name="discord_token_summary">Увядзіце свой токен Discord, каб уключыць Rich Presence</string>
|
||||
<string name="discord_token_description">Увядзіце свой токен Discord або націсніце %s, каб атрымаць яго з дапамогай браўзера</string>
|
||||
<string name="discord_token_hint">Устаўце сюды свой токен Discord</string>
|
||||
<string name="discord_rpc_summary">Пакажыце свой статус чытання ў Discord</string>
|
||||
<string name="obtain">Атрымаць</string>
|
||||
<string name="discord_rpc_description">Чытанне мангі на Kotatsu - праграма для чытання мангі</string>
|
||||
<string name="reading_s">Чытанне %s</string>
|
||||
<string name="read_on_s">Чытаць далей %s</string>
|
||||
<string name="rpc_skip_nsfw_summary">Не выкарыстоўваць RPC для дарослага кантэнту</string>
|
||||
<string name="invalid_token">Няправільны токен: %s</string>
|
||||
<string name="show_floating_control_button">Паказаць плаваючую кнопку кіравання</string>
|
||||
<string name="unavailable">Недаступна</string>
|
||||
<string name="manga_restricted_description">Гэтая манга недаступная для чытання на гэтай крыніцы. Паспрабуйце знайсці яе на іншых крыніцах або адкрыйце ў браўзеры для атрымання дадатковай інфармацыі</string>
|
||||
<string name="no_chapters_in_manga">Гэтая манга не ўтрымлівае раздзелаў</string>
|
||||
<string name="chapters_load_failed">Не атрымалася загрузіць спіс раздзелаў</string>
|
||||
<string name="telegram_integration">Інтэграцыя з Telegram</string>
|
||||
<string name="pull_top_no_prev">Няма папярэдняй главы</string>
|
||||
<string name="pull_bottom_no_next">Няма наступнай главы</string>
|
||||
<string name="enable_pull_gesture_title">Уключыць жэст перацягвання</string>
|
||||
<string name="enable_pull_gesture_summary">Выкарыстоўвайце жэст пацягвання, каб пераключацца паміж главамі ў манхве</string>
|
||||
<string name="test_parser">Праверыць крыніцу мангі</string>
|
||||
</resources>
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<string name="manga_shelf">تاقچه</string>
|
||||
<string name="not_available">در دسترس نیست</string>
|
||||
<string name="cannot_find_available_storage">محل ذخیره سازی قابل دسترسی ئی یافت نشد</string>
|
||||
<string name="scale_mode">حالت scale</string>
|
||||
<string name="scale_mode">مقیاس</string>
|
||||
<string name="zoom_mode_fit_center">متناسب به مرکز</string>
|
||||
<string name="zoom_mode_keep_start">نگه داشتن در شروع</string>
|
||||
<string name="reverse">معکوس</string>
|
||||
@@ -280,4 +280,333 @@
|
||||
<string name="report">گزارش</string>
|
||||
<string name="status_planned">برنامهریزی شده</string>
|
||||
<string name="show_reading_indicators_summary">نشان دادن درصد خوانده شده در تاریخچه و پسندیدهها</string>
|
||||
<string name="appwidget_recent_description">مانگا های اخیراً خوانده شده</string>
|
||||
<string name="clear_cookies_summary">میتواند در صورت بروز برخی مشکلات کمک کند. همه مجوزها باطل خواهند شد</string>
|
||||
<string name="show_all">نمایش همه</string>
|
||||
<string name="invalid_domain_message">دامنه نامعتبر</string>
|
||||
<string name="invalid_server_address_message">آدرس سرور نامعتیر</string>
|
||||
<string name="select_range">انتخاب بازه</string>
|
||||
<string name="clear_all_history">پاک کردن همه تاریخچه</string>
|
||||
<string name="last_2_hours">۲ ساعت گذشته</string>
|
||||
<string name="history_cleared">تاریخچه پاک شد</string>
|
||||
<string name="manage">مدیریت</string>
|
||||
<string name="no_bookmarks_yet">هنوز هیچ نشانکی وجود ندارد</string>
|
||||
<string name="no_bookmarks_summary">میتوانید هنگام خواندن مانگا نشانک ایجاد کنید</string>
|
||||
<string name="bookmarks_removed">نشانک حذف شد</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="explore">کاوش</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="removed_from_favourites">از مورد علاقهها حذف شد</string>
|
||||
<string name="options">گزینه ها</string>
|
||||
<string name="not_found_404">محتوا حذف شده یا پیدا نشد</string>
|
||||
<string name="incognito_mode">حالت ناشناس</string>
|
||||
<string name="no_chapters">بدون فصل</string>
|
||||
<string name="automatic_scroll">پیمایش خودکار</string>
|
||||
<string name="reader_info_pattern">چپتر.%2$d%1$dصفحه.%4$d%3$d</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="import_completed">وارد کردن کامل شد</string>
|
||||
<string name="import_completed_hint">شما میتوانید پرونده اصلی را از حافظه برای آزاد سازی فضای ذخیره سازی پاک کنید</string>
|
||||
<string name="import_will_start_soon">وارد کردن به زودی آغاز خواهد شد</string>
|
||||
<string name="feed">خوراک</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="color_correction">تصحیح رنگ</string>
|
||||
<string name="brightness">روشنایی</string>
|
||||
<string name="contrast">کنتراست</string>
|
||||
<string name="reset">بازنشانی</string>
|
||||
<string name="text_unsaved_changes_prompt">ذخیره یا لغو تغییرات ذخیره نشده؟</string>
|
||||
<string name="discard">لغو</string>
|
||||
<string name="error_no_space_left">هیچ فضایی روی دستگاه باقی نمانده است</string>
|
||||
<string name="reader_slider">نمایش اسلایدر تعویض صفحه</string>
|
||||
<string name="webtoon_zoom">بزرگ نمایی وبتوون</string>
|
||||
<string name="network_unavailable">شبکه در دسترس نیست</string>
|
||||
<string name="network_unavailable_hint">برای خواندن مانگا به صورت برخط، وایفای یا دیتای تلفن همراه را روشن کنید</string>
|
||||
<string name="server_error">خطای سمت سرویس دهنده (%1$d). لطفا بعداً دوباره تلاش کنید</string>
|
||||
<string name="clear_new_chapters_counters">اطلاعات مربوط به فصل های جدید رو هم پاک کن</string>
|
||||
<string name="compact">فشرده</string>
|
||||
<string name="source_disabled">منبع غیر فعال شده است</string>
|
||||
<string name="prefetch_content">پیش بازگذاری محتوا</string>
|
||||
<string name="mark_as_current">نشانه گذاری به عنوان فعلی</string>
|
||||
<string name="language">زبان</string>
|
||||
<string name="share_logs">اشتراک گذاری گزارش</string>
|
||||
<string name="enable_logging">فعال کردن گزارشات</string>
|
||||
<string name="enable_logging_summary">ثبت برخی عمیات برای اهداف اشکال زدایی. اگه نمیدونی داری چیکار میکنی این گزینه رو روشن نکن</string>
|
||||
<string name="show_suspicious_content">نمایش محتوای مشکوک</string>
|
||||
<string name="theme_name_dynamic">پویا</string>
|
||||
<string name="theme_name_expressive">صریح (تست)</string>
|
||||
<string name="color_theme">طرح رنگ</string>
|
||||
<string name="show_in_grid_view">نمایش به حالت شبکه ای</string>
|
||||
<string name="theme_name_miku">میکو</string>
|
||||
<string name="theme_name_asuka">آسوکا</string>
|
||||
<string name="theme_name_mion">میون</string>
|
||||
<string name="theme_name_rikka">ریکا</string>
|
||||
<string name="theme_name_sakura">ساکورا</string>
|
||||
<string name="theme_name_mamimi">مامیمی</string>
|
||||
<string name="theme_name_kanade">کاناده</string>
|
||||
<string name="nothing_here">چیزی اینجا نیست</string>
|
||||
<string name="scrobbling_empty_hint">برای فعال کردن پیگیری پیشرفت خواندن، منو را انتخاب کنید ← پیگیری در صفحه جزئیات مانگا.</string>
|
||||
<string name="services">خدمات</string>
|
||||
<string name="allow_unstable_updates">اجازه بروزرسانی های ناپایدار</string>
|
||||
<string name="allow_unstable_updates_summary">دریافت اعلان درباره ورژن های ناپایدار</string>
|
||||
<string name="download_started">بارگیری آغاز شد</string>
|
||||
<string name="got_it">متوجه شدم</string>
|
||||
<string name="sources_reorder_tip">برای مرتبسازی مجدد آیتمها، روی آنها ضربه بزنید و نگه دارید</string>
|
||||
<string name="user_agent">هدر UserAgent</string>
|
||||
<string name="settings_apply_restart_required">لطفا برای اعمال این تغییرات، برنامه را مجددا راه اندازی کنید</string>
|
||||
<string name="comics_archive_import_description">میتونی یک یا چند فایل .cbz یا .zip رو انتخاب کنی، هر فایل به صورت یک مانگای مجزا شناخته میشه.</string>
|
||||
<string name="folder_with_images_import_description">میتونی یک پوشه حاوی آرشیو ها یا تصاویر انتخاب کنی. هر آرشیو (یا زیرشاخه) به عنوان یک فصل شناخته میشن.</string>
|
||||
<string name="speed">سرعت</string>
|
||||
<string name="show_on_shelf">نمایش در ققسه</string>
|
||||
<string name="sync_auth_hint">میتونی وارد یک اکانت بشی یا یک اکانت جدید بسازی</string>
|
||||
<string name="find_similar">پیدا کردن مشابه</string>
|
||||
<string name="sync_settings">تنظیمات همگام سازی</string>
|
||||
<string name="server_address">آدرس سرور</string>
|
||||
<string name="sync_host_description">میتوانید از یک سرور همگامسازی خود-میزبان یا یک سرور پیشفرض استفاده کنید. اگر مطمئن نیستید که چه کاری انجام میدهید، این را تغییر ندهید.</string>
|
||||
<string name="ignore_ssl_errors">نادیده گرفتن خطا های SSL</string>
|
||||
<string name="mirror_switching">انتخاب خودکار آینه</string>
|
||||
<string name="mirror_switching_summary">در صورت وجود آینه، دامنهها را برای منابع مانگا به صورت خودکار تغییر دهید</string>
|
||||
<string name="pause">توقف</string>
|
||||
<string name="resume">ازسرگیری</string>
|
||||
<string name="paused">متوقف شده</string>
|
||||
<string name="remove_completed">منوی آیتم؛ اقدام برای حذف آیتم های کامل شده</string>
|
||||
<string name="cancel_all">لغو همه</string>
|
||||
<string name="downloads_wifi_only">بارگیری فقط از طریق وایفای</string>
|
||||
<string name="downloads_wifi_only_summary">توقف دانلود در زمان تغییرشبکه به داده تلفن همراه</string>
|
||||
<string name="suggestion_manga">پیشنهاد:%s</string>
|
||||
<string name="suggestions_notifications_summary">گاهی اوقات اعلانهایی با مانگا پیشنهادی نمایش داده میشود</string>
|
||||
<string name="more">بیشتر</string>
|
||||
<string name="enable">فعال</string>
|
||||
<string name="no_thanks">نه ممنون</string>
|
||||
<string name="cancel_all_downloads_confirm">تمام دانلودهای فعال لغو میشوند، دادههایی که بهطور ناقص دانلود شدهاند از بین میروند</string>
|
||||
<string name="remove_completed_downloads_confirm">تاریخچه بارگیری شما برای همیشه حذف خواهد شد. شامل فایل های بارگیری شده نمیشود</string>
|
||||
<string name="text_downloads_list_holder">شما هیچ بارگیری ندارید</string>
|
||||
<string name="downloads_paused">بارگیری ها متوقف شده اند</string>
|
||||
<string name="downloads_removed">بارگیری ها حذف شده اند</string>
|
||||
<string name="web_view_unavailable">وب ویو در دسترس نیست: بررسی کنید ارائه دهنده وب ویو نصب شده باشد</string>
|
||||
<string name="clear_network_cache">پاک کردن حافظه نهان شبکه</string>
|
||||
<string name="type">نویسه</string>
|
||||
<string name="address">آدرس</string>
|
||||
<string name="port">درگاه</string>
|
||||
<string name="proxy">پروکسی</string>
|
||||
<string name="invalid_value_message">مقدار نامعتبر</string>
|
||||
<string name="email_password_enter_hint">برای ادامه رایانامه و گذرواژه را وارد کنید</string>
|
||||
<string name="downloaded">بارگیری شده</string>
|
||||
<string name="images_proxy_title">پروکسی بهینه سازی تصاویر</string>
|
||||
<string name="images_procy_description">استفاده از سرویس wsrv.nl (در صورت امکان) برای کاهش مصرف ترافیک و افزایش سرعت بارگیری تصویر</string>
|
||||
<string name="invert_colors">معکوس کردن رنگ ها</string>
|
||||
<string name="username">نام کاربری</string>
|
||||
<string name="password">گذرواژه</string>
|
||||
<string name="authorization_optional">اعتبار سنجی (اختیاری)</string>
|
||||
<string name="invalid_port_number">شماره درگاه نامعتبر</string>
|
||||
<string name="network">شبکه</string>
|
||||
<string name="data_and_privacy">داده ها و حریم خصوصی</string>
|
||||
<string name="restore_summary">بازیابی نسخه پشتیبان تهیه شده قبلی</string>
|
||||
<string name="webtoon_zoom_summary">امکان زوم در حالت وبتون</string>
|
||||
<string name="reader_info_bar_summary">نمایش زمان و پیشرفت کنونی در بالای صفحه نمایش</string>
|
||||
<string name="show_pages_numbers_summary">نمایش شماره صفحه در گوشیه پایین صفحه</string>
|
||||
<string name="clear_source_cookies_summary">پاک کردن کوکی ها برای یک دامنه خاص. در بیشتر موارد، مجوز را باطل میکند</string>
|
||||
<string name="download_option_all_chapters">تمام فصل ها ترجمه شده %s</string>
|
||||
<string name="download_option_whole_manga">تمام مانگا</string>
|
||||
<string name="download_option_first_n_chapters">ابتدا %s</string>
|
||||
<string name="download_option_next_unread_n_chapters">خوانده نشده بعدی %s</string>
|
||||
<string name="download_option_all_unread">تمام فصل های خوانده نشده</string>
|
||||
<string name="download_option_all_unread_b">تمام فصل های خوانده نشده (%s)</string>
|
||||
<string name="download_option_manual_selection">انتخاب فصل به صورت دستی</string>
|
||||
<string name="pick_custom_directory">انتخاب پوشه سفارشی</string>
|
||||
<string name="no_access_to_file">شما دسترسی به این پرونده یا پوشه را ندارید</string>
|
||||
<string name="local_manga_directories">پوشه های مانگای محلی</string>
|
||||
<string name="description">توضیحات</string>
|
||||
<string name="this_month">این ماه</string>
|
||||
<string name="voice_search">جستجوی صوتی</string>
|
||||
<string name="related_manga">مانگا های مرتبط</string>
|
||||
<string name="color_light">حالت روز</string>
|
||||
<string name="color_dark">حالت شب</string>
|
||||
<string name="color_white">سفید</string>
|
||||
<string name="color_black">مشکی</string>
|
||||
<string name="background">پسزمینه</string>
|
||||
<string name="data_not_restored">داده ها بازیابی نشدند</string>
|
||||
<string name="data_not_restored_text">از انتخاب درست پرونده پشتیبان اطمینان حاصل کنید</string>
|
||||
<string name="manage_categories">مدیریت دسته بندی ها</string>
|
||||
<string name="suggestions_wifi_only_summary">بهروزرسانی نکردن پیشنهادها با استفاده از اتصالات شبکه محدود</string>
|
||||
<string name="tracker_wifi_only_summary">عدم بررسی بروزرسانی فصل ها با اتصال شبکه محدود</string>
|
||||
<string name="search_hint">عنوان مانگا را وارد کنید، ژانر یا نام منبع</string>
|
||||
<string name="progress">پیشرفت</string>
|
||||
<string name="order_added">اضافه شده</string>
|
||||
<string name="show">نمایش</string>
|
||||
<string name="captcha_required_summary">%s نیازمند حل کردن کپچا برای کار کردن است</string>
|
||||
<string name="languages">زبان ها</string>
|
||||
<string name="unknown">ناشناخته</string>
|
||||
<string name="in_progress">درحال انجام</string>
|
||||
<string name="disable_nsfw">غیر فعال سازی NSFW</string>
|
||||
<string name="too_many_requests_message">درخواستها خیلی زیاد است. بعداً دوباره امتحان کنید</string>
|
||||
<string name="too_many_requests_message_retry">درخواستها خیلی زیاد است. بعدا از %s دوباره تلاش کنید</string>
|
||||
<string name="related_manga_summary">نمایش یک فهرست از مانگا های مرتبط. در برخی موارد ممکن است اشتباه یا ناقص باشد</string>
|
||||
<string name="advanced">پیشرفته</string>
|
||||
<string name="manga_list">مدیریت فهرست</string>
|
||||
<string name="error_corrupted_file">داده نامعتبر است یا پرونده خراب است</string>
|
||||
<string name="on_device">روی دستگاه</string>
|
||||
<string name="directories">پوشه ها</string>
|
||||
<string name="main_screen_sections">بخشهای صفحه اصلی</string>
|
||||
<string name="items_limit_exceeded">نمیتوان آیتم های بیشتری اضافه کرد</string>
|
||||
<string name="to_top">به بالا</string>
|
||||
<string name="moved_to_top">به بالا منتقل شد</string>
|
||||
<string name="zoom_out">کوچک نمایی</string>
|
||||
<string name="zoom_in">بزرگ نمایی</string>
|
||||
<string name="reader_zoom_buttons">نمایش دکمه های بزرگنمایی</string>
|
||||
<string name="reader_zoom_buttons_summary">اینکه آیا دکمههای کنترل بزرگنمایی در گوشه پایین سمت راست نمایش داده شوند یا خیر</string>
|
||||
<string name="keep_screen_on">روشن نگه داشتن صفحه نمایش</string>
|
||||
<string name="keep_screen_on_summary">جلوگیری از خاموش شدن صفحه نمایش در زمان خواندن مانگاه</string>
|
||||
<string name="state_abandoned">رها شده</string>
|
||||
<string name="enhanced_colors_summary">باندینگ را کاهش میدهد، اما ممکن است بر عملکرد تأثیر بگذارد</string>
|
||||
<string name="enhanced_colors">حالت رنگ ۳۲ بیتی</string>
|
||||
<string name="suggest_new_sources">پیشنهاد منابع جدید بعد از بروزرسانی برنامه</string>
|
||||
<string name="suggest_new_sources_summary">درخواست فعال کردن منابع تازه اضافه شده پس از بهروزرسانی برنامه</string>
|
||||
<string name="list_options">گزینه های فهرست</string>
|
||||
<string name="by_relevance">ارتباط</string>
|
||||
<string name="categories">دسته بندی ها</string>
|
||||
<string name="online_variant">نوع برخط</string>
|
||||
<string name="periodic_backups">پشتیبانگیریهای دورهای</string>
|
||||
<string name="backup_frequency">فرکانس ایجاد نسخه پشتیبان</string>
|
||||
<string name="frequency_every_day">هر روز</string>
|
||||
<string name="frequency_every_2_days">هر ۲ دوز</string>
|
||||
<string name="frequency_once_per_week">هر هفته</string>
|
||||
<string name="frequency_twice_per_month">دوبار در ماه</string>
|
||||
<string name="frequency_once_per_month">یک بار درماه</string>
|
||||
<string name="periodic_backups_enable">فعال کردن پشتیبان گیری دوره ای</string>
|
||||
<string name="backups_output_directory">پوشه خروجی پشتیبان گیری</string>
|
||||
<string name="last_successful_backup">آخرین پشتیبان گیری موفق:%s</string>
|
||||
<string name="lock_screen_rotation">قفل چرخش صفحه</string>
|
||||
<string name="content_type_manga">مانگا</string>
|
||||
<string name="content_type_hentai">هنتای</string>
|
||||
<string name="content_type_comics">کامیک بوک</string>
|
||||
<string name="content_type_other">دیگر</string>
|
||||
<string name="sources_catalog">کاتالوگ منابع</string>
|
||||
<string name="source_enabled">منبع فعال شد</string>
|
||||
<string name="no_manga_sources_catalog_text">هیچ منبع فعالی در این بخش وجود ندارد، یا همه آنها قبلا اضافه شده اند.\nبا ما همراه باشید</string>
|
||||
<string name="no_manga_sources_found">هیچ منبع فعال بر اساس جستجوی شما یافت نشد</string>
|
||||
<string name="catalog">کاتالوگ</string>
|
||||
<string name="manage_sources">مدیریت منابع</string>
|
||||
<string name="manual">دستی</string>
|
||||
<string name="available_d">دردسترس: %1$d</string>
|
||||
<string name="disable_nsfw_summary">در صورت امکان مانگای بزرگسالان و منابع NSFW را غیر فعال کنید</string>
|
||||
<string name="state_paused">متوقف شده</string>
|
||||
<string name="reader_optimize">کاهش مصرف حافظه (آزمایشی)</string>
|
||||
<string name="reader_optimize_summary">کاهش کیفیت صفحات خارج از صفحه برای استفاده کمتر از حافظه</string>
|
||||
<string name="state">حالت</string>
|
||||
<string name="downloads_resumed">بارگیریها ازسر گرفته شدهاند</string>
|
||||
<string name="downloads_cancelled">بارگیریها لغو شدهاند</string>
|
||||
<string name="suggestions_enable_prompt">آیا میخواهید پیشنهادهای مانگای شخصی شده دریافت کنید؟</string>
|
||||
<string name="error_multiple_genres_not_supported">فیلتر کردن با چندین ژانر توسط این منبع مانگا پشتیبانی نمیشود</string>
|
||||
<string name="error_multiple_states_not_supported">فیلتر کردن با چندین وضعیت توسط این منبع مانگا پشتیبانی نمیشود</string>
|
||||
<string name="error_search_not_supported">جستجو کردن توسط این منبع مانگا پشتیبانی نمیشود</string>
|
||||
<string name="downloads_settings_info">اگر با مسدود شدن از طرف سرویس دهنده میشکل دارید، میتوانید کند کننده بارگیری را برای هر منبع مانگاه به صورت جداگانه در تنظیمات منبع فعال کنید</string>
|
||||
<string name="skip">رد شدن</string>
|
||||
<string name="grayscale">حالت خاکستری</string>
|
||||
<string name="globally">سراسری</string>
|
||||
<string name="this_manga">همین مانگا</string>
|
||||
<string name="color_correction_apply_text">این تنظیمها میتوانند برای همهی مانگاها یا فقط همین مانگا اعمال شوند. اگر برای همه اعمال شوند، تنظیمهای تکبهتک دستنخورده میمانند.</string>
|
||||
<string name="apply">اعمال</string>
|
||||
<string name="error_filter_locale_genre_not_supported">این منبع از فیلتر همزمان بر پایهی ژانر و زبان پشتیبانی نمیکند</string>
|
||||
<string name="error_filter_states_genre_not_supported">این منبع از فیلتر همزمان بر پایهی ژانر و وضعیت پشتیبانی نمیکند</string>
|
||||
<string name="genres_search_hint">برای شروع ژانر مورد نظر خود را بنویسید</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">اگر مشکلی با بارگیری دارید،ممکن است به شروع شدن آن کمک کند</string>
|
||||
<string name="welcome_text">لطفا منابع محتوا مورد نظر خود را انتخاب کنید. این بخش را میتوان بعدا از طریق تنظیمات تغییر داد</string>
|
||||
<string name="sync_auth">برای همگام سازی حساب وارد شوید</string>
|
||||
<string name="restore">بازیابی</string>
|
||||
<string name="backup_date_">تاریخ پشتیبان: %s</string>
|
||||
<string name="state_upcoming">پیشرو</string>
|
||||
<string name="by_name_reverse">نام برعکس</string>
|
||||
<string name="content_rating">امتیاز دهی به محتوا</string>
|
||||
<string name="genres_exclude">نادیده گرفتن ژانر ها</string>
|
||||
<string name="rating_safe">امن</string>
|
||||
<string name="rating_suggestive">تحریکآمیز</string>
|
||||
<string name="rating_adult">بزرگسال</string>
|
||||
<string name="default_tab">صفحه پیشفرض</string>
|
||||
<string name="mark_as_completed">علامت به عنوان تمام شده</string>
|
||||
<string name="mark_as_completed_prompt">علامت زدن مانگا به عنوان کاملا خوانده شده؟\n\nاخطار: پیشرفت فعلی از دست خواهد رفت.</string>
|
||||
<string name="category_hidden_done">دسته بندی از صفحه اصلی پنهان شده و میتوان از طریق منو ← دسته بندی مانگا ها به ان دسترسی داشت</string>
|
||||
<string name="volume_">جلد %d</string>
|
||||
<string name="volume_unknown">جلد نامشخص</string>
|
||||
<string name="incognito_mode_hint">پیشرفت خواندن شما ذخیره نخواهد شد</string>
|
||||
<string name="vertical">عمودی</string>
|
||||
<string name="last_read">اخرین خوانده شده</string>
|
||||
<string name="show_menu">نمایش فهرست</string>
|
||||
<string name="toggle_ui">نمایش/پنهان سازی رابط کاربری</string>
|
||||
<string name="prev_chapter">فصل قبل</string>
|
||||
<string name="next_chapter">فصل بعد</string>
|
||||
<string name="prev_page">صفحه قبل</string>
|
||||
<string name="next_page">صفحه بعد</string>
|
||||
<string name="reader_actions">کنشهای خواننده</string>
|
||||
<string name="reader_actions_summary">پیکربندی کنش ها برای قسمت های قابل لمس صفحه</string>
|
||||
<string name="switch_pages_volume_buttons">بهکار انداختن دکمههای صدا</string>
|
||||
<string name="switch_pages_volume_buttons_summary">استفاده از دکمه های صدا برای عوض کردن صفحات</string>
|
||||
<string name="reader_navigation_inverted">معکوس کردن کنترل های ناوبری</string>
|
||||
<string name="reader_navigation_inverted_summary">عوض کردن جهت دکمههای صدا و کلیدهای سختافزاری (چپ/راست/بالا/پایین)</string>
|
||||
<string name="tap_action">کنش های لمسی</string>
|
||||
<string name="long_tap_action">کنش های لمس طولانی</string>
|
||||
<string name="none">هیچکدام</string>
|
||||
<string name="config_reset_confirm">تنظیمات با مقادیر پیشفرض بازنویسی شود؟ این عمل قابل بازگشت نیست.</string>
|
||||
<string name="use_two_pages_landscape">نمایش دو صفحه در حالت افقی (آزمایشی)</string>
|
||||
<string name="default_webtoon_zoom_out">کوچکنمایی پیشفرض وبتون</string>
|
||||
<string name="fullscreen_mode">حالت تمام صفحه</string>
|
||||
<string name="reader_fullscreen_summary">پنهان کردن نوار وضعیت و راهبری</string>
|
||||
<string name="reading_time_estimation">نمایش زمان خواندن تخمینی</string>
|
||||
<string name="reading_time_estimation_summary">مقدار زمان تخمینی ممکن است نادرست باشد</string>
|
||||
<string name="suggestions_unavailable_text">پیشنهادات غیر فعال است</string>
|
||||
<string name="check_for_new_chapters_disabled">بررسی برای فصول جدید غیر فعال است</string>
|
||||
<string name="show_labels_in_navbar">نمایش برچسبها در نوار راهبری</string>
|
||||
<string name="pages_saving">ذخیرهسازی صفحهها</string>
|
||||
<string name="ask_for_dest_dir_every_time">هر بار پوشهی مقصد را بپرس</string>
|
||||
<string name="default_page_save_dir">پوشه پیشفرض ذخیره سازی صفحات</string>
|
||||
<string name="remove_from_history">پاک کردن از تاریخچه</string>
|
||||
<string name="location">مکان</string>
|
||||
<string name="preferred_download_format">قالب بارگیری ترجیحی</string>
|
||||
<string name="single_cbz_file">پروندهٔ CBZ تکی</string>
|
||||
<string name="multiple_cbz_files">پروندهٔ CBZ چندتایی</string>
|
||||
<string name="reading_stats">آمار خواندن</string>
|
||||
<string name="other_manga">مانگا های دیگر</string>
|
||||
<string name="less_than_minute">کمتر از یک دقیقه</string>
|
||||
<string name="statistics">آمار</string>
|
||||
<string name="clear_stats">پاک کردن آمار ها</string>
|
||||
<string name="stats_cleared">آمار ها پاک شدند</string>
|
||||
<string name="clear_stats_confirm">آیا قصد پاک کردن تمام آمار ها را دارید؟ این عمل قابل بازگشت نیست.</string>
|
||||
<string name="week">هفته</string>
|
||||
<string name="month">ماه</string>
|
||||
<string name="all_time">همه</string>
|
||||
<string name="day">روز</string>
|
||||
<string name="three_months">سه ماه</string>
|
||||
<string name="empty_stats_text">هیچ آماری برای دوره انتخاب شده وجود ندارد</string>
|
||||
<string name="pages_read_s">صفحات خوانده شده: %s</string>
|
||||
<string name="alternatives">جایگزینها</string>
|
||||
<string name="migrate">انتقال</string>
|
||||
<string name="migrate_confirmation">مانگا «%1$s» از «%2$s» با «%3$s» از «%4$s» (در صورت وجود داشتن) در تاریخچه و مورد علاقه ها جایگزین خواهد شد</string>
|
||||
<string name="manga_migration">انتقال مانگا</string>
|
||||
<string name="migration_completed">انتقال انجام شد</string>
|
||||
<string name="delete_read_chapters">پاک کردن فصل های خوانده شده</string>
|
||||
<string name="no_chapters_deleted">هیچ فصلی پاک نشد</string>
|
||||
<string name="chapters_deleted_pattern">%1$s حذف شده، %2$s پاک شده</string>
|
||||
<string name="delete_read_chapters_summary">فصل های خوانده شده برای آزاد سازی فضا ذخیره سازی حذف کنید</string>
|
||||
<string name="delete_read_chapters_prompt">این کار تمام فصل هایی که به عنوان خوانده شده علامت گزاری شده اند را برای همیشه پاک خواهد کرد. شما میتونید بعدا آن هارا دانلود کنید اما فصل های افزودهشده ممکن است برای همیشه پاک شوند</string>
|
||||
<string name="delete_read_chapters_auto">حذف فصل های خوانده شده به صورت خودکار</string>
|
||||
<string name="runs_on_app_start">با آغاز برنامه اجرا میشود</string>
|
||||
<string name="split_by_translations">بخشبندی بر اساس ترجمهه</string>
|
||||
<string name="split_by_translations_summary">نمایش فصل ها با ترجمه های مختلف به صورت جداگانه بجای نمایش در یک لیست</string>
|
||||
<string name="order_oldest">قدیمی ترین</string>
|
||||
</resources>
|
||||
|
||||
@@ -856,4 +856,5 @@
|
||||
<string name="manga_restricted_description">Ce manga n’est pas disponible à la lecture sur cette source. Essaie de le chercher dans d’autres sources ou ouvre-le dans un navigateur pour plus d’informations</string>
|
||||
<string name="no_chapters_in_manga">Ce manga ne contient aucun chapitre</string>
|
||||
<string name="chapters_load_failed">Échec du chargement de la liste des chapitres</string>
|
||||
<string name="telegram_integration">Intégration Telegram</string>
|
||||
</resources>
|
||||
|
||||
19
app/src/main/res/values-got/plurals.xml
Normal file
19
app/src/main/res/values-got/plurals.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">%1$d 𐍈𐌴𐌹𐌻𐌰</item>
|
||||
<item quantity="other">%1$d 𐍈𐌴𐌹𐌻𐍉𐍃</item>
|
||||
</plurals>
|
||||
<plurals name="minutes">
|
||||
<item quantity="one">%1$d 𐌼𐌹𐌽𐌿𐍄𐌿𐍃</item>
|
||||
<item quantity="other">%1$d 𐌼𐌹𐌽𐌿𐍄𐌾𐌿𐍃</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">𐍆𐌰𐌿𐍂𐌰 %1$d 𐌳𐌰𐌲</item>
|
||||
<item quantity="other">𐍆𐌰𐌿𐍂𐌰 %1$d 𐌳𐌰𐌲𐌰𐌽𐍃</item>
|
||||
</plurals>
|
||||
<plurals name="months_ago">
|
||||
<item quantity="one">𐍆𐌰𐌿𐍂𐌰 𐌼𐌴𐌽𐍉𐌸 %1$d</item>
|
||||
<item quantity="other">𐍆𐌰𐌿𐍂𐌰 𐌼𐌴𐌽𐍉𐌸𐌿𐌼 %1$d</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
11
app/src/main/res/values-got/strings.xml
Normal file
11
app/src/main/res/values-got/strings.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="history">𐍃𐍀𐌹𐌻𐌻</string>
|
||||
<string name="error_occurred">𐌰𐌹𐍂𐌶𐌴𐌹 𐍅𐌰𐍂𐌸</string>
|
||||
<string name="remote_sources">𐌼𐌰𐌲𐌲𐌹𐌽𐍃 𐌱𐍂𐌿𐌽𐌽𐌰𐌽𐍃</string>
|
||||
<string name="computing_">𐍂𐌰𐌷𐌽𐌾𐌰𐌳𐌰…</string>
|
||||
<string name="read">𐌰𐌽𐌰𐌺𐌿𐌽𐌽𐌰𐌽</string>
|
||||
<string name="share_s">%s 𐌳𐌰𐌹𐌻𐌾𐌰𐌽</string>
|
||||
<string name="search">𐍃𐍉𐌺𐌾𐌰𐌽</string>
|
||||
<string name="search_manga">𐌼𐌰𐌲𐌲𐌰 𐍃𐍉𐌺𐌾𐌰𐌽</string>
|
||||
</resources>
|
||||
@@ -44,12 +44,12 @@
|
||||
<string name="nothing_found">Ništa nije pronađeno</string>
|
||||
<string name="read">Čitaj</string>
|
||||
<string name="you_have_not_favourites_yet">Još nema favorita</string>
|
||||
<string name="add_to_favourites">Označite ovo kao favorit</string>
|
||||
<string name="add_to_favourites">Dodaj u favorite</string>
|
||||
<string name="add_new_category">Nova kategorija</string>
|
||||
<string name="add">Dodaj</string>
|
||||
<string name="save">Sačuvaj</string>
|
||||
<string name="share">Podijeli</string>
|
||||
<string name="create_shortcut">Napravi prečicu…</string>
|
||||
<string name="create_shortcut">Napravi prečicu</string>
|
||||
<string name="share_s">Podijeli %s</string>
|
||||
<string name="search">Pretraži</string>
|
||||
<string name="search_manga">Pretraži mangu</string>
|
||||
@@ -266,7 +266,7 @@
|
||||
<string name="enable_logging">Omogući bilježenje</string>
|
||||
<string name="enable_logging_summary">Snimite neke radnje u svrhu otklanjanja pogrešaka. Nemojte ga uključivati ako niste sigurni što radite</string>
|
||||
<string name="show_suspicious_content">Prikaži sumnjiv sadržaj</string>
|
||||
<string name="theme_name_dynamic">Dinamičan</string>
|
||||
<string name="theme_name_dynamic">Dinamična</string>
|
||||
<string name="color_theme">Shema boja</string>
|
||||
<string name="show_in_grid_view">Prikaži u mrežnom prikazu</string>
|
||||
<string name="theme_name_miku">Miku</string>
|
||||
@@ -474,7 +474,7 @@
|
||||
<string name="file_not_found">Datoteka nije pronađena</string>
|
||||
<string name="data_restored_success">Svi podaci su vraćeni</string>
|
||||
<string name="data_restored_with_errors">Podaci su vraćeni, ali ima grešaka</string>
|
||||
<string name="backup_information">Možete stvoriti sigurnosnu kopiju svoje povijesti i favorita i vratiti je</string>
|
||||
<string name="backup_information">Možete stvoriti sigurnosnu kopiju svoje povijesti i favorita i obnoviti je</string>
|
||||
<string name="yesterday">Jučer</string>
|
||||
<string name="long_ago">Davno</string>
|
||||
<string name="data_restored">Vraćeno</string>
|
||||
@@ -782,4 +782,24 @@
|
||||
<string name="global_search">Globalna pretraga</string>
|
||||
<string name="search_everywhere">Traži svuda</string>
|
||||
<string name="badges_in_lists">Značke u popisima</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="theme_name_expressive">Ekspresivna (test)</string>
|
||||
<string name="chapter_volume_number">Svezak %1$s Poglavlje %2$s</string>
|
||||
<string name="chapter_number">Poglavlje %s</string>
|
||||
<string name="unnamed_chapter">Neimenovano poglavlje</string>
|
||||
<string name="search_disabled_sources">Pretraži deaktivirane izvore</string>
|
||||
<string name="error_details">Detalji greške</string>
|
||||
<string name="error_disclaimer_manga">Pokušaj otvoriti manga u web pregledniku kako bi provjerio/la je li dostupan na izvornom mjestu.</string>
|
||||
<string name="error_disclaimer_app_outdated">Izgleda da je tvoja Kotatsu verzija zastarjela. Instaliraj najnoviju verziju za dobivanje svih dostupnih ispravki.</string>
|
||||
<string name="link_to_manga_in_app">Poveznica na manga u Kotatsu</string>
|
||||
<string name="link_to_manga_on_s">Poveznica na manga na %s</string>
|
||||
<string name="error_disclaimer_report">Pošalji izvještaj o grešci programerima. To će nam pomoći da istražimo i ispravimo problem.</string>
|
||||
<string name="clear_browser_data">Izbriši podatke iz preglednika</string>
|
||||
<string name="clear_browser_data_summary">Izbriši podatke iz preglednika poput predmemorije i kolačića. Upozorenje: Autorizacija na izvorima mange može postati nevažeća</string>
|
||||
<string name="no_write_permission_to_file">Nema dozvolu za pisanje u datoteku</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">Manga za odrasle se neće prikazivati u prijedlozima. Ova opcija možda neće točno funkcionirati s nekim izvorima</string>
|
||||
<string name="include_disabled_sources">Uključi deaktivirane izvore</string>
|
||||
<string name="suggestions_disabled_sources_summary">Prikaži prijedloge iz svih izvora mange, uključujući deaktivirane</string>
|
||||
<string name="tags_warnings">Istakni opasne žanrove</string>
|
||||
<string name="tags_warnings_summary">Istakni žanrove koji bi mogli biti neprikladni za većinu korisnika</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="items">
|
||||
<item quantity="other">%1$d butir</item>
|
||||
<item quantity="other">”%1$d butir</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="other">%1$d bab baru</item>
|
||||
<item quantity="other">%1$d Jilid Baru</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="other">%1$d bab</item>
|
||||
<item quantity="other">%1$d Jilid</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="other">%1$d menit yang lalu</item>
|
||||
@@ -27,4 +27,4 @@
|
||||
<plurals name="minutes">
|
||||
<item quantity="other">%1$d menit</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<string name="search_manga">Cari manga</string>
|
||||
<string name="manga_downloading_">Mengunduh…</string>
|
||||
<string name="processing_">Memproses…</string>
|
||||
<string name="download_complete">Diunduh</string>
|
||||
<string name="downloads">Unduhan</string>
|
||||
<string name="download_complete">Mengambil data (unduh)</string>
|
||||
<string name="downloads">Ambil data (Unduh)</string>
|
||||
<string name="by_name">Nama</string>
|
||||
<string name="popular">Populer</string>
|
||||
<string name="updated">Diperbarui</string>
|
||||
@@ -65,7 +65,7 @@
|
||||
<string name="text_delete_local_manga">Hapus \"%s\" dari perangkat secara permanen?</string>
|
||||
<string name="reader_settings">Pengaturan pembaca</string>
|
||||
<string name="switch_pages">Ganti halaman</string>
|
||||
<string name="chapters">Bab</string>
|
||||
<string name="chapters">Bagian jilid</string>
|
||||
<string name="list">Daftar</string>
|
||||
<string name="detailed_list">Daftar rinci</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
@@ -74,13 +74,13 @@
|
||||
<string name="clear_thumbs_cache">Bersihkan singgahan gambar mini</string>
|
||||
<string name="clear_search_history">Bersihkan riwayat pencarian</string>
|
||||
<string name="search_history_cleared">Dibersihkan</string>
|
||||
<string name="domain">Domain</string>
|
||||
<string name="domain">Ranah web</string>
|
||||
<string name="app_update_available">Versi baru aplikasi tersedia</string>
|
||||
<string name="open_in_browser">Buka di peramban web</string>
|
||||
<string name="notifications">Notifikasi</string>
|
||||
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d dari %2$d diaktifkan</string>
|
||||
<string name="new_chapters">Bab baru</string>
|
||||
<string name="download">Unduh</string>
|
||||
<string name="new_chapters">Sub-bab baru</string>
|
||||
<string name="download">Ambil data (Unduh)</string>
|
||||
<string name="notifications_settings">Pengaturan notifikasi</string>
|
||||
<string name="notification_sound">Suara notifikasi</string>
|
||||
<string name="text_empty_holder_primary">Sepi juga di sini…</string>
|
||||
@@ -277,7 +277,7 @@
|
||||
<string name="storage_usage">Penggunaan penyimpanan</string>
|
||||
<string name="available">Tersedia</string>
|
||||
<string name="incognito_mode">Mode penyamaran</string>
|
||||
<string name="automatic_scroll">Gulir otomatis</string>
|
||||
<string name="automatic_scroll">Pengguliran 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>
|
||||
@@ -307,18 +307,18 @@
|
||||
<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="history_shortcuts_summary">Buat manga terbaru tersedia dengan menekan lama ikon aplikasi</string>
|
||||
<string name="select_range">Pilih rentang</string>
|
||||
<string name="disable_all">Matikan semua</string>
|
||||
<string name="dns_over_https">DNS over HTTPS</string>
|
||||
<string name="dns_over_https">DNS melalui HTTPS</string>
|
||||
<string name="status_dropped">Didrop</string>
|
||||
<string name="theme_name_mamimi">Mamimi</string>
|
||||
<string name="server_error">Galat sisi server (%1$d). Silakan coba lagi nanti</string>
|
||||
<string name="server_error">Kesalahan di sisi server (%1$d). Silakan coba lagi nanti</string>
|
||||
<string name="compact">Padat</string>
|
||||
<string name="prefetch_content">Pramuat konten</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
<string name="nothing_here">Tidak ada apapun di sini</string>
|
||||
<string name="reader_control_ltr_summary">Jangan sesuaikan arah pindah halaman ke mode pembaca, mis. menekan tombol kanan selalu pindah ke halaman selanjutnya. Opsi ini hanya berdampak pada perangkat dengan tombol masukan perangkat keras</string>
|
||||
<string name="reader_control_ltr_summary">Jangan ubah arah pergantian halaman ke mode pembaca, misalnya, menekan tombol kanan selalu beralih ke halaman berikutnya. Opsi ini hanya berlaku untuk perangkat masukan hardware</string>
|
||||
<string name="source_disabled">Sumber dinonaktifkan</string>
|
||||
<string name="mark_as_current">Tandai sebagai saat ini</string>
|
||||
<string name="show_suspicious_content">Tampilkan konten yang mencurigakan</string>
|
||||
@@ -355,7 +355,7 @@
|
||||
<string name="reader_control_ltr">Kontrol pembaca ergonomis</string>
|
||||
<string name="color_correction">Koreksi warna</string>
|
||||
<string name="reader_slider">Perlihatkan penggeser peralihan halaman</string>
|
||||
<string name="webtoon_zoom">Zum Webtoon</string>
|
||||
<string name="webtoon_zoom">Pembesaran Webtoon</string>
|
||||
<string name="network_unavailable">Jaringan tidak tersedia</string>
|
||||
<string name="got_it">Oke</string>
|
||||
<string name="sources_reorder_tip">Ketuk dan tahan item untuk menyusun ulang</string>
|
||||
@@ -509,7 +509,7 @@
|
||||
<string name="state_upcoming">Mendatang</string>
|
||||
<string name="color_correction_apply_text">Pengaturan ini dapat diterapkan secara menyeluruh atau hanya pada manga saat ini. Jika diterapkan secara menyeluruh, pengaturan pada manga tidak akan ditimpa.</string>
|
||||
<string name="source_enabled">Sumber yang diaktifkan</string>
|
||||
<string name="disable_nsfw_summary">Nonaktifkan sumber TAUSB and sembunyikan manga dewasa dari daftar jika memungkinkan</string>
|
||||
<string name="disable_nsfw_summary">Nonaktifkan sumber TAUSB dan sembunyikan manga dewasa dari daftar jika memungkinkan</string>
|
||||
<string name="content_rating">Peringkat konten</string>
|
||||
<string name="backup_date_">Tanggal dicadangkan %s</string>
|
||||
<string name="available_d">Tersedia:%1$d</string>
|
||||
@@ -729,13 +729,13 @@
|
||||
<string name="filter_search_warning">Sumber ini tidak mendukung pencarian dengan filter. Filter Anda telah dikosongkan</string>
|
||||
<string name="demographic_shoujo">Shoujo</string>
|
||||
<string name="manga_replaced">Manga \"%1$s\" (%2$s) diganti dengan \"%3$s\" (%4$s)</string>
|
||||
<string name="content_type_manhua">Manhua</string>
|
||||
<string name="content_type_manhua">Komik Mandarin (manhua)</string>
|
||||
<string name="user_manual">Manual pengguna</string>
|
||||
<string name="destination_directory">Direktori tujuan</string>
|
||||
<string name="demographic_josei">Josei</string>
|
||||
<string name="download_new_chapters">Unduh bab baru</string>
|
||||
<string name="content_type_novel">Novel</string>
|
||||
<string name="content_type_manhwa">Manhwa</string>
|
||||
<string name="content_type_manhwa">Komik Korea (Manhwa)</string>
|
||||
<string name="source_code">Kode sumber</string>
|
||||
<string name="download_cellular_confirm">Izinkan mengunduh melalui jaringan seluler?</string>
|
||||
<string name="demographics">Demografis</string>
|
||||
@@ -757,7 +757,7 @@
|
||||
<string name="chapter_selection_hint">Anda bisa memilih bab untuk diunduh dengan menekan lama item di dalam daftar bab.</string>
|
||||
<string name="rating">Nilai</string>
|
||||
<string name="source">Sumber</string>
|
||||
<string name="added_long_ago">Bahasa</string>
|
||||
<string name="added_long_ago">Ditambahkan sejak lama</string>
|
||||
<string name="popular_in_hour">Populer Dalam Satu Jam Terakhir</string>
|
||||
<string name="stuck">Bahasa</string>
|
||||
<string name="error_connection_reset">Koneksi diatur ulang oleh host jarak jauh</string>
|
||||
@@ -807,10 +807,10 @@
|
||||
<string name="tags_warnings_summary">Tandai genre yang mungkin tidak pantas untuk sebagian besar pengguna</string>
|
||||
<string name="error_non_file_uri">Jalur yang dipilih tidak dapat digunakan karena tidak menunjukkan berkas atau direktori</string>
|
||||
<string name="use_default_cover">gunakan penutup default</string>
|
||||
<string name="pick_manga_page">Indonesian</string>
|
||||
<string name="pick_custom_file">Indonesian</string>
|
||||
<string name="change_cover">Indonesian</string>
|
||||
<string name="theme_name_expressive">Indonesian</string>
|
||||
<string name="pick_manga_page">Pilih halaman manga</string>
|
||||
<string name="pick_custom_file">Pilih file khusus</string>
|
||||
<string name="change_cover">Ganti penutup</string>
|
||||
<string name="theme_name_expressive">Ekspresif (Test)</string>
|
||||
<string name="page_switch_timer">Halaman akan berganti setiap ~%d detik</string>
|
||||
<string name="expand">Memperluas</string>
|
||||
<string name="adblock">Blokir iklan di browser</string>
|
||||
@@ -846,4 +846,23 @@
|
||||
<string name="discord_token_description">Masukkan Token Discord Anda atau klik %s untuk mendapatkannya melalui browser</string>
|
||||
<string name="discord_token_hint">Tempelkan Token Discord Anda di sini</string>
|
||||
<string name="discord_rpc_summary">Tampilkan status membaca Anda di Discord</string>
|
||||
<string name="discord_rpc_description">Membaca manga di Kotatsu - aplikasi pembaca manga</string>
|
||||
<string name="manga_restricted_description">Manga ini tidak tersedia untuk dibaca di sumber ini. Coba cari di sumber lain atau buka di browser untuk informasi lebih lanjut</string>
|
||||
<string name="chapters_load_failed">Gagal memuat daftar bab</string>
|
||||
<string name="rpc_skip_nsfw_summary">Jangan gunakan RPC untuk konten dewasa</string>
|
||||
<string name="show_floating_control_button">Tampilkan tombol kontrol mengambang</string>
|
||||
<string name="unavailable">Tidak tersedia</string>
|
||||
<string name="invalid_token">Token tidak valid: %s</string>
|
||||
<string name="no_chapters_in_manga">Manga ini tidak mengandung bab apa pun</string>
|
||||
<string name="telegram_integration">Integrasi Telegram</string>
|
||||
<string name="test_parser">Uji sumber manga</string>
|
||||
<string name="reading_s">Membaca %s</string>
|
||||
<string name="read_on_s">Baca terus %s</string>
|
||||
<string name="obtain">Memperoleh</string>
|
||||
<string name="pull_to_prev_chapter">Lepas untuk membuka bab sebelumnya</string>
|
||||
<string name="pull_to_next_chapter">Lepas untuk membuka bab selanjutnya</string>
|
||||
<string name="pull_top_no_prev">Tidak ada bab sebelumnya</string>
|
||||
<string name="pull_bottom_no_next">Tidak ada bab berikutnya</string>
|
||||
<string name="enable_pull_gesture_title">Aktifkan gerakan tarik</string>
|
||||
<string name="enable_pull_gesture_summary">Gunakan gerakan tarik untuk pindah bab di webtoon</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user