Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77bb5c2fcd | ||
|
|
475a4904a9 | ||
|
|
cf43b8ebda | ||
|
|
f34096af98 | ||
|
|
d60ff2a052 | ||
|
|
59d4953554 | ||
|
|
f76052b1d6 | ||
|
|
26e59b0953 | ||
|
|
9ee1164f08 | ||
|
|
cfc3823593 | ||
|
|
8407a414c5 | ||
|
|
a379604974 | ||
|
|
c01d80f7da | ||
|
|
7533dce0d2 | ||
|
|
9f1e97fd54 | ||
|
|
382a73310c | ||
|
|
5eeab7fd08 | ||
|
|
bc54e7cfba | ||
|
|
4502ffb6d2 | ||
|
|
b6f9ce824e | ||
|
|
d33081c1c7 | ||
|
|
76c08535d6 | ||
|
|
b55fef67e1 | ||
|
|
56798677d5 | ||
|
|
ff30b9c225 | ||
|
|
5c3293ec44 | ||
|
|
1b17605e0e | ||
|
|
ba4e4dcf56 | ||
|
|
b35d5d4779 | ||
|
|
124f31ebe1 | ||
|
|
173087ee19 | ||
|
|
8d7bad97de | ||
|
|
188fbfbb95 | ||
|
|
3498a54bdf | ||
|
|
18169c2355 | ||
|
|
87beb9442f | ||
|
|
e642d54929 | ||
|
|
59ce5d5e67 | ||
|
|
58d5237692 |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 645
|
||||
versionName = '7.1.2'
|
||||
versionCode = 650
|
||||
versionName = '7.2.1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:26be293f24') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:7ed8c9f787') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
@@ -90,15 +90,15 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.7.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.0'
|
||||
implementation 'androidx.transition:transition-ktx:1.5.0'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
@@ -106,7 +106,7 @@ dependencies {
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.2'
|
||||
implementation 'androidx.webkit:webkit:1.11.0'
|
||||
|
||||
implementation 'androidx.work:work-runtime:2.9.0'
|
||||
@@ -136,7 +136,7 @@ dependencies {
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.6.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.6.0'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:02e6d6cfe9'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:8cafac256e'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
|
||||
|
||||
@@ -12,135 +12,184 @@ import org.koitharu.kotatsu.history.data.toMangaHistory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
class MigrateUseCase @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val database: MangaDatabase,
|
||||
private val progressUpdateUseCase: ProgressUpdateUseCase,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(oldManga: Manga, newManga: Manga) {
|
||||
val oldDetails = if (oldManga.chapters.isNullOrEmpty()) {
|
||||
runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
|
||||
}.getOrDefault(oldManga)
|
||||
} else {
|
||||
oldManga
|
||||
}
|
||||
val newDetails = if (newManga.chapters.isNullOrEmpty()) {
|
||||
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
|
||||
} else {
|
||||
newManga
|
||||
}
|
||||
mangaDataRepository.storeManga(newDetails)
|
||||
database.withTransaction {
|
||||
// replace favorites
|
||||
val favoritesDao = database.getFavouritesDao()
|
||||
val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
|
||||
if (oldFavourites.isNotEmpty()) {
|
||||
favoritesDao.delete(oldManga.id)
|
||||
for (f in oldFavourites) {
|
||||
val e = f.copy(
|
||||
mangaId = newManga.id,
|
||||
class MigrateUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val database: MangaDatabase,
|
||||
private val progressUpdateUseCase: ProgressUpdateUseCase,
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
oldManga: Manga,
|
||||
newManga: Manga,
|
||||
) {
|
||||
val oldDetails =
|
||||
if (oldManga.chapters.isNullOrEmpty()) {
|
||||
runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
|
||||
}.getOrDefault(oldManga)
|
||||
} else {
|
||||
oldManga
|
||||
}
|
||||
val newDetails =
|
||||
if (newManga.chapters.isNullOrEmpty()) {
|
||||
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
|
||||
} else {
|
||||
newManga
|
||||
}
|
||||
mangaDataRepository.storeManga(newDetails)
|
||||
database.withTransaction {
|
||||
// replace favorites
|
||||
val favoritesDao = database.getFavouritesDao()
|
||||
val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
|
||||
if (oldFavourites.isNotEmpty()) {
|
||||
favoritesDao.delete(oldManga.id)
|
||||
for (f in oldFavourites) {
|
||||
val e =
|
||||
f.copy(
|
||||
mangaId = newManga.id,
|
||||
)
|
||||
favoritesDao.upsert(e)
|
||||
}
|
||||
}
|
||||
// replace history
|
||||
val historyDao = database.getHistoryDao()
|
||||
val oldHistory = historyDao.find(oldDetails.id)
|
||||
val newHistory =
|
||||
if (oldHistory != null) {
|
||||
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
|
||||
historyDao.delete(oldDetails.id)
|
||||
historyDao.upsert(newHistory)
|
||||
newHistory
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// track
|
||||
val tracksDao = database.getTracksDao()
|
||||
val oldTrack = tracksDao.find(oldDetails.id)
|
||||
if (oldTrack != null) {
|
||||
val lastChapter = newDetails.chapters?.lastOrNull()
|
||||
val newTrack =
|
||||
TrackEntity(
|
||||
mangaId = newDetails.id,
|
||||
lastChapterId = lastChapter?.id ?: 0L,
|
||||
newChapters = 0,
|
||||
lastCheckTime = System.currentTimeMillis(),
|
||||
lastChapterDate = lastChapter?.uploadDate ?: 0L,
|
||||
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
||||
lastError = null,
|
||||
)
|
||||
tracksDao.delete(oldDetails.id)
|
||||
tracksDao.upsert(newTrack)
|
||||
}
|
||||
// scrobbling
|
||||
for (scrobbler in scrobblers) {
|
||||
if (!scrobbler.isEnabled) {
|
||||
continue
|
||||
}
|
||||
val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue
|
||||
scrobbler.unregisterScrobbling(oldDetails.id)
|
||||
scrobbler.linkManga(newDetails.id, prevInfo.targetId)
|
||||
scrobbler.updateScrobblingInfo(
|
||||
mangaId = newDetails.id,
|
||||
rating = prevInfo.rating,
|
||||
status =
|
||||
prevInfo.status ?: when {
|
||||
newHistory == null -> ScrobblingStatus.PLANNED
|
||||
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
|
||||
else -> ScrobblingStatus.READING
|
||||
},
|
||||
comment = prevInfo.comment,
|
||||
)
|
||||
favoritesDao.upsert(e)
|
||||
if (newHistory != null) {
|
||||
scrobbler.scrobble(
|
||||
manga = newDetails,
|
||||
chapterId = newHistory.chapterId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// replace history
|
||||
val historyDao = database.getHistoryDao()
|
||||
val oldHistory = historyDao.find(oldDetails.id)
|
||||
if (oldHistory != null) {
|
||||
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
|
||||
historyDao.delete(oldDetails.id)
|
||||
historyDao.upsert(newHistory)
|
||||
}
|
||||
// track
|
||||
val tracksDao = database.getTracksDao()
|
||||
val oldTrack = tracksDao.find(oldDetails.id)
|
||||
if (oldTrack != null) {
|
||||
val lastChapter = newDetails.chapters?.lastOrNull()
|
||||
val newTrack = TrackEntity(
|
||||
mangaId = newDetails.id,
|
||||
lastChapterId = lastChapter?.id ?: 0L,
|
||||
newChapters = 0,
|
||||
lastCheckTime = System.currentTimeMillis(),
|
||||
lastChapterDate = lastChapter?.uploadDate ?: 0L,
|
||||
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
||||
)
|
||||
tracksDao.delete(oldDetails.id)
|
||||
tracksDao.upsert(newTrack)
|
||||
}
|
||||
progressUpdateUseCase(newManga)
|
||||
}
|
||||
progressUpdateUseCase(newManga)
|
||||
}
|
||||
|
||||
private fun makeNewHistory(
|
||||
oldManga: Manga,
|
||||
newManga: Manga,
|
||||
history: HistoryEntity,
|
||||
): HistoryEntity {
|
||||
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
|
||||
val branch = newManga.getPreferredBranch(null)
|
||||
val chapters = checkNotNull(newManga.getChapters(branch))
|
||||
val currentChapter = if (history.percent in 0f..1f) {
|
||||
chapters[(chapters.lastIndex * history.percent).toInt()]
|
||||
} else {
|
||||
chapters.first()
|
||||
private fun makeNewHistory(
|
||||
oldManga: Manga,
|
||||
newManga: Manga,
|
||||
history: HistoryEntity,
|
||||
): HistoryEntity {
|
||||
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
|
||||
val branch = newManga.getPreferredBranch(null)
|
||||
val chapters = checkNotNull(newManga.getChapters(branch))
|
||||
val currentChapter =
|
||||
if (history.percent in 0f..1f) {
|
||||
chapters[(chapters.lastIndex * history.percent).toInt()]
|
||||
} else {
|
||||
chapters.first()
|
||||
}
|
||||
return HistoryEntity(
|
||||
mangaId = newManga.id,
|
||||
createdAt = history.createdAt,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
chapterId = currentChapter.id,
|
||||
page = history.page,
|
||||
scroll = history.scroll,
|
||||
percent = history.percent,
|
||||
deletedAt = 0,
|
||||
chaptersCount = chapters.size,
|
||||
)
|
||||
}
|
||||
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
|
||||
val oldChapters = checkNotNull(oldManga.getChapters(branch))
|
||||
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
|
||||
if (index < 0) {
|
||||
index =
|
||||
if (history.percent in 0f..1f) {
|
||||
(oldChapters.lastIndex * history.percent).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
|
||||
val newBranch =
|
||||
if (newChapters.containsKey(branch)) {
|
||||
branch
|
||||
} else {
|
||||
newManga.getPreferredBranch(null)
|
||||
}
|
||||
val newChapterId =
|
||||
checkNotNull(newChapters[newBranch])
|
||||
.let {
|
||||
val oldChapter = oldChapters[index]
|
||||
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
|
||||
}.id
|
||||
|
||||
return HistoryEntity(
|
||||
mangaId = newManga.id,
|
||||
createdAt = history.createdAt,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
chapterId = currentChapter.id,
|
||||
chapterId = newChapterId,
|
||||
page = history.page,
|
||||
scroll = history.scroll,
|
||||
percent = history.percent,
|
||||
percent = PROGRESS_NONE,
|
||||
deletedAt = 0,
|
||||
chaptersCount = chapters.size,
|
||||
chaptersCount = checkNotNull(newChapters[newBranch]).size,
|
||||
)
|
||||
}
|
||||
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
|
||||
val oldChapters = checkNotNull(oldManga.getChapters(branch))
|
||||
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
|
||||
if (index < 0) {
|
||||
index = if (history.percent in 0f..1f) {
|
||||
(oldChapters.lastIndex * history.percent).toInt()
|
||||
|
||||
private fun List<MangaChapter>.findByNumber(
|
||||
volume: Int,
|
||||
number: Float,
|
||||
): MangaChapter? =
|
||||
if (number <= 0f) {
|
||||
null
|
||||
} else {
|
||||
0
|
||||
firstOrNull { it.volume == volume && it.number == number }
|
||||
}
|
||||
}
|
||||
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
|
||||
val newBranch = if (newChapters.containsKey(branch)) {
|
||||
branch
|
||||
} else {
|
||||
newManga.getPreferredBranch(null)
|
||||
}
|
||||
val newChapterId = checkNotNull(newChapters[newBranch]).let {
|
||||
val oldChapter = oldChapters[index]
|
||||
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
|
||||
}.id
|
||||
|
||||
return HistoryEntity(
|
||||
mangaId = newManga.id,
|
||||
createdAt = history.createdAt,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
chapterId = newChapterId,
|
||||
page = history.page,
|
||||
scroll = history.scroll,
|
||||
percent = PROGRESS_NONE,
|
||||
deletedAt = 0,
|
||||
chaptersCount = checkNotNull(newChapters[newBranch]).size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<MangaChapter>.findByNumber(volume: Int, number: Float): MangaChapter? {
|
||||
return if (number <= 0f) {
|
||||
null
|
||||
} else {
|
||||
firstOrNull { it.volume == volume && it.number == number }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||
@@ -152,10 +153,12 @@ interface AppModule {
|
||||
appProtectHelper: AppProtectHelper,
|
||||
activityRecreationHandle: ActivityRecreationHandle,
|
||||
acraScreenLogger: AcraScreenLogger,
|
||||
screenshotPolicyHelper: ScreenshotPolicyHelper,
|
||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||
appProtectHelper,
|
||||
activityRecreationHandle,
|
||||
acraScreenLogger,
|
||||
screenshotPolicyHelper,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -84,6 +84,7 @@ class JsonDeserializer(private val json: JSONObject) {
|
||||
source = json.getString("source"),
|
||||
isEnabled = json.getBoolean("enabled"),
|
||||
sortKey = json.getInt("sort_key"),
|
||||
addedIn = json.getIntOrDefault("added_in", 0),
|
||||
)
|
||||
|
||||
fun toMap(): Map<String, Any?> {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration17To18
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration18To19
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
||||
@@ -58,7 +59,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
|
||||
const val DATABASE_VERSION = 20
|
||||
const val DATABASE_VERSION = 21
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
@@ -118,6 +119,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||
Migration17To18(),
|
||||
Migration18To19(),
|
||||
Migration19To20(),
|
||||
Migration20To21(),
|
||||
)
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
|
||||
@@ -23,6 +24,9 @@ abstract class MangaSourcesDao {
|
||||
@Query("SELECT source FROM sources WHERE enabled = 1")
|
||||
abstract suspend fun findAllEnabledNames(): List<String>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE added_in >= :version")
|
||||
abstract suspend fun findAllFromVersion(version: Int): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||
|
||||
@@ -68,6 +72,7 @@ abstract class MangaSourcesDao {
|
||||
source = source,
|
||||
isEnabled = isEnabled,
|
||||
sortKey = getMaxSortKey() + 1,
|
||||
addedIn = BuildConfig.VERSION_CODE,
|
||||
)
|
||||
upsert(entity)
|
||||
}
|
||||
|
||||
@@ -14,4 +14,5 @@ data class MangaSourceEntity(
|
||||
val source: String,
|
||||
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
||||
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
||||
@ColumnInfo(name = "added_in") val addedIn: Int,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration20To21 : Migration(20, 21) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE tracks ADD COLUMN `last_error` TEXT DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE sources ADD COLUMN `added_in` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,13 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import java.util.Locale
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
fun MangaSource(name: String): MangaSource {
|
||||
MangaSource.entries.forEach {
|
||||
if (it.name == name) return it
|
||||
}
|
||||
return MangaSource.DUMMY
|
||||
return MangaSource.UNKNOWN
|
||||
}
|
||||
|
||||
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
||||
@@ -39,7 +37,7 @@ val ContentType.titleResId
|
||||
|
||||
fun MangaSource.getSummary(context: Context): String {
|
||||
val type = context.getString(contentType.titleResId)
|
||||
val locale = locale?.toLocale().getDisplayName(context)
|
||||
val locale = locale.toLocale().getDisplayName(context)
|
||||
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ data class ParcelableChapter(
|
||||
scanlator = parcel.readString(),
|
||||
uploadDate = parcel.readLong(),
|
||||
branch = parcel.readString(),
|
||||
source = parcel.readSerializableCompat() ?: MangaSource.DUMMY,
|
||||
source = parcel.readSerializableCompat() ?: MangaSource.UNKNOWN,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class DoHManager(
|
||||
).build()
|
||||
|
||||
DoHProvider.ZERO_MS -> DnsOverHttps.Builder().client(bootstrapClient)
|
||||
.url("https://0ms.dev/dns-query".toHttpUrl())
|
||||
.url("https://2ca4h4crra.cloudflare-gateway.com/dns-query".toHttpUrl())
|
||||
.resolvePublicAddresses(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
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.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
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 EmptyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("localhost")
|
||||
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub(null)
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = stub(null)
|
||||
|
||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> = stub(seed)
|
||||
|
||||
private fun stub(manga: Manga?): Nothing {
|
||||
throw UnsupportedSourceException("This manga source is not supported", manga)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class MangaLinkResolver @Inject constructor(
|
||||
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
|
||||
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
|
||||
val source = MangaSource(sourceName)
|
||||
require(source != MangaSource.DUMMY) { "Manga source $sourceName is not supported" }
|
||||
require(source != MangaSource.UNKNOWN) { "Manga source $sourceName is not supported" }
|
||||
val repo = repositoryFactory.create(source)
|
||||
return repo.findExact(
|
||||
url = uri.getQueryParameter("url"),
|
||||
|
||||
@@ -5,9 +5,9 @@ import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
|
||||
return if (source == MangaSource.DUMMY) {
|
||||
DummyParser(loaderContext)
|
||||
} else {
|
||||
loaderContext.newParserInstance(source)
|
||||
return when (source) {
|
||||
MangaSource.UNKNOWN -> EmptyParser(loaderContext)
|
||||
MangaSource.DUMMY -> DummyParser(loaderContext)
|
||||
else -> loaderContext.newParserInstance(source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,17 +290,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||
|
||||
val isNewSourcesTipEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
|
||||
var sourcesVersion: Int
|
||||
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
|
||||
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
|
||||
|
||||
val isPagesNumbersEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||
|
||||
val screenshotsPolicy: ScreenshotsPolicy
|
||||
get() = runCatching {
|
||||
val key = prefs.getString(KEY_SCREENSHOTS_POLICY, null)?.uppercase(Locale.ROOT)
|
||||
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
|
||||
}.getOrDefault(ScreenshotsPolicy.ALLOW)
|
||||
get() = prefs.getEnumValue(KEY_SCREENSHOTS_POLICY, ScreenshotsPolicy.ALLOW)
|
||||
|
||||
var userSpecifiedMangaDirectories: Set<File>
|
||||
get() {
|
||||
@@ -653,7 +651,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_APP_LOCALE = "app_locale"
|
||||
const val KEY_LOGGING_ENABLED = "logging"
|
||||
const val KEY_SOURCES_GRID = "sources_grid"
|
||||
const val KEY_SOURCES_NEW = "sources_new"
|
||||
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
||||
const val KEY_TIPS_CLOSED = "tips_closed"
|
||||
const val KEY_SSL_BYPASS = "ssl_bypass"
|
||||
@@ -689,6 +686,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_STATS_ENABLED = "stats_on"
|
||||
const val KEY_FEED_HEADER = "feed_header"
|
||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||
const val KEY_SOURCES_VERSION = "sources_version"
|
||||
|
||||
// keys for non-persistent preferences
|
||||
const val KEY_APP_VERSION = "app_version"
|
||||
|
||||
@@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.prefs
|
||||
enum class ScreenshotsPolicy {
|
||||
|
||||
// Do not rename this
|
||||
ALLOW, BLOCK_NSFW, BLOCK_ALL;
|
||||
}
|
||||
ALLOW, BLOCK_NSFW, BLOCK_INCOGNITO, BLOCK_ALL;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
@@ -26,10 +28,12 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
abstract class BaseActivity<B : ViewBinding> :
|
||||
AppCompatActivity(),
|
||||
ScreenshotPolicyHelper.ContentContainer,
|
||||
WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
private var isAmoledTheme = false
|
||||
@@ -151,6 +155,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
}
|
||||
}
|
||||
|
||||
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
|
||||
|
||||
private fun putDataToExtras(intent: Intent?) {
|
||||
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ abstract class BaseViewModel : ViewModel() {
|
||||
errorEvent.call(error)
|
||||
}
|
||||
|
||||
protected inline suspend fun <T> withLoading(block: () -> T): T = try {
|
||||
protected inline fun <T> withLoading(block: () -> T): T = try {
|
||||
loadingCounter.increment()
|
||||
block()
|
||||
} finally {
|
||||
|
||||
@@ -52,6 +52,16 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
||||
}
|
||||
|
||||
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.setPadding(left, top, right, bottom)
|
||||
fastScroller.setPadding(left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||
super.setPaddingRelative(start, top, end, bottom)
|
||||
fastScroller.setPaddingRelative(start, top, end, bottom)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
fastScroller.attachRecyclerView(this)
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.google.android.material.chip.ChipGroup
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
||||
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ChipsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -24,7 +26,9 @@ class ChipsView @JvmOverloads constructor(
|
||||
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
||||
}
|
||||
private val chipOnCloseListener = OnClickListener {
|
||||
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
|
||||
val chip = it as Chip
|
||||
val data = it.tag
|
||||
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
|
||||
}
|
||||
private val chipStyle: Int
|
||||
var onChipClickListener: OnChipClickListener? = null
|
||||
@@ -48,7 +52,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
if (isInEditMode) {
|
||||
setChips(
|
||||
List(5) {
|
||||
ChipModel(0, "Chip $it", 0, isCheckable = false, isChecked = false)
|
||||
ChipModel(title = "Chip $it")
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -99,6 +103,15 @@ class ChipsView @JvmOverloads constructor(
|
||||
chip.isChipIconVisible = true
|
||||
}
|
||||
chip.isChecked = model.isChecked
|
||||
chip.isCheckedIconVisible = chip.isCheckable && model.icon == 0
|
||||
chip.isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
|
||||
chip.setCloseIconResource(
|
||||
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
chip.tag = model.data
|
||||
}
|
||||
|
||||
@@ -106,12 +119,11 @@ class ChipsView @JvmOverloads constructor(
|
||||
val chip = Chip(context)
|
||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
||||
chip.setChipDrawable(drawable)
|
||||
chip.isCheckedIconVisible = true
|
||||
chip.isChipIconVisible = false
|
||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
chip.setOnClickListener(chipOnClickListener)
|
||||
chip.isElegantTextHeight = false
|
||||
addView(chip)
|
||||
return chip
|
||||
}
|
||||
@@ -127,11 +139,12 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
data class ChipModel(
|
||||
@ColorRes val tint: Int,
|
||||
val title: CharSequence,
|
||||
@DrawableRes val icon: Int,
|
||||
val isCheckable: Boolean,
|
||||
val isChecked: Boolean,
|
||||
@DrawableRes val icon: Int = 0,
|
||||
val isCheckable: Boolean = false,
|
||||
@ColorRes val tint: Int = 0,
|
||||
val isChecked: Boolean = false,
|
||||
val isDropdown: Boolean = false,
|
||||
val data: Any? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.map
|
||||
import org.koitharu.kotatsu.core.util.ext.iterator
|
||||
import java.util.Locale
|
||||
|
||||
class LocaleComparator : Comparator<Locale> {
|
||||
|
||||
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
|
||||
.map { it.language }
|
||||
.distinct()
|
||||
private val deviceLocales: List<String>
|
||||
|
||||
init {
|
||||
val localeList = LocaleListCompat.getAdjustedDefault()
|
||||
deviceLocales = buildList(localeList.size() + 1) {
|
||||
add("")
|
||||
val set = HashSet<String>(localeList.size() + 1)
|
||||
set.add("")
|
||||
for (locale in localeList) {
|
||||
val lang = locale.language
|
||||
if (set.add(lang)) {
|
||||
add(lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun compare(a: Locale, b: Locale): Int {
|
||||
val indexA = deviceLocales.indexOf(a.language)
|
||||
|
||||
@@ -22,11 +22,10 @@ fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementE
|
||||
|
||||
fun String.toLocale() = Locale(this)
|
||||
|
||||
fun Locale?.getDisplayName(context: Context): String {
|
||||
if (this == null) {
|
||||
return context.getString(R.string.various_languages)
|
||||
}
|
||||
return getDisplayLanguage(this).toTitleCase(this)
|
||||
fun Locale?.getDisplayName(context: Context): String = when (this) {
|
||||
null -> context.getString(R.string.all_languages)
|
||||
Locale.ROOT -> context.getString(R.string.various_languages)
|
||||
else -> getDisplayLanguage(this).toTitleCase(this)
|
||||
}
|
||||
|
||||
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
|
||||
|
||||
@@ -34,10 +34,12 @@ import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
@@ -197,6 +199,8 @@ class DetailsActivity :
|
||||
addMenuProvider(menuProvider)
|
||||
}
|
||||
|
||||
override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.isNsfw == true }
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_read -> openReader(isIncognitoMode = false)
|
||||
@@ -459,7 +463,7 @@ class DetailsActivity :
|
||||
imageViewState.isVisible = false
|
||||
}
|
||||
|
||||
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
|
||||
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.UNKNOWN) {
|
||||
infoLayout.chipSource.isVisible = false
|
||||
} else {
|
||||
infoLayout.chipSource.text = manga.source.title
|
||||
@@ -534,7 +538,7 @@ class DetailsActivity :
|
||||
}
|
||||
val isFirstCall = buttonRead.tag == null
|
||||
buttonRead.tag = Unit
|
||||
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, !isFirstCall)
|
||||
buttonRead.setProgress(info.percent.coerceIn(0f, 1f), !isFirstCall)
|
||||
buttonDownload?.isEnabled = info.isValid && info.canDownload
|
||||
buttonRead.isEnabled = info.isValid
|
||||
}
|
||||
@@ -612,10 +616,7 @@ class DetailsActivity :
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = tagHighlighter.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -93,15 +93,19 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
val details = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
|
||||
val manga = details.map { x -> x?.toManga() }
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val history = historyRepository.observeOne(mangaId)
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val favouriteCategories = interactor.observeFavourite(mangaId)
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
val isStatsAvailable = statsRepository.observeHasStats(mangaId)
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
val remoteManga = MutableStateFlow<Manga?>(null)
|
||||
@@ -162,7 +166,7 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||
val isScrobblingAvailable: Boolean
|
||||
get() = scrobblers.any { it.isAvailable }
|
||||
get() = scrobblers.any { it.isEnabled }
|
||||
|
||||
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
@@ -393,7 +397,7 @@ class DetailsViewModel @Inject constructor(
|
||||
private fun getScrobbler(index: Int): Scrobbler? {
|
||||
val info = scrobblingInfo.value.getOrNull(index)
|
||||
val scrobbler = if (info != null) {
|
||||
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
|
||||
scrobblers.find { it.scrobblerService == info.scrobbler && it.isEnabled }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@ data class HistoryInfo(
|
||||
|
||||
val canContinue
|
||||
get() = currentChapter >= 0
|
||||
|
||||
val percent: Float
|
||||
get() = if (history != null && (canContinue || isChapterMissing)) {
|
||||
history.percent
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
fun HistoryInfo(
|
||||
|
||||
@@ -6,21 +6,22 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.Collections
|
||||
import java.util.EnumSet
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
@@ -29,11 +30,13 @@ class MangaSourcesRepository @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
private val isNewSourcesAssimilated = AtomicBoolean(false)
|
||||
private val dao: MangaSourcesDao
|
||||
get() = db.getSourcesDao()
|
||||
|
||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
||||
remove(MangaSource.LOCAL)
|
||||
remove(MangaSource.UNKNOWN)
|
||||
if (!BuildConfig.DEBUG) {
|
||||
remove(MangaSource.DUMMY)
|
||||
}
|
||||
@@ -43,25 +46,62 @@ class MangaSourcesRepository @Inject constructor(
|
||||
get() = Collections.unmodifiableSet(remoteSources)
|
||||
|
||||
suspend fun getEnabledSources(): List<MangaSource> {
|
||||
assimilateNewSources()
|
||||
val order = settings.sourcesSortOrder
|
||||
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
|
||||
}
|
||||
|
||||
suspend fun getDisabledSources(): Set<MangaSource> {
|
||||
assimilateNewSources()
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
val enabled = dao.findAllEnabledNames()
|
||||
for (name in enabled) {
|
||||
val source = MangaSource(name)
|
||||
val source = name.toMangaSourceOrNull() ?: continue
|
||||
result.remove(source)
|
||||
}
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
result.removeAll { it.isNsfw() }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getAvailableSources(
|
||||
isDisabledOnly: Boolean,
|
||||
isNewOnly: Boolean,
|
||||
excludeBroken: Boolean,
|
||||
types: Set<ContentType>,
|
||||
query: String?,
|
||||
locale: String?,
|
||||
sortOrder: SourcesSortOrder?,
|
||||
): List<MangaSource> {
|
||||
assimilateNewSources()
|
||||
val entities = dao.findAll().toMutableList()
|
||||
if (isDisabledOnly) {
|
||||
entities.removeAll { it.isEnabled }
|
||||
}
|
||||
if (isNewOnly) {
|
||||
entities.retainAll { it.addedIn == BuildConfig.VERSION_CODE }
|
||||
}
|
||||
val sources = entities.toSources(
|
||||
skipNsfwSources = settings.isNsfwContentDisabled,
|
||||
sortOrder = sortOrder,
|
||||
)
|
||||
if (locale != null) {
|
||||
sources.retainAll { it.locale == locale }
|
||||
}
|
||||
if (excludeBroken) {
|
||||
sources.removeAll { it.isBroken }
|
||||
}
|
||||
if (types.isNotEmpty()) {
|
||||
sources.retainAll { it.contentType in types }
|
||||
}
|
||||
if (!query.isNullOrEmpty()) {
|
||||
sources.retainAll {
|
||||
it.title.contains(query, ignoreCase = true) || it.name.contains(query, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
fun observeIsEnabled(source: MangaSource): Flow<Boolean> {
|
||||
return dao.observeIsEnabled(source.name)
|
||||
return dao.observeIsEnabled(source.name).onStart { assimilateNewSources() }
|
||||
}
|
||||
|
||||
fun observeEnabledSourcesCount(): Flow<Int> {
|
||||
@@ -69,8 +109,10 @@ class MangaSourcesRepository @Inject constructor(
|
||||
observeIsNsfwDisabled(),
|
||||
dao.observeEnabled(SourcesSortOrder.MANUAL),
|
||||
) { skipNsfw, sources ->
|
||||
sources.count { !skipNsfw || !MangaSource(it.source).isNsfw() }
|
||||
}.distinctUntilChanged()
|
||||
sources.count {
|
||||
it.source.toMangaSourceOrNull()?.let { s -> !skipNsfw || !s.isNsfw() } == true
|
||||
}
|
||||
}.distinctUntilChanged().onStart { assimilateNewSources() }
|
||||
}
|
||||
|
||||
fun observeAvailableSourcesCount(): Flow<Int> {
|
||||
@@ -82,7 +124,7 @@ class MangaSourcesRepository @Inject constructor(
|
||||
allMangaSources.count { x ->
|
||||
x.name !in enabled && (!skipNsfw || !x.isNsfw())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}.distinctUntilChanged().onStart { assimilateNewSources() }
|
||||
}
|
||||
|
||||
fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
|
||||
@@ -92,18 +134,18 @@ class MangaSourcesRepository @Inject constructor(
|
||||
dao.observeEnabled(order).map {
|
||||
it.toSources(skipNsfw, order)
|
||||
}
|
||||
}.flatMapLatest { it }
|
||||
}.flatMapLatest { it }.onStart { assimilateNewSources() }
|
||||
|
||||
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
|
||||
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
|
||||
for (entity in entities) {
|
||||
val source = MangaSource(entity.source)
|
||||
val source = entity.source.toMangaSourceOrNull() ?: continue
|
||||
if (source in remoteSources) {
|
||||
result.add(source to entity.isEnabled)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}.onStart { assimilateNewSources() }
|
||||
|
||||
suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle {
|
||||
setSourcesEnabledImpl(sources, isEnabled)
|
||||
@@ -114,6 +156,7 @@ class MangaSourcesRepository @Inject constructor(
|
||||
|
||||
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
|
||||
db.withTransaction {
|
||||
assimilateNewSources()
|
||||
for (s in remoteSources) {
|
||||
dao.setEnabled(s.name, s in sources)
|
||||
}
|
||||
@@ -135,31 +178,34 @@ class MangaSourcesRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun observeNewSources(): Flow<Set<MangaSource>> = observeIsNewSourcesEnabled().flatMapLatest {
|
||||
if (it) {
|
||||
combine(
|
||||
dao.observeAll(),
|
||||
observeIsNsfwDisabled(),
|
||||
) { entities, skipNsfw ->
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
for (e in entities) {
|
||||
result.remove(MangaSource(e.source))
|
||||
}
|
||||
if (skipNsfw) {
|
||||
result.removeAll { x -> x.isNsfw() }
|
||||
}
|
||||
result
|
||||
}.distinctUntilChanged()
|
||||
fun observeHasNewSources(): Flow<Boolean> = observeIsNsfwDisabled().map { skipNsfw ->
|
||||
val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null)
|
||||
sources.isNotEmpty() && sources.size != remoteSources.size
|
||||
}.onStart { assimilateNewSources() }
|
||||
|
||||
fun observeHasNewSourcesForBadge(): Flow<Boolean> = combine(
|
||||
settings.observeAsFlow(AppSettings.KEY_SOURCES_VERSION) { sourcesVersion },
|
||||
observeIsNsfwDisabled(),
|
||||
) { version, skipNsfw ->
|
||||
if (version < BuildConfig.VERSION_CODE) {
|
||||
val sources = dao.findAllFromVersion(version).toSources(skipNsfw, null)
|
||||
sources.isNotEmpty()
|
||||
} else {
|
||||
assimilateNewSources()
|
||||
flowOf(emptySet())
|
||||
false
|
||||
}
|
||||
}.onStart { assimilateNewSources() }
|
||||
|
||||
fun clearNewSourcesBadge() {
|
||||
settings.sourcesVersion = BuildConfig.VERSION_CODE
|
||||
}
|
||||
|
||||
suspend fun assimilateNewSources(): Set<MangaSource> {
|
||||
private suspend fun assimilateNewSources(): Boolean {
|
||||
if (isNewSourcesAssimilated.getAndSet(true)) {
|
||||
return false
|
||||
}
|
||||
val new = getNewSources()
|
||||
if (new.isEmpty()) {
|
||||
return emptySet()
|
||||
return false
|
||||
}
|
||||
var maxSortKey = dao.getMaxSortKey()
|
||||
val entities = new.map { x ->
|
||||
@@ -167,17 +213,15 @@ class MangaSourcesRepository @Inject constructor(
|
||||
source = x.name,
|
||||
isEnabled = false,
|
||||
sortKey = ++maxSortKey,
|
||||
addedIn = BuildConfig.VERSION_CODE,
|
||||
)
|
||||
}
|
||||
dao.insertIfAbsent(entities)
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
new.removeAll { x -> x.isNsfw() }
|
||||
}
|
||||
return new
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun isSetupRequired(): Boolean {
|
||||
return dao.findAll().isEmpty()
|
||||
return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty()
|
||||
}
|
||||
|
||||
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
|
||||
@@ -196,7 +240,7 @@ class MangaSourcesRepository @Inject constructor(
|
||||
val entities = dao.findAll()
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
for (e in entities) {
|
||||
result.remove(MangaSource(e.source))
|
||||
result.remove(e.source.toMangaSourceOrNull() ?: continue)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -204,10 +248,10 @@ class MangaSourcesRepository @Inject constructor(
|
||||
private fun List<MangaSourceEntity>.toSources(
|
||||
skipNsfwSources: Boolean,
|
||||
sortOrder: SourcesSortOrder?,
|
||||
): List<MangaSource> {
|
||||
): MutableList<MangaSource> {
|
||||
val result = ArrayList<MangaSource>(size)
|
||||
for (entity in this) {
|
||||
val source = MangaSource(entity.source)
|
||||
val source = entity.source.toMangaSourceOrNull() ?: continue
|
||||
if (skipNsfwSources && source.isNsfw()) {
|
||||
continue
|
||||
}
|
||||
@@ -225,11 +269,9 @@ class MangaSourcesRepository @Inject constructor(
|
||||
isNsfwContentDisabled
|
||||
}
|
||||
|
||||
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
|
||||
isNewSourcesTipEnabled
|
||||
}
|
||||
|
||||
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
|
||||
sourcesSortOrder
|
||||
}
|
||||
|
||||
private fun String.toMangaSourceOrNull(): MangaSource? = MangaSource.entries.find { it.name == this }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.widgets.TipView
|
||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
@@ -40,13 +39,11 @@ import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.TipModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||
import javax.inject.Inject
|
||||
@@ -56,7 +53,7 @@ class ExploreFragment :
|
||||
BaseFragment<FragmentExploreBinding>(),
|
||||
RecyclerViewOwner,
|
||||
ExploreListEventListener,
|
||||
OnListItemClickListener<MangaSourceItem>, TipView.OnButtonClickListener, ListSelectionController.Callback2 {
|
||||
OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback2 {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
@@ -74,7 +71,7 @@ class ExploreFragment :
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this, this) { manga, view ->
|
||||
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this) { manga, view ->
|
||||
startActivity(DetailsActivity.newIntent(view.context, manga))
|
||||
}
|
||||
sourceSelectionController = ListSelectionController(
|
||||
@@ -124,18 +121,6 @@ class ExploreFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrimaryButtonClick(tipView: TipView) {
|
||||
when ((tipView.tag as? TipModel)?.key) {
|
||||
ExploreViewModel.TIP_NEW_SOURCES -> NewSourcesDialogFragment.show(childFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSecondaryButtonClick(tipView: TipView) {
|
||||
when ((tipView.tag as? TipModel)?.key) {
|
||||
ExploreViewModel.TIP_NEW_SOURCES -> viewModel.discardNewSources()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val intent = when (v.id) {
|
||||
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
|
||||
|
||||
@@ -102,12 +102,6 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun discardNewSources() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
sourcesRepository.assimilateNewSources()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPinShortcut(source: MangaSource) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
shortcutManager.requestPinShortcut(source)
|
||||
@@ -124,7 +118,7 @@ class ExploreViewModel @Inject constructor(
|
||||
getSuggestionFlow(),
|
||||
isGrid,
|
||||
isRandomLoading,
|
||||
sourcesRepository.observeNewSources(),
|
||||
sourcesRepository.observeHasNewSourcesForBadge(),
|
||||
) { content, suggestions, grid, randomLoading, newSources ->
|
||||
buildList(content, suggestions, grid, randomLoading, newSources)
|
||||
}.withErrorHandling()
|
||||
@@ -134,7 +128,7 @@ class ExploreViewModel @Inject constructor(
|
||||
recommendation: List<Manga>,
|
||||
isGrid: Boolean,
|
||||
randomLoading: Boolean,
|
||||
newSources: Set<MangaSource>,
|
||||
hasNewSources: Boolean,
|
||||
): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(sources.size + 3)
|
||||
result += ExploreButtons(randomLoading)
|
||||
@@ -146,7 +140,7 @@ class ExploreViewModel @Inject constructor(
|
||||
result += ListHeader(
|
||||
textRes = R.string.remote_sources,
|
||||
buttonTextRes = R.string.catalog,
|
||||
badge = if (newSources.isNotEmpty()) "" else null,
|
||||
badge = if (hasNewSources) "" else null,
|
||||
)
|
||||
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
|
||||
} else {
|
||||
@@ -191,6 +185,5 @@ class ExploreViewModel @Inject constructor(
|
||||
|
||||
private const val TIP_SUGGESTIONS = "suggestions"
|
||||
private const val SUGGESTIONS_COUNT = 8
|
||||
const val TIP_NEW_SOURCES = "new_sources"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.widgets.TipView
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.tipAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
@@ -18,7 +16,6 @@ class ExploreAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
listener: ExploreListEventListener,
|
||||
tipClickListener: TipView.OnButtonClickListener,
|
||||
clickListener: OnListItemClickListener<MangaSourceItem>,
|
||||
mangaClickListener: OnListItemClickListener<Manga>,
|
||||
) : BaseListAdapter<ListModel>() {
|
||||
@@ -34,6 +31,5 @@ class ExploreAdapter(
|
||||
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
|
||||
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
addDelegate(ListItemType.TIP, tipAD(tipClickListener))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ class FilterCoordinator @Inject constructor(
|
||||
}
|
||||
oldValue.copy(
|
||||
tagsExclude = newTags,
|
||||
tags = oldValue.tags - newTags
|
||||
tags = oldValue.tags - newTags,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -308,7 +308,7 @@ class FilterCoordinator @Inject constructor(
|
||||
currentState.update { oldValue ->
|
||||
oldValue.copy(
|
||||
tags = tags,
|
||||
tagsExclude = oldValue.tagsExclude - tags
|
||||
tagsExclude = oldValue.tagsExclude - tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -391,9 +391,7 @@ class FilterCoordinator @Inject constructor(
|
||||
val result = LinkedList<ChipsView.ChipModel>()
|
||||
for (tag in tags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = selectedTags.remove(tag),
|
||||
data = tag,
|
||||
@@ -406,9 +404,7 @@ class FilterCoordinator @Inject constructor(
|
||||
}
|
||||
for (tag in selectedTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
|
||||
@@ -61,10 +61,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
|
||||
}
|
||||
|
||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(R.string.more),
|
||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
@@ -29,7 +30,6 @@ import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import java.util.Locale
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -122,10 +122,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
b.spinnerLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map {
|
||||
it?.getDisplayLanguage(it)?.toTitleCase(it)
|
||||
?: b.spinnerLocale.context.getString(R.string.various_languages)
|
||||
},
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
@@ -144,9 +141,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
|
||||
value.selectedItems.mapTo(chips) { tag ->
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
@@ -155,9 +150,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
value.availableItems.mapNotNullTo(chips) { tag ->
|
||||
if (tag !in value.selectedItems) {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = false,
|
||||
data = tag,
|
||||
@@ -168,12 +161,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
}
|
||||
chips.add(
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(R.string.more),
|
||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
data = null,
|
||||
),
|
||||
)
|
||||
b.chipsGenres.setChips(chips)
|
||||
@@ -200,9 +189,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
value.availableItems.mapNotNullTo(chips) { tag ->
|
||||
if (tag !in value.selectedItems) {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = false,
|
||||
data = tag,
|
||||
@@ -213,12 +200,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
}
|
||||
chips.add(
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(R.string.more),
|
||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
data = null,
|
||||
),
|
||||
)
|
||||
b.chipsGenresExclude.setChips(chips)
|
||||
@@ -233,9 +216,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
}
|
||||
val chips = value.availableItems.map { state ->
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(state.titleResId),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = state in value.selectedItems,
|
||||
data = state,
|
||||
@@ -253,9 +234,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
}
|
||||
val chips = value.availableItems.map { contentRating ->
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(contentRating.titleResId),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = contentRating in value.selectedItems,
|
||||
data = contentRating,
|
||||
|
||||
@@ -37,9 +37,6 @@ suspend fun Manga.toListDetailedModel(
|
||||
ChipsView.ChipModel(
|
||||
tint = extraProvider?.getTagTint(it) ?: 0,
|
||||
title = it.title,
|
||||
icon = 0,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
data = it,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -85,10 +85,7 @@ class PreviewViewModel @Inject constructor(
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = extraProvider.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
@@ -227,9 +227,11 @@ class LocalMangaRepository @Inject constructor(
|
||||
}.filterNotNullTo(ArrayList(files.size))
|
||||
}
|
||||
|
||||
private suspend fun getAllFiles() = storageManager.getReadableDirs().asSequence().flatMap { dir ->
|
||||
dir.children()
|
||||
}
|
||||
private suspend fun getAllFiles() = storageManager.getReadableDirs()
|
||||
.asSequence()
|
||||
.flatMap { dir ->
|
||||
dir.children().filterNot { it.isHidden }
|
||||
}
|
||||
|
||||
private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.koitharu.kotatsu.main.ui.protect
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScreenshotPolicyHelper @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : DefaultActivityLifecycleCallbacks {
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
(activity as? ContentContainer)?.setupScreenshotPolicy(activity)
|
||||
}
|
||||
|
||||
private fun ContentContainer.setupScreenshotPolicy(activity: Activity) =
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }
|
||||
.flatMapLatest { policy ->
|
||||
when (policy) {
|
||||
ScreenshotsPolicy.ALLOW -> flowOf(false)
|
||||
ScreenshotsPolicy.BLOCK_NSFW -> withContext(Dispatchers.Main) {
|
||||
isNsfwContent()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
ScreenshotsPolicy.BLOCK_ALL -> flowOf(true)
|
||||
ScreenshotsPolicy.BLOCK_INCOGNITO -> settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) {
|
||||
isIncognitoModeEnabled
|
||||
}
|
||||
}
|
||||
}.collect { isSecure ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isSecure) {
|
||||
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ContentContainer : LifecycleOwner {
|
||||
|
||||
@MainThread
|
||||
fun isNsfwContent(): Flow<Boolean>
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.databinding.SheetWelcomeBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
import java.util.Locale
|
||||
|
||||
@@ -58,7 +58,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked)
|
||||
is Locale? -> viewModel.setLocaleChecked(data, chip.isChecked)
|
||||
is Locale -> viewModel.setLocaleChecked(data, chip.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +86,12 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalesChanged(value: FilterProperty<Locale?>) {
|
||||
private fun onLocalesChanged(value: FilterProperty<Locale>) {
|
||||
val chips = viewBinding?.chipsLocales ?: return
|
||||
chips.setChips(
|
||||
value.availableItems.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
|
||||
icon = 0,
|
||||
title = it.getDisplayName(chips.context),
|
||||
isCheckable = true,
|
||||
isChecked = it in value.selectedItems,
|
||||
data = it,
|
||||
@@ -107,9 +105,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
|
||||
chips.setChips(
|
||||
value.availableItems.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(it.titleResId),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = it in value.selectedItems,
|
||||
data = it,
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
@@ -27,14 +28,14 @@ class WelcomeViewModel @Inject constructor(
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val allSources = repository.allMangaSources
|
||||
private val localesGroups by lazy { allSources.groupBy { it.locale?.let { x -> Locale(x) } } }
|
||||
private val localesGroups by lazy { allSources.groupBy { it.locale.toLocale() } }
|
||||
|
||||
private var updateJob: Job
|
||||
|
||||
val locales = MutableStateFlow(
|
||||
FilterProperty<Locale?>(
|
||||
availableItems = listOf(null),
|
||||
selectedItems = setOf(null),
|
||||
FilterProperty<Locale>(
|
||||
availableItems = listOf(Locale.ROOT),
|
||||
selectedItems = setOf(Locale.ROOT),
|
||||
isLoading = true,
|
||||
error = null,
|
||||
),
|
||||
@@ -51,22 +52,23 @@ class WelcomeViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
val languages = localesGroups.keys.associateBy { x -> x?.language }
|
||||
val selectedLocales = HashSet<Locale?>(2)
|
||||
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
|
||||
val languages = localesGroups.keys.associateBy { x -> x.language }
|
||||
val selectedLocales = HashSet<Locale>(2)
|
||||
ConfigurationCompat.getLocales(context.resources.configuration).toList()
|
||||
.firstNotNullOfOrNull { lc -> languages[lc.language] }
|
||||
selectedLocales += null
|
||||
?.let { selectedLocales += it }
|
||||
selectedLocales += Locale.ROOT
|
||||
locales.value = locales.value.copy(
|
||||
availableItems = localesGroups.keys.sortedWithSafe(nullsFirst(LocaleComparator())),
|
||||
availableItems = localesGroups.keys.sortedWithSafe(LocaleComparator()),
|
||||
selectedItems = selectedLocales,
|
||||
isLoading = false,
|
||||
)
|
||||
repository.assimilateNewSources()
|
||||
repository.clearNewSourcesBadge()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocaleChecked(locale: Locale?, isChecked: Boolean) {
|
||||
fun setLocaleChecked(locale: Locale, isChecked: Boolean) {
|
||||
val snapshot = locales.value
|
||||
locales.value = snapshot.copy(
|
||||
selectedItems = if (isChecked) {
|
||||
@@ -99,7 +101,7 @@ class WelcomeViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun commit() {
|
||||
val languages = locales.value.selectedItems.mapToSet { it?.language }
|
||||
val languages = locales.value.selectedItems.mapToSet { it.language }
|
||||
val types = types.value.selectedItems
|
||||
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
||||
x.contentType in types && x.locale in languages
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
@@ -142,7 +143,6 @@ class ReaderActivity :
|
||||
viewModel.content.observe(this) {
|
||||
onLoadingStateChanged(viewModel.isLoading.value)
|
||||
}
|
||||
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
|
||||
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
|
||||
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
|
||||
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
|
||||
@@ -179,6 +179,8 @@ class ReaderActivity :
|
||||
viewModel.onPause()
|
||||
}
|
||||
|
||||
override fun isNsfwContent(): Flow<Boolean> = viewModel.isMangaNsfw
|
||||
|
||||
override fun onIdle() {
|
||||
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
||||
}
|
||||
@@ -297,14 +299,6 @@ class ReaderActivity :
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setWindowSecure(isSecure: Boolean) {
|
||||
if (isSecure) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setKeepScreenOn(isKeep: Boolean) {
|
||||
if (isKeep) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
@@ -11,7 +11,9 @@ import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.RoundedCorner
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import androidx.annotation.AttrRes
|
||||
@@ -46,8 +48,10 @@ class ReaderInfoBarView @JvmOverloads constructor(
|
||||
private var insetLeft: Int = 0
|
||||
private var insetRight: Int = 0
|
||||
private var insetTop: Int = 0
|
||||
private var cutoutInsetLeft = 0
|
||||
private var cutoutInsetRight = 0
|
||||
private val insetLeftFallback: Int
|
||||
private val insetRightFallback: Int
|
||||
private val insetTopFallback: Int
|
||||
private val insetCornerFallback = getSystemUiDimensionOffset("rounded_corner_content_padding")
|
||||
private val colorText = ColorUtils.setAlphaComponent(
|
||||
context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK),
|
||||
200,
|
||||
@@ -80,14 +84,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
|
||||
paint.strokeWidth = getDimension(R.styleable.ReaderInfoBarView_android_strokeWidth, 2f)
|
||||
paint.textSize = getDimension(R.styleable.ReaderInfoBarView_android_textSize, 16f)
|
||||
}
|
||||
val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding")
|
||||
val fallbackInset = resources.getDimensionPixelOffset(R.dimen.reader_bar_inset_fallback)
|
||||
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start", fallbackInset) + insetCorner
|
||||
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end", fallbackInset) + insetCorner
|
||||
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start")
|
||||
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end")
|
||||
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
|
||||
insetLeft = if (isRtl) insetEnd else insetStart
|
||||
insetRight = if (isRtl) insetStart else insetEnd
|
||||
insetTop = minOf(insetLeft, insetRight)
|
||||
insetLeftFallback = if (isRtl) insetEnd else insetStart
|
||||
insetRightFallback = if (isRtl) insetStart else insetEnd
|
||||
insetTopFallback = minOf(insetLeftFallback, insetRightFallback)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
@@ -110,12 +112,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
|
||||
paint.textAlign = Paint.Align.LEFT
|
||||
canvas.drawTextOutline(
|
||||
text,
|
||||
(paddingLeft + insetLeft + cutoutInsetLeft).toFloat(),
|
||||
(paddingLeft + insetLeft).toFloat(),
|
||||
paddingTop + insetTop + ty,
|
||||
)
|
||||
if (isTimeVisible) {
|
||||
paint.textAlign = Paint.Align.RIGHT
|
||||
var endX = (width - paddingRight - insetRight - cutoutInsetRight).toFloat()
|
||||
var endX = (width - paddingRight - insetRight).toFloat()
|
||||
canvas.drawTextOutline(timeText, endX, paddingTop + insetTop + ty)
|
||||
if (batteryText.isNotEmpty()) {
|
||||
paint.getTextBounds(timeText, 0, timeText.length, textBounds)
|
||||
@@ -221,15 +223,29 @@ class ReaderInfoBarView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun updateCutoutInsets(insetsCompat: WindowInsetsCompat?) {
|
||||
val cutouts = (insetsCompat ?: return).displayCutout?.boundingRects.orEmpty()
|
||||
cutoutInsetLeft = 0
|
||||
cutoutInsetRight = 0
|
||||
for (rect in cutouts) {
|
||||
if (rect.left <= paddingLeft) {
|
||||
cutoutInsetLeft += rect.width()
|
||||
insetLeft = insetLeftFallback
|
||||
insetRight = insetRightFallback
|
||||
insetTop = insetTopFallback
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && insetsCompat != null) {
|
||||
val nativeInsets = insetsCompat.toWindowInsets()
|
||||
nativeInsets?.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.let { corner ->
|
||||
insetLeft += corner.radius
|
||||
}
|
||||
if (rect.right >= width - paddingRight) {
|
||||
cutoutInsetRight += rect.width()
|
||||
nativeInsets?.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.let { corner ->
|
||||
insetRight += corner.radius
|
||||
}
|
||||
} else {
|
||||
insetLeft += insetCornerFallback
|
||||
insetRight += insetCornerFallback
|
||||
}
|
||||
insetsCompat?.displayCutout?.let { cutout ->
|
||||
for (rect in cutout.boundingRects) {
|
||||
if (rect.left <= paddingLeft) {
|
||||
insetLeft += rect.width()
|
||||
}
|
||||
if (rect.right >= width - paddingRight) {
|
||||
insetRight += rect.width()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,13 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.findChapter
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
@@ -70,7 +70,9 @@ private const val BOUNDS_PAGE_OFFSET = 2
|
||||
private const val PREFETCH_LIMIT = 10
|
||||
|
||||
@HiltViewModel
|
||||
class ReaderViewModel @Inject constructor(
|
||||
class ReaderViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
@@ -85,7 +87,6 @@ class ReaderViewModel @Inject constructor(
|
||||
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
||||
private val statsCollector: StatsCollector,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
private val preselectedBranch = savedStateHandle.get<String>(ReaderActivity.EXTRA_BRANCH)
|
||||
|
||||
@@ -105,9 +106,11 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
val incognitoMode = if (savedStateHandle.get<Boolean>(ReaderActivity.EXTRA_INCOGNITO) == true) {
|
||||
MutableStateFlow(true)
|
||||
} else mangaFlow.map {
|
||||
it != null && historyRepository.shouldSkip(it)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
} else {
|
||||
mangaFlow.map {
|
||||
it != null && historyRepository.shouldSkip(it)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
}
|
||||
|
||||
val isPagesSheetEnabled = observeIsPagesSheetEnabled()
|
||||
|
||||
@@ -166,13 +169,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
|
||||
)
|
||||
|
||||
val isScreenshotsBlockEnabled = combine(
|
||||
mangaFlow,
|
||||
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
|
||||
) { manga, policy ->
|
||||
policy == ScreenshotsPolicy.BLOCK_ALL ||
|
||||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||
val isMangaNsfw = mangaFlow.map { it?.isNsfw == true }
|
||||
|
||||
val isBookmarkAdded = currentState.flatMapLatest { state ->
|
||||
val manga = mangaData.value?.toManga()
|
||||
@@ -385,9 +382,7 @@ class ReaderViewModel @Inject constructor(
|
||||
val manga = details.toManga()
|
||||
// obtain state
|
||||
if (currentState.value == null) {
|
||||
currentState.value = historyRepository.getOne(manga)?.let {
|
||||
ReaderState(it)
|
||||
} ?: ReaderState(manga, preselectedBranch ?: manga.getPreferredBranch(null))
|
||||
currentState.value = getStateFromIntent(manga)
|
||||
}
|
||||
val mode = detectReaderModeUseCase.invoke(manga, currentState.value)
|
||||
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
||||
@@ -484,4 +479,18 @@ class ReaderViewModel @Inject constructor(
|
||||
.filter { it == AppSettings.KEY_PAGES_TAB || it == AppSettings.KEY_DETAILS_TAB || it == AppSettings.KEY_DETAILS_LAST_TAB }
|
||||
.map { settings.defaultDetailsTab == TAB_PAGES }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.defaultDetailsTab == TAB_PAGES)
|
||||
|
||||
private suspend fun getStateFromIntent(manga: Manga): ReaderState {
|
||||
val history = historyRepository.getOne(manga)
|
||||
val result = if (history != null) {
|
||||
if (preselectedBranch != null && preselectedBranch != manga.findChapter(history.chapterId)?.branch) {
|
||||
null
|
||||
} else {
|
||||
ReaderState(history)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return result ?: ReaderState(manga, preselectedBranch ?: manga.getPreferredBranch(null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ abstract class BasePageHolder<B : ViewBinding>(
|
||||
}
|
||||
|
||||
protected fun SubsamplingScaleImageView.applyDownsampling(isForeground: Boolean) {
|
||||
downsampling = when {
|
||||
downSampling = when {
|
||||
isForeground || !settings.isReaderOptimizationEnabled -> 1
|
||||
context.isLowRamDevice() -> 8
|
||||
else -> 4
|
||||
|
||||
@@ -97,8 +97,8 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
setMeasuredDimension(desiredWidth, desiredHeight)
|
||||
}
|
||||
|
||||
override fun onDownsamplingChanged() {
|
||||
super.onDownsamplingChanged()
|
||||
override fun onDownSamplingChanged() {
|
||||
super.onDownSamplingChanged()
|
||||
post {
|
||||
adjustScale()
|
||||
}
|
||||
|
||||
@@ -221,7 +221,14 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
syncMatrixValues()
|
||||
}
|
||||
|
||||
private fun scaleChild(newScale: Float, focusX: Float, focusY: Float) {
|
||||
private fun scaleChild(
|
||||
newScale: Float,
|
||||
focusX: Float,
|
||||
focusY: Float,
|
||||
): Boolean {
|
||||
if (scale.isNaN() || scale == 0f) {
|
||||
return false
|
||||
}
|
||||
val factor = newScale / scale
|
||||
if (newScale > 1) {
|
||||
translateBounds.set(
|
||||
@@ -240,13 +247,12 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
}
|
||||
transformMatrix.postScale(factor, factor, focusX, focusY)
|
||||
invalidateTarget()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
val newScale = (scale * detector.scaleFactor).coerceIn(MIN_SCALE, MAX_SCALE)
|
||||
scaleChild(newScale, detector.focusX, detector.focusY)
|
||||
return true
|
||||
return scaleChild(newScale, detector.focusX, detector.focusY)
|
||||
}
|
||||
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
|
||||
@@ -51,7 +51,7 @@ abstract class Scrobbler(
|
||||
}
|
||||
}
|
||||
|
||||
val isAvailable: Boolean
|
||||
val isEnabled: Boolean
|
||||
get() = repository.isAuthorized
|
||||
|
||||
suspend fun authorize(authCode: String): ScrobblerUser {
|
||||
|
||||
@@ -42,7 +42,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
||||
|
||||
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
|
||||
|
||||
val availableScrobblers = scrobblers.filter { it.isAvailable }
|
||||
val availableScrobblers = scrobblers.filter { it.isEnabled }
|
||||
|
||||
val selectedScrobblerIndex = MutableStateFlow(0)
|
||||
|
||||
|
||||
@@ -15,11 +15,14 @@ import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
@@ -58,6 +61,8 @@ class MangaListActivity :
|
||||
"Cannot find FilterOwner fragment in ${supportFragmentManager.fragments}"
|
||||
}.filter
|
||||
|
||||
private var source: MangaSource? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityMangaListBinding.inflate(layoutInflater))
|
||||
@@ -66,16 +71,19 @@ class MangaListActivity :
|
||||
if (viewBinding.containerFilterHeader != null) {
|
||||
viewBinding.appbar.addOnOffsetChangedListener(this)
|
||||
}
|
||||
val source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
|
||||
if (source == null) {
|
||||
source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
|
||||
val src = source
|
||||
if (src == null) {
|
||||
finishAfterTransition()
|
||||
return
|
||||
} else {
|
||||
viewBinding.buttonOrder?.setOnClickListener(this)
|
||||
title = if (src == MangaSource.LOCAL) getString(R.string.local_storage) else src.title
|
||||
initList(src, tags)
|
||||
}
|
||||
viewBinding.buttonOrder?.setOnClickListener(this)
|
||||
title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title
|
||||
initList(source, tags)
|
||||
}
|
||||
|
||||
override fun isNsfwContent(): Flow<Boolean> = flowOf(source?.isNsfw() == true)
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
|
||||
@@ -172,12 +172,8 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
|
||||
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.newsources
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NewSourcesDialogFragment :
|
||||
AlertDialogFragment<DialogOnboardBinding>(),
|
||||
SourceConfigListener,
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private val viewModel by viewModels<NewSourcesViewModel>()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
): DialogOnboardBinding {
|
||||
return DialogOnboardBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val adapter = SourcesSelectAdapter(this, coil, viewLifecycleOwner)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.textViewTitle.setText(R.string.new_sources_text)
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
return super.onBuildDialog(builder)
|
||||
.setPositiveButton(R.string.done, this)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.remote_sources)
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit
|
||||
|
||||
override fun onItemLiftClick(item: SourceConfigItem.SourceItem) = Unit
|
||||
|
||||
override fun onItemShortcutClick(item: SourceConfigItem.SourceItem) = Unit
|
||||
|
||||
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||
viewModel.onItemEnabledChanged(item, isEnabled)
|
||||
}
|
||||
|
||||
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "NewSources"
|
||||
|
||||
fun show(fm: FragmentManager) = NewSourcesDialogFragment().show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.newsources
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NewSourcesViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val newSources = SuspendLazy {
|
||||
repository.assimilateNewSources()
|
||||
}
|
||||
val content: StateFlow<List<SourceConfigItem>> = repository.observeAll()
|
||||
.map { sources ->
|
||||
val new = newSources.get()
|
||||
val skipNsfw = settings.isNsfwContentDisabled
|
||||
sources.mapNotNull { (source, enabled) ->
|
||||
if (source in new) {
|
||||
SourceConfigItem.SourceItem(
|
||||
source = source,
|
||||
isEnabled = enabled,
|
||||
isDraggable = false,
|
||||
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.setSourcesEnabled(setOf(item.source), isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.newsources
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.sourceConfigItemCheckableDelegate
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
|
||||
class SourcesSelectAdapter(
|
||||
listener: SourceConfigListener,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : BaseListAdapter<SourceConfigItem>() {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(sourceConfigItemCheckableDelegate(listener, coil, lifecycleOwner))
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ sealed interface SourceCatalogItem : ListModel {
|
||||
|
||||
data class Source(
|
||||
val source: MangaSource,
|
||||
val showSummary: Boolean,
|
||||
) : SourceCatalogItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -15,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
@@ -22,28 +24,32 @@ import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemCatalogPageBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun sourceCatalogItemSourceAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>
|
||||
) = adapterDelegateViewBinding<SourceCatalogItem.Source, SourceCatalogItem, ItemSourceCatalogBinding>(
|
||||
) = adapterDelegateViewBinding<SourceCatalogItem.Source, ListModel, ItemSourceCatalogBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
|
||||
},
|
||||
) {
|
||||
|
||||
binding.imageViewAdd.setOnClickListener { v ->
|
||||
listener.onItemLongClick(item, v)
|
||||
}
|
||||
binding.root.setOnClickListener { v ->
|
||||
listener.onItemClick(item, v)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
if (item.showSummary) {
|
||||
binding.textViewDescription.text = item.source.getSummary(context)
|
||||
binding.textViewDescription.isVisible = true
|
||||
binding.textViewDescription.text = item.source.getSummary(context)
|
||||
binding.textViewDescription.drawableStart = if (item.source.isBroken) {
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_off_small)
|
||||
} else {
|
||||
binding.textViewDescription.isVisible = false
|
||||
null
|
||||
}
|
||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||
@@ -61,7 +67,7 @@ fun sourceCatalogItemSourceAD(
|
||||
fun sourceCatalogItemHintAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, SourceCatalogItem, ItemEmptyHintBinding>(
|
||||
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, ListModel, ItemEmptyHintBinding>(
|
||||
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView.ChipModel
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
AppBarOwner, MenuItem.OnActionExpandListener {
|
||||
AppBarOwner, MenuItem.OnActionExpandListener, ChipsView.OnChipClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private var newSourcesSnackbar: Snackbar? = null
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = viewBinding.appbar
|
||||
|
||||
@@ -45,18 +50,20 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val pagerAdapter = SourcesCatalogPagerAdapter(this, coil, this)
|
||||
viewBinding.pager.adapter = pagerAdapter
|
||||
val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter)
|
||||
tabMediator.attach()
|
||||
viewModel.content.observe(this, pagerAdapter)
|
||||
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged)
|
||||
val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
|
||||
with(viewBinding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
adapter = sourcesAdapter
|
||||
}
|
||||
viewBinding.chipsFilter.onChipClickListener = this
|
||||
viewModel.content.observe(this, sourcesAdapter)
|
||||
viewModel.onActionDone.observeEvent(
|
||||
this,
|
||||
ReversibleActionObserver(viewBinding.pager),
|
||||
ReversibleActionObserver(viewBinding.recyclerView),
|
||||
)
|
||||
viewModel.locale.observe(this) {
|
||||
supportActionBar?.subtitle = it?.toLocale().getDisplayName(this)
|
||||
combine(viewModel.appliedFilter, viewModel.hasNewSources, ::Pair).observe(this) {
|
||||
updateFilers(it.first, it.second)
|
||||
}
|
||||
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
|
||||
}
|
||||
@@ -66,51 +73,85 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
viewBinding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is ContentType -> viewModel.setContentType(data, chip.isChecked)
|
||||
is Boolean -> viewModel.setNewOnly(chip.isChecked)
|
||||
else -> showLocalesMenu(chip)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
||||
startActivity(MangaListActivity.newIntent(this, item.source))
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: SourceCatalogItem.Source, view: View): Boolean {
|
||||
viewModel.addSource(item.source)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
viewBinding.tabs.isVisible = false
|
||||
viewBinding.pager.isUserInputEnabled = false
|
||||
val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty()
|
||||
viewModel.performSearch(sq)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
viewBinding.tabs.isVisible = true
|
||||
viewBinding.pager.isUserInputEnabled = true
|
||||
viewModel.performSearch(null)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onHasNewSourcesChanged(hasNewSources: Boolean) {
|
||||
private fun updateFilers(
|
||||
appliedFilter: SourcesCatalogFilter,
|
||||
hasNewSources: Boolean,
|
||||
) {
|
||||
val chips = ArrayList<ChipModel>(ContentType.entries.size + 2)
|
||||
chips += ChipModel(
|
||||
title = appliedFilter.locale?.toLocale().getDisplayName(this),
|
||||
icon = R.drawable.ic_language,
|
||||
isDropdown = true,
|
||||
)
|
||||
if (hasNewSources) {
|
||||
if (newSourcesSnackbar?.isShownOrQueued == true) {
|
||||
return
|
||||
}
|
||||
val snackbar = Snackbar.make(viewBinding.pager, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbar.setAction(R.string.explore) {
|
||||
NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
snackbar.addCallback(
|
||||
object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
super.onDismissed(transientBottomBar, event)
|
||||
if (event == DISMISS_EVENT_SWIPE) {
|
||||
viewModel.skipNewSources()
|
||||
}
|
||||
}
|
||||
},
|
||||
chips += ChipModel(
|
||||
title = getString(R.string._new),
|
||||
icon = R.drawable.ic_updated_selector,
|
||||
isCheckable = true,
|
||||
isChecked = appliedFilter.isNewOnly,
|
||||
data = true,
|
||||
)
|
||||
snackbar.show()
|
||||
newSourcesSnackbar = snackbar
|
||||
} else {
|
||||
newSourcesSnackbar?.dismiss()
|
||||
newSourcesSnackbar = null
|
||||
}
|
||||
for (type in ContentType.entries) {
|
||||
if (type == ContentType.HENTAI && viewModel.isNsfwDisabled) {
|
||||
continue
|
||||
}
|
||||
chips += ChipModel(
|
||||
title = getString(type.titleResId),
|
||||
isCheckable = true,
|
||||
isChecked = type in appliedFilter.types,
|
||||
data = type,
|
||||
)
|
||||
}
|
||||
viewBinding.chipsFilter.setChips(chips)
|
||||
}
|
||||
|
||||
private fun showLocalesMenu(anchor: View) {
|
||||
val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) {
|
||||
it to it?.toLocale()
|
||||
}
|
||||
locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second })
|
||||
val menu = PopupMenu(this, anchor)
|
||||
for ((i, lc) in locales.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(this))
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
viewModel.setLocale(locales.getOrNull(it.order)?.first)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class SourcesCatalogAdapter(
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : BaseListAdapter<SourceCatalogItem>(), FastScroller.SectionIndexer {
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
|
||||
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
|
||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
|
||||
data class SourcesCatalogFilter(
|
||||
val types: Set<ContentType>,
|
||||
val locale: String?,
|
||||
val isNewOnly: Boolean,
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.room.InvalidationTracker
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||
import org.koitharu.kotatsu.core.db.removeObserverAsync
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
|
||||
class SourcesCatalogListProducer @AssistedInject constructor(
|
||||
@Assisted private val locale: String?,
|
||||
@Assisted private val contentType: ContentType,
|
||||
@Assisted lifecycle: ViewModelLifecycle,
|
||||
private val repository: MangaSourcesRepository,
|
||||
private val database: MangaDatabase,
|
||||
) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
|
||||
|
||||
private val scope = lifecycle.lifecycleScope
|
||||
private var query: String? = null
|
||||
val list = MutableStateFlow(emptyList<SourceCatalogItem>())
|
||||
|
||||
private var job = scope.launch(Dispatchers.Default) {
|
||||
list.value = buildList()
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
database.invalidationTracker.addObserver(this@SourcesCatalogListProducer)
|
||||
}
|
||||
lifecycle.addOnClearedListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
database.invalidationTracker.removeObserverAsync(this)
|
||||
}
|
||||
|
||||
override fun onInvalidated(tables: Set<String>) {
|
||||
val prevJob = job
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
prevJob.cancelAndJoin()
|
||||
list.update { buildList() }
|
||||
}
|
||||
}
|
||||
|
||||
fun setQuery(value: String?) {
|
||||
this.query = value
|
||||
onInvalidated(emptySet())
|
||||
}
|
||||
|
||||
private suspend fun buildList(): List<SourceCatalogItem> {
|
||||
val sources = repository.getDisabledSources().toMutableList()
|
||||
when (val q = query) {
|
||||
null -> sources.retainAll { it.contentType == contentType && it.locale == locale }
|
||||
"" -> return emptyList()
|
||||
else -> sources.retainAll { it.title.contains(q, ignoreCase = true) }
|
||||
}
|
||||
return if (sources.isEmpty()) {
|
||||
listOf(
|
||||
if (query == null) {
|
||||
SourceCatalogItem.Hint(
|
||||
icon = R.drawable.ic_empty_feed,
|
||||
title = R.string.no_manga_sources,
|
||||
text = R.string.no_manga_sources_catalog_text,
|
||||
)
|
||||
} else {
|
||||
SourceCatalogItem.Hint(
|
||||
icon = R.drawable.ic_empty_feed,
|
||||
title = R.string.nothing_found,
|
||||
text = R.string.no_manga_sources_found,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
sources.sortBy { it.title }
|
||||
sources.map {
|
||||
SourceCatalogItem.Source(
|
||||
source = it,
|
||||
showSummary = query != null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
locale: String?,
|
||||
contentType: ContentType,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
): SourcesCatalogListProducer
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,9 @@ import android.app.Activity
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
|
||||
class SourcesCatalogMenuProvider(
|
||||
@@ -32,14 +27,7 @@ class SourcesCatalogMenuProvider(
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_locales -> {
|
||||
showLocalesMenu()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||
@@ -57,24 +45,4 @@ class SourcesCatalogMenuProvider(
|
||||
viewModel.performSearch(newText?.trim().orEmpty())
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showLocalesMenu() {
|
||||
val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) {
|
||||
it to it?.toLocale()
|
||||
}
|
||||
locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second })
|
||||
|
||||
val anchor: View = (activity as AppBarOwner).appBar.let {
|
||||
it.findViewById<View?>(R.id.toolbar) ?: it
|
||||
}
|
||||
val menu = PopupMenu(activity, anchor)
|
||||
for ((i, lc) in locales.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(activity))
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
viewModel.setLocale(locales.getOrNull(it.order)?.first)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
|
||||
class SourcesCatalogPagerAdapter(
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : BaseListAdapter<SourceCatalogPage>(), TabLayoutMediator.TabConfigurationStrategy {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(sourceCatalogPageAD(listener, coil, lifecycleOwner))
|
||||
}
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
val item = items.getOrNull(position) ?: return
|
||||
tab.setText(item.type.titleResId)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
|
||||
import androidx.room.invalidationTrackerFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
@@ -31,41 +31,47 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class SourcesCatalogViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
private val listProducerFactory: SourcesCatalogListProducer.Factory,
|
||||
private val settings: AppSettings,
|
||||
db: MangaDatabase,
|
||||
settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val lifecycle = RetainedLifecycleImpl()
|
||||
private var searchQuery: String? = null
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val locales = repository.allMangaSources.mapToSet { it.locale }
|
||||
val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales })
|
||||
val locales: Set<String?> = repository.allMangaSources.mapTo(HashSet<String?>()) { it.locale }.also {
|
||||
it.add(null)
|
||||
}
|
||||
|
||||
val hasNewSources = repository.observeNewSources()
|
||||
.map { it.isNotEmpty() }
|
||||
private val searchQuery = MutableStateFlow<String?>(null)
|
||||
val appliedFilter = MutableStateFlow(
|
||||
SourcesCatalogFilter(
|
||||
types = emptySet(),
|
||||
locale = Locale.getDefault().language.takeIf { it in locales },
|
||||
isNewOnly = false,
|
||||
),
|
||||
)
|
||||
|
||||
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||
|
||||
val hasNewSources = repository.observeHasNewSources()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||
|
||||
private val listProducers = locale.map { lc ->
|
||||
createListProducers(lc)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value))
|
||||
val content: StateFlow<List<ListModel>> = combine(
|
||||
searchQuery,
|
||||
appliedFilter,
|
||||
db.invalidationTrackerFlow(TABLE_SOURCES),
|
||||
) { q, f, _ ->
|
||||
buildSourcesList(f, q)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
|
||||
val content: StateFlow<List<SourceCatalogPage>> = listProducers.flatMapLatest {
|
||||
val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } }
|
||||
combine<SourceCatalogPage, List<SourceCatalogPage>>(flows, Array<SourceCatalogPage>::toList)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
lifecycle.dispatchOnCleared()
|
||||
init {
|
||||
repository.clearNewSourcesBadge()
|
||||
}
|
||||
|
||||
fun performSearch(query: String?) {
|
||||
searchQuery = query
|
||||
listProducers.value.forEach { (_, v) -> v.setQuery(query) }
|
||||
searchQuery.value = query?.trim()
|
||||
}
|
||||
|
||||
fun setLocale(value: String?) {
|
||||
locale.value = value
|
||||
appliedFilter.value = appliedFilter.value.copy(locale = value)
|
||||
}
|
||||
|
||||
fun addSource(source: MangaSource) {
|
||||
@@ -75,21 +81,53 @@ class SourcesCatalogViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun skipNewSources() {
|
||||
launchJob {
|
||||
repository.assimilateNewSources()
|
||||
fun setContentType(value: ContentType, isAdd: Boolean) {
|
||||
val filter = appliedFilter.value
|
||||
val types = EnumSet.noneOf(ContentType::class.java)
|
||||
types.addAll(filter.types)
|
||||
if (isAdd) {
|
||||
types.add(value)
|
||||
} else {
|
||||
types.remove(value)
|
||||
}
|
||||
appliedFilter.value = filter.copy(types = types)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> {
|
||||
val types = EnumSet.allOf(ContentType::class.java)
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
types.remove(ContentType.HENTAI)
|
||||
}
|
||||
return types.associateWithTo(EnumMap(ContentType::class.java)) { type ->
|
||||
listProducerFactory.create(lc, type, lifecycle).also {
|
||||
it.setQuery(searchQuery)
|
||||
fun setNewOnly(value: Boolean) {
|
||||
appliedFilter.value = appliedFilter.value.copy(isNewOnly = value)
|
||||
}
|
||||
|
||||
private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List<SourceCatalogItem> {
|
||||
val sources = repository.getAvailableSources(
|
||||
isDisabledOnly = true,
|
||||
isNewOnly = filter.isNewOnly,
|
||||
excludeBroken = false,
|
||||
types = filter.types,
|
||||
query = query,
|
||||
locale = filter.locale,
|
||||
sortOrder = SourcesSortOrder.ALPHABETIC,
|
||||
)
|
||||
return if (sources.isEmpty()) {
|
||||
listOf(
|
||||
if (query == null) {
|
||||
SourceCatalogItem.Hint(
|
||||
icon = R.drawable.ic_empty_feed,
|
||||
title = R.string.no_manga_sources,
|
||||
text = R.string.no_manga_sources_catalog_text,
|
||||
)
|
||||
} else {
|
||||
SourceCatalogItem.Hint(
|
||||
icon = R.drawable.ic_empty_feed,
|
||||
title = R.string.nothing_found,
|
||||
text = R.string.no_manga_sources_found,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
sources.sortedBy {
|
||||
it.isBroken
|
||||
}.map {
|
||||
SourceCatalogItem.Source(source = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
@@ -22,6 +23,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
@@ -29,6 +31,7 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
@@ -63,6 +66,10 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
appShortcutManager.isDynamicShortcutsAvailable()
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
findPreference<ListPreference>(AppSettings.KEY_SCREENSHOTS_POLICY)?.run {
|
||||
entryValues = ScreenshotsPolicy.entries.names()
|
||||
setDefaultValueCompat(ScreenshotsPolicy.ALLOW.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.stats.data.StatsEntity
|
||||
import javax.inject.Inject
|
||||
@@ -62,7 +64,11 @@ class StatsCollector @Inject constructor(
|
||||
|
||||
private fun commit(entity: StatsEntity) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
db.getStatsDao().upsert(entity)
|
||||
runCatchingCancellable {
|
||||
db.getStatsDao().upsert(entity)
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
|
||||
.setIcon(R.drawable.ic_delete)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clear()
|
||||
viewModel.clearStats()
|
||||
}.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package org.koitharu.kotatsu.stats.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
@@ -63,7 +56,7 @@ class StatsViewModel @Inject constructor(
|
||||
selectedCategories.value = snapshot
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
fun clearStats() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
repository.clearStats()
|
||||
readingStats.value = emptyList()
|
||||
|
||||
@@ -25,6 +25,7 @@ class TrackEntity(
|
||||
@ColumnInfo(name = "last_check_time") val lastCheckTime: Long,
|
||||
@ColumnInfo(name = "last_chapter_date") val lastChapterDate: Long,
|
||||
@ColumnInfo(name = "last_result") val lastResult: Int,
|
||||
@ColumnInfo(name = "last_error") val lastError: String?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -42,6 +43,7 @@ class TrackEntity(
|
||||
lastCheckTime = 0L,
|
||||
lastChapterDate = 0,
|
||||
lastResult = RESULT_NONE,
|
||||
lastError = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ class TrackingRepository @Inject constructor(
|
||||
lastCheckTime = tracking.lastCheck?.toEpochMilli() ?: 0L,
|
||||
lastChapterDate = tracking.lastChapterDate?.toEpochMilli() ?: 0L,
|
||||
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
||||
lastError = null,
|
||||
)
|
||||
db.getTracksDao().upsert(entity)
|
||||
}
|
||||
@@ -230,6 +231,7 @@ class TrackingRepository @Inject constructor(
|
||||
lastCheckTime = System.currentTimeMillis(),
|
||||
lastChapterDate = lastChapterDate,
|
||||
lastResult = TrackEntity.RESULT_FAILED,
|
||||
lastError = updates.error?.toString(),
|
||||
)
|
||||
|
||||
is MangaUpdates.Success -> TrackEntity(
|
||||
@@ -239,6 +241,7 @@ class TrackingRepository @Inject constructor(
|
||||
lastCheckTime = System.currentTimeMillis(),
|
||||
lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate },
|
||||
lastResult = if (updates.isNotEmpty()) TrackEntity.RESULT_HAS_UPDATE else TrackEntity.RESULT_NO_UPDATE,
|
||||
lastError = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
13
app/src/main/res/drawable/ic_off_small.xml
Normal file
13
app/src/main/res/drawable/ic_off_small.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M22.11 21.46L2.39 1.73L1.11 3L6.25 8.14C6.1 8.41 6 8.7 6 9V14.5L9.5 18V21H14.5V18L15.31 17.2L20.84 22.73L22.11 21.46M13.09 16.59L12.67 17H11.33L10.92 16.59L8 13.67V9.89L13.89 15.78L13.09 16.59M12.2 9L10.2 7H14V3H16V7C17 7 18 8 18 9V14.5L17.85 14.65L16 12.8V9.09C16 9.06 15.95 9 15.92 9H12.2M10 6.8L8 4.8V3H10V6.8Z" />
|
||||
|
||||
</vector>
|
||||
@@ -18,6 +18,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:paddingHorizontal="6dp"
|
||||
android:paddingTop="8dp"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
android:layout_marginEnd="6dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
app:collapseIcon="@null"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:navigationContentDescription="@string/search"
|
||||
app:navigationIcon="?attr/actionModeWebSearchDrawable">
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:padding="6dp"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -17,21 +17,39 @@
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scrollView_chips"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabGravity="start"
|
||||
app:tabMode="scrollable" />
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="@dimen/list_spacing_large"
|
||||
android:scrollbars="none">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
|
||||
app:selectionRequired="false"
|
||||
app:singleLine="true"
|
||||
app:singleSelection="false" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusable="true"
|
||||
app:doubleTapZoomStyle="center"
|
||||
app:restoreStrategy="deferred" />
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:windowBackground"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal"
|
||||
@@ -45,17 +45,27 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
tools:drawableStart="@drawable/ic_off_small"
|
||||
tools:text="English" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:background="?colorOutline" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/list_spacing_small"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/add"
|
||||
android:padding="@dimen/margin_small"
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_locales"
|
||||
android:icon="@drawable/ic_expand_more"
|
||||
android:title="@string/languages"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="?actionModeWebSearchDrawable"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="zero">%1$d دقيقة مضت</item>
|
||||
<item quantity="one">%1$d دقيقة مضت</item>
|
||||
<item quantity="two">%1$d دقائق مضت</item>
|
||||
<item quantity="two">%1$d دقيقتين مضت</item>
|
||||
<item quantity="few">%1$d دقائق مضت</item>
|
||||
<item quantity="many">%1$d دقائق مضت</item>
|
||||
<item quantity="other">%1$d دقائق مضت</item>
|
||||
@@ -57,7 +57,7 @@
|
||||
<item quantity="other">%1$d ساعات مضت</item>
|
||||
</plurals>
|
||||
<plurals name="minutes">
|
||||
<item quantity="zero"></item>
|
||||
<item quantity="zero"/>
|
||||
<item quantity="one">دقيقة</item>
|
||||
<item quantity="two">دقيقتان</item>
|
||||
<item quantity="few">ثلاث دقائق</item>
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
<string name="options">خيارات</string>
|
||||
<string name="incognito_mode">الوضع الخفي</string>
|
||||
<string name="automatic_scroll">تمرير تلقائي</string>
|
||||
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
|
||||
<string name="reader_info_pattern">فصل %1$d/%2$d صفحة %3$d/%4$d</string>
|
||||
<string name="reader_info_bar">عرض شريط المعلومات في قارئ الصفحات</string>
|
||||
<string name="folder_with_images">مجلد مع صور</string>
|
||||
<string name="exclude_nsfw_from_history_summary">المانغا +18 لن يتم إضافتها إلى السجل، ولن يتم حفظ تقدمك فيها.\"</string>
|
||||
@@ -334,4 +334,53 @@
|
||||
<string name="import_completed_hint">يمكنك حذف الملف الأصلي من التخزين لتوفير مساحة</string>
|
||||
<string name="import_will_start_soon">الإستيراد سيبدأ عن قريب</string>
|
||||
<string name="history_shortcuts">إظهار اختصارات المانجا الحديثة</string>
|
||||
<string name="network_unavailable_hint">قم بتشغيل الواي فاي أو شبكة الهاتف المحمول لقراءة المانجا عبر الإنترنت</string>
|
||||
<string name="contrast">تباين</string>
|
||||
<string name="text_unsaved_changes_prompt">هل تريد حفظ أو تجاهل التغييرات الغير المحفوظة؟</string>
|
||||
<string name="error_no_space_left">لا توجد مساحة متبقية على الجهاز</string>
|
||||
<string name="reader_slider">إظهار شريط التمرير لتبديل الصفحات</string>
|
||||
<string name="server_error">خطأ من جانب الخادم (%1$s). الرجاء المحاولة مرة أخرى لاحقًا</string>
|
||||
<string name="chapters_grid_view">عرض الشبكة</string>
|
||||
<string name="manga_error_description_pattern">تفاصيل الخطأ:<br><tt>%1$s</tt><br><br>1. حاول <a href=\"%2$s\">فتح المانجا في متصفح ويب</a> للتأكد من أنها متوفرة على مصدرها<br>2. تأكد من أنك تستخدم <a href=kotatsu://about>أحدث إصدار من Kotatsu</a><br>3. إذا كانت متوفرة، أرسل تقرير خطأ إلى المطورين.</string>
|
||||
<string name="history_shortcuts_summary">إتاحة المانجا الحديثة بالضغط المطول على أيقونة التطبيق</string>
|
||||
<string name="reader_control_ltr_summary">النقر على الحافة اليمنى أو الضغط على المفتاح الأيمن يؤدي دائمًا إلى الانتقال للصفحة التالية.</string>
|
||||
<string name="reader_control_ltr">تحكم مريح في القراءة</string>
|
||||
<string name="brightness">سطوع</string>
|
||||
<string name="clear_new_chapters_counters">قم أيضًا بمسح المعلومات حول الفصول الجديدة</string>
|
||||
<string name="color_correction">تصحيح الألوان</string>
|
||||
<string name="reset">إعادة تعيين</string>
|
||||
<string name="discard">تجاهل</string>
|
||||
<string name="webtoon_zoom">تكبير الويبتون</string>
|
||||
<string name="network_unavailable">الشبكة غير متاحة</string>
|
||||
<string name="more">المزيد</string>
|
||||
<string name="prefetch_content">اعادة تحميل المحتوى</string>
|
||||
<string name="enable_logging">تفعيل التسجيل</string>
|
||||
<string name="theme_name_asuka">أسوكا</string>
|
||||
<string name="remove_completed_downloads_confirm">سيتم حذف سجل التحميلات خاصتك بشكل دائم</string>
|
||||
<string name="theme_name_dynamic">الديناميكية</string>
|
||||
<string name="mark_as_current">تسجيل على كونها الحالي</string>
|
||||
<string name="theme_name_mamimi">ماميمي</string>
|
||||
<string name="theme_name_kanade">كانادي</string>
|
||||
<string name="got_it">وجدتها</string>
|
||||
<string name="downloads_wifi_only_summary">إيقاف التحميل عند الانتقال إلى شبكة الهاتف المحمول</string>
|
||||
<string name="resume">استئناف</string>
|
||||
<string name="cancel_all">إلغاء الكل</string>
|
||||
<string name="source_disabled">المصدر معطل</string>
|
||||
<string name="theme_name_rikka">ريكا</string>
|
||||
<string name="theme_name_sakura">ساكورا</string>
|
||||
<string name="pause">إيقاف مؤقت</string>
|
||||
<string name="show_on_shelf">العرض في الرف</string>
|
||||
<string name="remove_completed">تمت الإزالة</string>
|
||||
<string name="enable_logging_summary">سجل بعض الأفعال لغايات التصحيح. لا تقم بتشغيله إذا لم تكن متأكدًا مما تفعله</string>
|
||||
<string name="language">اللغة</string>
|
||||
<string name="show_suspicious_content">إظهار المحتوى مشكوك فيه</string>
|
||||
<string name="color_theme">مخطط الألوان</string>
|
||||
<string name="show_in_grid_view">العرض في الشبكة</string>
|
||||
<string name="theme_name_miku">ميكو</string>
|
||||
<string name="theme_name_mion">ميون</string>
|
||||
<string name="nothing_here">لا يوجد شيئ هنا</string>
|
||||
<string name="sync_auth_hint">يمكنك تسجيل الدخول إلى حساب موجود أصلا أو إنشاء حساب جديد</string>
|
||||
<string name="paused">متوقف مؤقتاً</string>
|
||||
<string name="downloads_wifi_only">التحميل عبر شبكة الوايفاي فقط</string>
|
||||
<string name="suggestions_notifications_summary">إظهار الإشعارات أحيانًا بالمانغا المقترحة</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,5 @@
|
||||
<string name="disable_nsfw_notifications">Адключыць апавяшчэння NSFW</string>
|
||||
<string name="disable">Адкл.</string>
|
||||
<string name="sources_disabled">Крыніцы адключаны</string>
|
||||
<string name="_new">Новае</string>
|
||||
</resources>
|
||||
@@ -632,4 +632,16 @@
|
||||
<string name="pin_navigation_ui">Upevnit uživatelské rozhraní pro navigaci</string>
|
||||
<string name="fix">Upevnit</string>
|
||||
<string name="pin_navigation_ui_summary">Neskrývat navigační panel a zobrazení vyhledávání při posouvání</string>
|
||||
<string name="disable_connectivity_check">Vypnout kontrolu připojení</string>
|
||||
<string name="ignore_ssl_errors_summary">Můžeš vypnout SSL certifikáty pokud se potýkáš s problémy při připojení k internetovým zdrojům. Toto může ovlivnit tvoji bezpečnost. Restart aplikace je požadován po změnění tohoto nastavení.</string>
|
||||
<string name="sources_disabled">Vypnout zdroje</string>
|
||||
<string name="disable">Vypnout</string>
|
||||
<string name="_new">Nový l</string>
|
||||
<string name="disable_connectivity_check_summary">Přeskoč kontrolu připojení pokud s tím máš problémy (např. zapnout offline režim i když jsi připojený k internetu)</string>
|
||||
<string name="disable_nsfw_notifications_summary">Nezobrazovat notifikace o nových NSFW manga kapitolách</string>
|
||||
<string name="all_languages">Všechny jazyky</string>
|
||||
<string name="screenshots_block_incognito">Zablokovat když je privátní režim</string>
|
||||
<string name="disable_nsfw_notifications">Vypnout NSFW oznámení</string>
|
||||
<string name="tracker_debug_info">Log kontroly nových kapitol</string>
|
||||
<string name="tracker_debug_info_summary">Debug informace o kontrole nových kapitol na pozadí</string>
|
||||
</resources>
|
||||
@@ -609,4 +609,7 @@
|
||||
<string name="enable_source">Quelle aktivieren</string>
|
||||
<string name="unsupported_source">Diese Manga-Quelle wird nicht unterstützt</string>
|
||||
<string name="show_pages_thumbs">Seitenvorschau anzeigen</string>
|
||||
<string name="unsupported_backup_message">Bitte wähle eine richtige Kotatsu-Backup-Datei aus</string>
|
||||
<string name="show_pages_thumbs_summary">Registerkarte \"Seiten\" auf dem Detailbildschirm aktivieren</string>
|
||||
<string name="error_no_data_received">Keine Daten vom Server erhalten</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,7 @@
|
||||
<string name="disable_nsfw_notifications_summary">No mostrar notificaciones sobre actualizaciones de manga NSFW</string>
|
||||
<string name="tracker_debug_info">Comprobando el registro de nuevos capítulos</string>
|
||||
<string name="tracker_debug_info_summary">Información de depuración sobre verificaciones de antecedentes para nuevos capítulos</string>
|
||||
<string name="_new">Nuevos</string>
|
||||
<string name="all_languages">Todos los idiomas</string>
|
||||
<string name="screenshots_block_incognito">Bloquear en modo incógnito</string>
|
||||
</resources>
|
||||
@@ -637,4 +637,11 @@
|
||||
<string name="disable_connectivity_check">Di paganahin ang pagtingin sa koneksyon</string>
|
||||
<string name="disable_connectivity_check_summary">Laktawan ang pagsuri sa koneksyon kung sakaling mayroon kang isyu rito (hal. pagpunta sa offline mode kahit na nakakonekta sa network)</string>
|
||||
<string name="ignore_ssl_errors_summary">Maaaring di paganahin ang pag-verify ng mga SSL certificate kung sakaling makaharap ka ng mga isyu na nauugnay sa SSL kapag nag-a-access ng mga network resource. Ito ay makaapekto sa iyong seguridad. Kinakailangang mag-restart ang aplikasyon pagkatapos baguhin ang setting na ito.</string>
|
||||
<string name="disable_nsfw_notifications_summary">Huwag magpakita ng mga abiso tungkol sa mga update ng NSFW manga</string>
|
||||
<string name="tracker_debug_info">Sinusuri ang mga log ng mga bagong kabanata</string>
|
||||
<string name="tracker_debug_info_summary">Debug na impormasyon tungkol sa mga pagsusuri sa background para sa mga bagong kabanata</string>
|
||||
<string name="disable_nsfw_notifications">Di paganahin ang mga abisong NSFW</string>
|
||||
<string name="_new">Mga bago</string>
|
||||
<string name="screenshots_block_incognito">Harangan pag naka-incognito mode</string>
|
||||
<string name="all_languages">Lahat ng wika</string>
|
||||
</resources>
|
||||
@@ -631,4 +631,15 @@
|
||||
<string name="less_frequently">Moins souvent</string>
|
||||
<string name="new_chapters_pattern">%1$s : %2$d</string>
|
||||
<string name="pin_navigation_ui_summary">Ne pas masquer la barre de navigation et la vue de recherche lors du défilement</string>
|
||||
<string name="ignore_ssl_errors_summary">Vous pouvez désactiver la vérification des certificats SSL au cas où vous rencontreriez des problèmes liés à SSL lors de l\'accès aux ressources réseau. Cela peut affecter votre sécurité. Le redémarrage de l\'application est requis après avoir modifié ce paramètre.</string>
|
||||
<string name="frequency_of_check">Fréquence de vérification</string>
|
||||
<string name="disable">Désactiver</string>
|
||||
<string name="sources_disabled">Sources désactivées</string>
|
||||
<string name="disable_connectivity_check">Désactiver la vérification de la connectivité</string>
|
||||
<string name="disable_connectivity_check_summary">Ignorez la vérification de la connectivité au cas où vous rencontreriez des problèmes (par exemple, passage en mode hors ligne alors que le réseau est connecté)</string>
|
||||
<string name="disable_nsfw_notifications">Désactiver les notifications NSFW</string>
|
||||
<string name="disable_nsfw_notifications_summary">Ne pas afficher les notifications concernant les mises à jour des mangas NSFW</string>
|
||||
<string name="tracker_debug_info">Vérification du journal des nouveaux chapitres</string>
|
||||
<string name="tracker_debug_info_summary">Informations de débogage sur la vérification en arrière-plan des nouveaux chapitres</string>
|
||||
<string name="_new">Nouveaux</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,7 @@
|
||||
<string name="disable_nsfw_notifications_summary">NSFW मंगा अपडेट के बारे में सूचनाएं न दिखाएं</string>
|
||||
<string name="tracker_debug_info">नए अध्याय लॉग की जांच की जा रही है</string>
|
||||
<string name="tracker_debug_info_summary">नए अध्यायों के लिए पृष्ठभूमि जांच के बारे में जानकारी डीबग करें</string>
|
||||
<string name="_new">नया</string>
|
||||
<string name="all_languages">सभी भाषाएं</string>
|
||||
<string name="screenshots_block_incognito">गुप्त मोड में ब्लॉक करें</string>
|
||||
</resources>
|
||||
48
app/src/main/res/values-hr/plurals.xml
Normal file
48
app/src/main/res/values-hr/plurals.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$d stavka</item>
|
||||
<item quantity="few">%1$d stavke</item>
|
||||
<item quantity="other">%1$d stavkih</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%1$d novo poglavlje</item>
|
||||
<item quantity="few">%1$d nova poglavlja</item>
|
||||
<item quantity="other">%1$d novih poglavlja</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$d poglavlje</item>
|
||||
<item quantity="few">%1$d poglavlja</item>
|
||||
<item quantity="other">%1$d poglavlja</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">prije %1$d minute</item>
|
||||
<item quantity="few">prije %1$d minuta</item>
|
||||
<item quantity="other">prije %1$d minuta</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">prije %1$d sat</item>
|
||||
<item quantity="few">prije %1$d sata</item>
|
||||
<item quantity="other">prije %1$d sati</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">prije %1$d dan</item>
|
||||
<item quantity="few">prije %1$d dana</item>
|
||||
<item quantity="other">prije %1$d dana</item>
|
||||
</plurals>
|
||||
<plurals name="months_ago">
|
||||
<item quantity="one">prije %1$d mjesec</item>
|
||||
<item quantity="few">prije %1$d mjeseca</item>
|
||||
<item quantity="other">prije %1$d mjeseci</item>
|
||||
</plurals>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">%1$d sat</item>
|
||||
<item quantity="few">%1$d sata</item>
|
||||
<item quantity="other">%1$d sati</item>
|
||||
</plurals>
|
||||
<plurals name="minutes">
|
||||
<item quantity="one">%1$d minuta</item>
|
||||
<item quantity="few">%1$d minute</item>
|
||||
<item quantity="other">%1$d minuta</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
642
app/src/main/res/values-hr/strings.xml
Normal file
642
app/src/main/res/values-hr/strings.xml
Normal file
@@ -0,0 +1,642 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="missing_storage_permission">Nema dopuštenja za pristup mangi na vanjskoj pohrani</string>
|
||||
<string name="show_pages_thumbs_summary">Omogućite karticu \"Stranice\" na zaslonu s detaljima</string>
|
||||
<string name="error_no_data_received">Nikakvi podaci nisu primljeni s poslužitelja</string>
|
||||
<string name="unsupported_backup_message">Odaberite odgovarajuću datoteku sigurnosne kopije Kotatsu</string>
|
||||
<string name="hours_short">%d h</string>
|
||||
<string name="minutes_short">%d m</string>
|
||||
<string name="hours_minutes_short">%1$d h %2$d m</string>
|
||||
<string name="fix">Popravi</string>
|
||||
<string name="show_updated">Prikaži ažurirano</string>
|
||||
<string name="webtoon_gaps_summary">Prikaži okomite razmake između stranica u načinu webtoon</string>
|
||||
<string name="more_frequently">Češće</string>
|
||||
<string name="pin_navigation_ui_summary">Nemoj skrivati navigacijsku traku i prikaz pretraživanja prilikom pomicanja</string>
|
||||
<string name="suggested_queries">Predloženi upiti</string>
|
||||
<string name="last_used">Zadnje korišteno</string>
|
||||
<string name="webtoon_gaps">Praznine u webtoon modu</string>
|
||||
<string name="blocked_by_server_message">Poslužitelj vas je blokirao. Pokušajte koristiti drugu mrežnu vezu (VPN, proxy itd.)</string>
|
||||
<string name="less_frequently">Rjeđe</string>
|
||||
<string name="frequency_of_check">Učestalost provjere</string>
|
||||
<string name="recent_queries">Nedavni upiti</string>
|
||||
<string name="disable">Onemogući</string>
|
||||
<string name="pin_navigation_ui">Zakači navigacijsko sučelje</string>
|
||||
<string name="search_suggestions">Prijedlozi za pretraživanje</string>
|
||||
<string name="authors">Autori</string>
|
||||
<string name="sources_disabled">Izvori onemogućeni</string>
|
||||
<string name="local_storage">Lokalna pohrana</string>
|
||||
<string name="favourites">Favoriti</string>
|
||||
<string name="history">Povijest</string>
|
||||
<string name="error_occurred">Dogodila se greška</string>
|
||||
<string name="details">Detalji</string>
|
||||
<string name="chapters">Poglavlja</string>
|
||||
<string name="list">Popis</string>
|
||||
<string name="detailed_list">Detaljan popis</string>
|
||||
<string name="grid">Mreža</string>
|
||||
<string name="settings">Postavke</string>
|
||||
<string name="remote_sources">Manga izvori</string>
|
||||
<string name="loading_">Učitavanje…</string>
|
||||
<string name="computing_">Računanje…</string>
|
||||
<string name="chapter_d_of_d">Poglavlje %1$d od %2$d</string>
|
||||
<string name="close">Zatvori</string>
|
||||
<string name="try_again">Pokušajte ponovno</string>
|
||||
<string name="clear_history">Obriši povijest</string>
|
||||
<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_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="share_s">Podijeli %s</string>
|
||||
<string name="search">Pretraži</string>
|
||||
<string name="search_manga">Pretraži mangu</string>
|
||||
<string name="manga_downloading_">Preuzimanje…</string>
|
||||
<string name="network_error">Pogreška mreže</string>
|
||||
<string name="list_mode">Modus popisa</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="light">Svijetla</string>
|
||||
<string name="dark">Tamna</string>
|
||||
<string name="clear">Očisti</string>
|
||||
<string name="remove">Ukloni</string>
|
||||
<string name="_s_deleted_from_local_storage">\"%s\" izbrisano iz lokalne pohrane</string>
|
||||
<string name="page_saved">Spremljeno</string>
|
||||
<string name="share_image">Dijeli sliku</string>
|
||||
<string name="_import">Uvoz</string>
|
||||
<string name="delete">Izbriši</string>
|
||||
<string name="operation_not_supported">Ova operacija nije podržana</string>
|
||||
<string name="text_file_not_supported">Odaberite ZIP ili CBZ datoteku.</string>
|
||||
<string name="no_description">Bez opisa</string>
|
||||
<string name="clear_pages_cache">Očisti predmemoriju stranice</string>
|
||||
<string name="read_mode">Način čitanja</string>
|
||||
<string name="grid_size">Veličina mreže</string>
|
||||
<string name="search_on_s">Traži na %s</string>
|
||||
<string name="delete_manga">Izbriši mangu</string>
|
||||
<string name="text_delete_local_manga">Trajno izbrisati \"%s\" s uređaja?</string>
|
||||
<string name="reader_settings">Postavke čitača</string>
|
||||
<string name="switch_pages">Promijenite stranice</string>
|
||||
<string name="_continue">Nastaviti</string>
|
||||
<string name="error">Greška</string>
|
||||
<string name="clear_thumbs_cache">Očisti predmemoriju sličica</string>
|
||||
<string name="search_history_cleared">Očišćeno</string>
|
||||
<string name="internal_storage">Interna pohrana</string>
|
||||
<string name="external_storage">Vanjska pohrana</string>
|
||||
<string name="domain">Domena</string>
|
||||
<string name="app_update_available">Dostupna je nova verzija aplikacije</string>
|
||||
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">Omogućeno je %1$d od %2$d</string>
|
||||
<string name="new_chapters">Nova poglavlja</string>
|
||||
<string name="download">Preuzimi</string>
|
||||
<string name="notifications_settings">Postavke obavijesti</string>
|
||||
<string name="notification_sound">Zvuk obavijesti</string>
|
||||
<string name="light_indicator">LED indikator</string>
|
||||
<string name="vibration">Vibracija</string>
|
||||
<string name="favourites_categories">Omiljene kategorije</string>
|
||||
<string name="text_search_holder_secondary">Pokušajte preformulirati upit.</string>
|
||||
<string name="text_history_holder_primary">Ono što pročitate bit će prikazano ovdje</string>
|
||||
<string name="text_history_holder_secondary">Pronađite što čitati u odjeljku «Istražite»</string>
|
||||
<string name="text_local_holder_primary">Prvo spremite nešto</string>
|
||||
<string name="text_local_holder_secondary">Spremite nešto iz online kataloga ili uvezite iz datoteke.</string>
|
||||
<string name="manga_shelf">Polica</string>
|
||||
<string name="recent_manga">Nedavno</string>
|
||||
<string name="pages_animation">Animacija stranice</string>
|
||||
<string name="manga_save_location">Mapa preuzimanja</string>
|
||||
<string name="not_available">Nije dostupno</string>
|
||||
<string name="cannot_find_available_storage">Nema dostupnog prostora za pohranu</string>
|
||||
<string name="other_storage">Ostala pohrana</string>
|
||||
<string name="all_favourites">Svi favoriti</string>
|
||||
<string name="favourites_category_empty">Prazna kategorija</string>
|
||||
<string name="text_feed_holder">Ovdje su prikazana nova poglavlja onoga što čitate</string>
|
||||
<string name="search_results">Rezultati pretraživanja</string>
|
||||
<string name="new_version_s">Nova verzija: %s</string>
|
||||
<string name="dont_check">Ne provjeravaj</string>
|
||||
<string name="enter_password">Upišite lozinku</string>
|
||||
<string name="wrong_password">Pogrešna lozinka</string>
|
||||
<string name="protect_application">Zaštitite aplikaciju</string>
|
||||
<string name="protect_application_summary">Traži lozinku prilikom pokretanja Kotatsua</string>
|
||||
<string name="repeat_password">Ponovite lozinku</string>
|
||||
<string name="passwords_mismatch">Nepodudarne lozinke</string>
|
||||
<string name="about">O aplikaciji</string>
|
||||
<string name="app_version">Verzija %s</string>
|
||||
<string name="check_for_updates">Provjerite ima li ažuriranja</string>
|
||||
<string name="no_update_available">Nema dostupnih ažuriranja</string>
|
||||
<string name="right_to_left">S desna na lijevo</string>
|
||||
<string name="create_category">Nova kategorija</string>
|
||||
<string name="scale_mode">Način skaliranja</string>
|
||||
<string name="zoom_mode_fit_width">Prilagodi širini</string>
|
||||
<string name="zoom_mode_fit_height">Prilagodi visini</string>
|
||||
<string name="zoom_mode_fit_center">Prilagodi sredini</string>
|
||||
<string name="zoom_mode_keep_start">Zadrži na početku</string>
|
||||
<string name="black_dark_theme">Crno</string>
|
||||
<string name="black_dark_theme_summary">Koristi manje energije na AMOLED zaslonima</string>
|
||||
<string name="backup_restore">Sigurnosno kopiranje i vraćanje</string>
|
||||
<string name="create_backup">Stvorite sigurnosnu kopiju podataka</string>
|
||||
<string name="group">Grupa</string>
|
||||
<string name="today">Danas</string>
|
||||
<string name="tap_to_try_again">Dodirnite za ponovni pokušaj</string>
|
||||
<string name="reader_mode_hint">Odabrana konfiguracija bit će zapamćena za ovu mangu</string>
|
||||
<string name="silent">Tiho</string>
|
||||
<string name="captcha_required">Potrebna CAPTCHA</string>
|
||||
<string name="captcha_solve">Riješi</string>
|
||||
<string name="clear_cookies">Obriši kolačiće</string>
|
||||
<string name="cookies_cleared">Svi kolačići su uklonjeni</string>
|
||||
<string name="clear_feed">Očisti novosti</string>
|
||||
<string name="text_clear_updates_feed_prompt">Trajno izbrisati svu povijest ažuriranja?</string>
|
||||
<string name="check_for_new_chapters">Provjerite ima li novih poglavlja</string>
|
||||
<string name="reverse">Unazad</string>
|
||||
<string name="chapters_grid_view">Mrežni prikaz</string>
|
||||
<string name="sign_in">Prijaviti se</string>
|
||||
<string name="auth_required">Prijavite se da vidite ovaj sadržaj</string>
|
||||
<string name="default_s">Zadano: %s</string>
|
||||
<string name="next">Sljedeće</string>
|
||||
<string name="protect_application_subtitle">Unesite lozinku za pokretanje aplikacije</string>
|
||||
<string name="confirm">Potvrdi</string>
|
||||
<string name="password_length_hint">Lozinka mora imati 4 znaka ili više</string>
|
||||
<string name="tracker_warning">Neki uređaji imaju drugačije ponašanje sustava, što može prekinuti pozadinske zadatke.</string>
|
||||
<string name="read_more">Čitaj više</string>
|
||||
<string name="queued">U redu čekanja</string>
|
||||
<string name="chapter_is_missing">Poglavlje nedostaje</string>
|
||||
<string name="about_app_translation_summary">Prevedi ovu aplikaciju</string>
|
||||
<string name="about_app_translation">Prijevod</string>
|
||||
<string name="auth_complete">Ovlašteni</string>
|
||||
<string name="auth_not_supported_by">Prijava na %s nije podržana</string>
|
||||
<string name="text_clear_cookies_prompt">Bit ćete odjavljeni sa svih izvora</string>
|
||||
<string name="genres">Žanrovi</string>
|
||||
<string name="state_finished">Završeno</string>
|
||||
<string name="state_ongoing">U tijeku</string>
|
||||
<string name="system_default">Zadano</string>
|
||||
<string name="exclude_nsfw_from_history">Isključi NSFW mangu iz povijesti</string>
|
||||
<string name="show_pages_numbers">Numerirane stranice</string>
|
||||
<string name="screenshots_policy">Pravila snimanja zaslona</string>
|
||||
<string name="suggestions_info">Svi se podaci analiziraju samo lokalno na ovom uređaju i nikada se ne šalju nigdje.</string>
|
||||
<string name="text_suggestion_holder">Počnite čitati mangu i dobit ćete personalizirane prijedloge</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Nemojte predlagati NSFW mangu</string>
|
||||
<string name="reset_filter">Resetiraj filter</string>
|
||||
<string name="onboard_text">Odaberite jezike na kojima želite čitati mangu. Kasnije ga možete promijeniti u postavkama.</string>
|
||||
<string name="never">Nikada</string>
|
||||
<string name="only_using_wifi">Samo na Wi-Fi</string>
|
||||
<string name="always">Uvijek</string>
|
||||
<string name="logged_in_as">Prijavljeni kao %s</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="various_languages">Razni jezici</string>
|
||||
<string name="search_chapters">Pronađi poglavlje</string>
|
||||
<string name="chapters_will_removed_background">Poglavlja će biti uklonjena u pozadini</string>
|
||||
<string name="canceled">Otkazano</string>
|
||||
<string name="account_already_exists">Račun već postoji</string>
|
||||
<string name="back">Nazad</string>
|
||||
<string name="sync">Sinkronizacija</string>
|
||||
<string name="sync_title">Sinkronizirajte vaše podatke</string>
|
||||
<string name="email_enter_hint">Unesite vašu e-poštu za nastavak</string>
|
||||
<string name="hide">Sakrij</string>
|
||||
<string name="new_sources_text">Dostupni su novi izvori mange</string>
|
||||
<string name="check_new_chapters_title">Provjeriti ima li novih poglavlja i obavijestiti o tome</string>
|
||||
<string name="show_notification_new_chapters_on">Primit ćete obavijesti o ažuriranjima mange koju čitate</string>
|
||||
<string name="show_notification_new_chapters_off">Nećete primati obavijesti, ali će nova poglavlja biti istaknuta na popisima</string>
|
||||
<string name="notifications_enable">Omogući obavijesti</string>
|
||||
<string name="name">Ime</string>
|
||||
<string name="edit">Uredi</string>
|
||||
<string name="edit_category">Uredi kategoriju</string>
|
||||
<string name="tracking">Pratim</string>
|
||||
<string name="bookmark_add">Dodaj zabilješku</string>
|
||||
<string name="bookmark_remove">Ukloni zabilješku</string>
|
||||
<string name="bookmark_added">Zabilješka dodana</string>
|
||||
<string name="removed_from_history">Uklonjeno iz povijesti</string>
|
||||
<string name="detect_reader_mode">Automatsko otkrivanje načina čitanja</string>
|
||||
<string name="disable_battery_optimization_summary">Pomaže pri provjerama ažuriranja u pozadini</string>
|
||||
<string name="crash_text">Nešto je pošlo po zlu. Pošaljite izvješće o pogrešci razvojnim programerima kako biste nam pomogli da je popravimo.</string>
|
||||
<string name="send">Pošalji</string>
|
||||
<string name="status_planned">Planirani</string>
|
||||
<string name="status_reading">Čitam</string>
|
||||
<string name="status_re_reading">Ponovo čitam</string>
|
||||
<string name="status_completed">Dovršeno</string>
|
||||
<string name="status_on_hold">Na čekanju</string>
|
||||
<string name="report">Prijavi</string>
|
||||
<string name="show_reading_indicators">Prikaži indikatore napretka tokom čitanja</string>
|
||||
<string name="data_deletion">Brisanje podataka</string>
|
||||
<string name="show_reading_indicators_summary">Prikaži postotak pročitanih u povijesti i favoritima</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Manga označena kao NSFW nikada neće biti dodana u povijest i vaš napredak neće biti spremljen</string>
|
||||
<string name="clear_cookies_summary">Može pomoći u slučaju nekih problema. Sva će ovlaštenja biti poništena</string>
|
||||
<string name="show_all">Prikaži sve</string>
|
||||
<string name="invalid_domain_message">Nevažeća domena</string>
|
||||
<string name="select_range">Odaberite raspon</string>
|
||||
<string name="clear_all_history">Izbriši svu povijest</string>
|
||||
<string name="last_2_hours">Zadnja 2 sata</string>
|
||||
<string name="history_cleared">Povijest izbrisana</string>
|
||||
<string name="manage">Upravljaj</string>
|
||||
<string name="no_bookmarks_yet">Još nema zabilješki</string>
|
||||
<string name="bookmarks_removed">Zabilješka izbrisana</string>
|
||||
<string name="no_manga_sources">Nema izvora mange</string>
|
||||
<string name="no_manga_sources_text">Omogućite izvore mange za čitanje mange na mreži</string>
|
||||
<string name="random">Nasumično</string>
|
||||
<string name="categories_delete_confirm">Jeste li sigurni da želite izbrisati odabrane omiljene kategorije?
|
||||
\nSve mange u njoj bit će izgubljene i to se ne može poništiti.</string>
|
||||
<string name="reader_info_pattern">Poglavlje %1$d/%2$d Stranica %3$d/%4$d</string>
|
||||
<string name="reader_info_bar">Prikaži informacijsku traku u čitaču</string>
|
||||
<string name="comics_archive">Arhiva stripova</string>
|
||||
<string name="folder_with_images">Mapa sa slikama</string>
|
||||
<string name="importing_manga">Uvoz mange</string>
|
||||
<string name="import_completed">Uvoz dovršen</string>
|
||||
<string name="import_completed_hint">Možete izbrisati izvornu datoteku iz pohrane radi uštede prostora</string>
|
||||
<string name="import_will_start_soon">Uvoz će uskoro početi</string>
|
||||
<string name="feed">Novosti</string>
|
||||
<string name="manga_error_description_pattern">Detalji pogreške:<br><tt>%1$s</tt><br><br>1. Pokušajte <a href=%2$s>otvoriti mangu u web-pregledniku</a> kako biste bili sigurni da je dostupna na izvoru<br>2. Provjerite koristite li <a href=kotatsu://about>najnoviju verziju Kotatsu</a><br>3. Ako je dostupno, pošaljite izvješće o pogrešci programerima.</string>
|
||||
<string name="history_shortcuts">Prikaži nedavne manga prečice</string>
|
||||
<string name="history_shortcuts_summary">Učinite nedavnu mangu dostupnom dugim pritiskom na ikonu aplikacije</string>
|
||||
<string name="reader_control_ltr_summary">Dodirom desnog ruba ili pritiskom desne tipke uvijek prelazi na sljedeću stranicu.</string>
|
||||
<string name="reader_control_ltr">Ergonomska kontrola čitača</string>
|
||||
<string name="color_correction">Korekcija boja</string>
|
||||
<string name="brightness">Svjetlina</string>
|
||||
<string name="contrast">Kontrast</string>
|
||||
<string name="reset">Resetiraj</string>
|
||||
<string name="text_unsaved_changes_prompt">Spremi ili odbaci nespremljene promjene?</string>
|
||||
<string name="discard">Odbaci</string>
|
||||
<string name="error_no_space_left">Nema više prostora na uređaju</string>
|
||||
<string name="reader_slider">Prikaži klizač za promjenu stranice</string>
|
||||
<string name="webtoon_zoom">Webtoon zumiranje</string>
|
||||
<string name="network_unavailable">Mreža nije dostupna</string>
|
||||
<string name="network_unavailable_hint">Uključite Wi-Fi ili mobilnu mrežu da biste čitali mangu online</string>
|
||||
<string name="server_error">Pogreška na strani poslužitelja (%1$d). Molimo pokušajte ponovo kasnije</string>
|
||||
<string name="clear_new_chapters_counters">Također očistite informacije o novim poglavljima</string>
|
||||
<string name="compact">Kompaktan</string>
|
||||
<string name="source_disabled">Izvor onemogućen</string>
|
||||
<string name="prefetch_content">Predučitavanje sadržaja</string>
|
||||
<string name="mark_as_current">Označi kao trenutno</string>
|
||||
<string name="language">Jezik</string>
|
||||
<string name="share_logs">Podijelite zapise</string>
|
||||
<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="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>
|
||||
<string name="theme_name_asuka">Asuka</string>
|
||||
<string name="theme_name_mion">Mion</string>
|
||||
<string name="theme_name_rikka">Rikka</string>
|
||||
<string name="theme_name_sakura">Sakura</string>
|
||||
<string name="theme_name_mamimi">Mamimi</string>
|
||||
<string name="services">Usluge</string>
|
||||
<string name="allow_unstable_updates">Dopusti nestabilna ažuriranja</string>
|
||||
<string name="allow_unstable_updates_summary">Primajte obavijesti o nestabilnim verzijama</string>
|
||||
<string name="download_started">Preuzimanje je počelo</string>
|
||||
<string name="got_it">Shvaćam</string>
|
||||
<string name="speed">Brzina</string>
|
||||
<string name="server_address">Adresa poslužitelja</string>
|
||||
<string name="sync_host_description">Možete koristiti poslužitelj za sinkronizaciju s vlastitim hostom ili zadanim. Nemojte ovo mijenjati ako niste sigurni što radite.</string>
|
||||
<string name="ignore_ssl_errors">Ignorirajte SSL greške</string>
|
||||
<string name="mirror_switching">Odaberite posluživač automatski</string>
|
||||
<string name="mirror_switching_summary">Automatski mijenjaj domene za izvore mange u slučaju grešaka ako su poslužitelji dostupni</string>
|
||||
<string name="pause">Pauza</string>
|
||||
<string name="resume">Nastavi</string>
|
||||
<string name="paused">Pauzirano</string>
|
||||
<string name="remove_completed">Ukloni dovršene</string>
|
||||
<string name="cancel_all">Otkaži sve</string>
|
||||
<string name="downloads_wifi_only">Preuzimanje samo putem Wi-Fi mreže</string>
|
||||
<string name="downloads_wifi_only_summary">Zaustavite preuzimanje kada se prebacite na mobilnu mrežu</string>
|
||||
<string name="suggestion_manga">Prijedlog: %s</string>
|
||||
<string name="suggestions_notifications_summary">Ponekad prikaži obavijesti s predloženom mangom</string>
|
||||
<string name="more">Više</string>
|
||||
<string name="enable">Omogući</string>
|
||||
<string name="no_thanks">Ne hvala</string>
|
||||
<string name="cancel_all_downloads_confirm">Sva aktivna preuzimanja bit će otkazana, djelomično preuzeti podaci bit će izgubljeni</string>
|
||||
<string name="remove_completed_downloads_confirm">Vaša povijest preuzimanja bit će trajno izbrisana</string>
|
||||
<string name="downloads_resumed">Preuzimanja su nastavljena</string>
|
||||
<string name="downloads_paused">Preuzimanja su pauzirana</string>
|
||||
<string name="downloads_removed">Preuzimanja su uklonjena</string>
|
||||
<string name="suggestions_enable_prompt">Želite li primati personalizirane prijedloge za mange?</string>
|
||||
<string name="web_view_unavailable">WebView nije dostupan: provjerite je li instaliran WebView provider</string>
|
||||
<string name="type">Tip</string>
|
||||
<string name="port">Port</string>
|
||||
<string name="proxy">Proxy</string>
|
||||
<string name="invalid_value_message">Nevažeća vrijednost</string>
|
||||
<string name="email_password_enter_hint">Unesite vašu e-poštu i lozinku za nastavak</string>
|
||||
<string name="invert_colors">Invertiraj boje</string>
|
||||
<string name="username">Korisničko ime</string>
|
||||
<string name="password">Lozinka</string>
|
||||
<string name="authorization_optional">Autorizacija (nije obavezno)</string>
|
||||
<string name="invalid_port_number">Nevažeći broj port-a</string>
|
||||
<string name="network">Mreža</string>
|
||||
<string name="data_and_privacy">Podaci i privatnost</string>
|
||||
<string name="restore_summary">Vratite prethodno stvorenu sigurnosnu kopiju</string>
|
||||
<string name="reader_info_bar_summary">Prikažite trenutno vrijeme i napredak čitanja na vrhu zaslona</string>
|
||||
<string name="show_pages_numbers_summary">Prikaži brojeve stranica u donjem kutu</string>
|
||||
<string name="clear_source_cookies_summary">Brisanje kolačića samo za određenu domenu. U većini slučajeva poništit će autorizaciju</string>
|
||||
<string name="download_option_whole_manga">Cijela manga</string>
|
||||
<string name="download_option_first_n_chapters">Prvi %s</string>
|
||||
<string name="description">Opis</string>
|
||||
<string name="tracker_wifi_only_summary">Ne provjeravajte nova poglavlja pomoću mrežnih veza s ograničenim protokom</string>
|
||||
<string name="search_hint">Unesite naslov mange, žanr ili naziv izvora</string>
|
||||
<string name="progress">Napredak</string>
|
||||
<string name="order_added">Dodano</string>
|
||||
<string name="show">Prikaži</string>
|
||||
<string name="captcha_required_summary">%s zahtijeva da se captcha riješi kako bi ispravno radio</string>
|
||||
<string name="to_top">Na vrh</string>
|
||||
<string name="zoom_out">Umanji</string>
|
||||
<string name="zoom_in">Povećaj</string>
|
||||
<string name="reader_zoom_buttons">Prikaži gumbe za zumiranje</string>
|
||||
<string name="reader_zoom_buttons_summary">Treba li prikazati gumbe za kontrolu zumiranja u donjem desnom kutu</string>
|
||||
<string name="keep_screen_on">Držite zaslon uključenim</string>
|
||||
<string name="keep_screen_on_summary">Ne isključujte ekran dok čitate mangu</string>
|
||||
<string name="state_abandoned">Izbačeno</string>
|
||||
<string name="enhanced_colors_summary">Smanjuje trake, ali može utjecati na performanse</string>
|
||||
<string name="enhanced_colors">32-bitni način rada u boji</string>
|
||||
<string name="suggest_new_sources">Predloži nove izvore nakon ažuriranja aplikacije</string>
|
||||
<string name="suggest_new_sources_summary">Upit za omogućavanje novododanih izvora nakon ažuriranja aplikacije</string>
|
||||
<string name="list_options">Popis opcija</string>
|
||||
<string name="by_relevance">Relevantnost</string>
|
||||
<string name="categories">Kategorije</string>
|
||||
<string name="online_variant">Online varijanta</string>
|
||||
<string name="periodic_backups">Periodične sigurnosne kopije</string>
|
||||
<string name="backup_frequency">Učestalost stvaranja sigurnosne kopije</string>
|
||||
<string name="frequency_every_day">Svaki dan</string>
|
||||
<string name="content_type_manga">Manga</string>
|
||||
<string name="content_type_hentai">Hentai</string>
|
||||
<string name="content_type_comics">Stripovi</string>
|
||||
<string name="content_type_other">Ostalo</string>
|
||||
<string name="sources_catalog">Katalog izvora</string>
|
||||
<string name="source_enabled">Izvor omogućen</string>
|
||||
<string name="no_manga_sources_catalog_text">U ovom odjeljku nema dostupnih izvora ili su svi možda već dodani.
|
||||
\nBudite u toku</string>
|
||||
<string name="no_manga_sources_found">Vašim upitom nisu pronađeni dostupni izvori mangi</string>
|
||||
<string name="catalog">Katalog</string>
|
||||
<string name="disable_nsfw_summary">Onemogućite NSFW izvore i sakrijte mangu za odrasle s popisa ako je moguće</string>
|
||||
<string name="state_paused">Pauzirano</string>
|
||||
<string name="reader_optimize">Smanjite potrošnju memorije (beta)</string>
|
||||
<string name="reader_optimize_summary">Smanjite kvalitetu stranica izvan zaslona kako biste koristili manje memorije</string>
|
||||
<string name="state">Stanje</string>
|
||||
<string name="error_multiple_genres_not_supported">Ovaj izvor mange ne podržava filtriranje prema više žanrova</string>
|
||||
<string name="apply">Primjeni</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Ovaj izvor ne podržava filtriranje po žanrovima i lokalitetima</string>
|
||||
<string name="error_filter_states_genre_not_supported">Ovaj izvor ne podržava filtriranje po žanrovima i stanjima</string>
|
||||
<string name="genres_search_hint">Počnite upisivati naziv žanra</string>
|
||||
<string name="backup_date_">Datum sigurnosne kopije: %s</string>
|
||||
<string name="state_upcoming">Nadolazeće</string>
|
||||
<string name="by_name_reverse">Ime obrnuto</string>
|
||||
<string name="content_rating">Ocjena sadržaja</string>
|
||||
<string name="rating_safe">Sigurno</string>
|
||||
<string name="rating_suggestive">Sugestivno</string>
|
||||
<string name="rating_adult">Za odrasle</string>
|
||||
<string name="default_tab">Zadana kartica</string>
|
||||
<string name="mark_as_completed">Označi kao dovršeno</string>
|
||||
<string name="mark_as_completed_prompt">Označiti odabranu mangu kao potpuno pročitanu?
|
||||
\n
|
||||
\nUpozorenje: trenutni napredak čitanja bit će izgubljen.</string>
|
||||
<string name="category_hidden_done">Ova je kategorija bila skrivena s glavnog zaslona i dostupna je kroz Meni → Upravljanje kategorijama</string>
|
||||
<string name="volume_">Svezak %d</string>
|
||||
<string name="volume_unknown">Nepoznati svezak</string>
|
||||
<string name="incognito_mode_hint">Vaš napredak u čitanju neće biti spremljen</string>
|
||||
<string name="vertical">Okomito</string>
|
||||
<string name="last_read">Zadnje pročitano</string>
|
||||
<string name="show_menu">Prikaži meni</string>
|
||||
<string name="toggle_ui">Prikaži/sakrij korisničko sučelje</string>
|
||||
<string name="prev_chapter">Prethodno poglavlje</string>
|
||||
<string name="next_chapter">Sljedeće poglavlje</string>
|
||||
<string name="next_page">Sljedeća stranica</string>
|
||||
<string name="default_webtoon_zoom_out">Zadani webtoon smanji</string>
|
||||
<string name="fullscreen_mode">Cijeli zaslon</string>
|
||||
<string name="reader_fullscreen_summary">Sakrij status sustava i navigacijske trake</string>
|
||||
<string name="reading_time_estimation">Prikaži procijenjeno vrijeme čitanja</string>
|
||||
<string name="reading_time_estimation_summary">Vrijednost procjene vremena može biti netočna</string>
|
||||
<string name="suggestions_unavailable_text">Značajka prijedloga je onemogućena</string>
|
||||
<string name="check_for_new_chapters_disabled">Provjera novih poglavlja je onemogućena</string>
|
||||
<string name="show_labels_in_navbar">Prikaži oznake u navigacijskoj traci</string>
|
||||
<string name="pages_saving">Spremanje stranica</string>
|
||||
<string name="ask_for_dest_dir_every_time">Svaki put pitajte za odredišni direktorij</string>
|
||||
<string name="default_page_save_dir">Zadani direktorij za spremanje stranice</string>
|
||||
<string name="remove_from_history">Ukloni iz povijesti</string>
|
||||
<string name="location">Lokacija</string>
|
||||
<string name="preferred_download_format">Preferirani format preuzimanja</string>
|
||||
<string name="automatic">Automatski</string>
|
||||
<string name="single_cbz_file">Jedna CBZ datoteka</string>
|
||||
<string name="multiple_cbz_files">Više CBZ datoteka</string>
|
||||
<string name="reading_stats">Statistika čitanja</string>
|
||||
<string name="other_manga">Druge mange</string>
|
||||
<string name="less_than_minute">Manje od minute</string>
|
||||
<string name="statistics">Statistika</string>
|
||||
<string name="stats_cleared">Statistika obrisana</string>
|
||||
<string name="clear_stats">Obriši statistiku</string>
|
||||
<string name="delete_read_chapters_summary">Izbrišite poglavlja koja ste već pročitali iz lokalne pohrane kako biste oslobodili prostor</string>
|
||||
<string name="delete_read_chapters_prompt">Ovo će trajno izbrisati sva poglavlja označena kao pročitana iz vaše lokalne pohrane. Kasnije ga možete ponovo preuzeti, ali uvezena poglavlja mogu biti zauvijek izgubljena</string>
|
||||
<string name="delete_read_chapters_auto">Automatski izbrišite pročitana poglavlja</string>
|
||||
<string name="runs_on_app_start">Pokreće se kada se aplikacija pokrene</string>
|
||||
<string name="split_by_translations">Podjeli po prijevodima</string>
|
||||
<string name="split_by_translations_summary">Pokažite poglavlja s različitim prijevodima odvojeno, umjesto na jednom popisu</string>
|
||||
<string name="order_oldest">Najstariji</string>
|
||||
<string name="long_ago_read">Davno pročitano</string>
|
||||
<string name="unread">Nepročitano</string>
|
||||
<string name="enable_source">Omogući izvor</string>
|
||||
<string name="unsupported_source">Ovaj izvor mange nije podržan</string>
|
||||
<string name="show_pages_thumbs">Prikaži sličice stranica</string>
|
||||
<string name="disable_connectivity_check">Onemogući provjeru povezivanja</string>
|
||||
<string name="ignore_ssl_errors_summary">Možete onemogućiti provjeru SSL certifikata u slučaju da se suočite s problemima povezanim sa SSL-om prilikom pristupa mrežnim resursima. To može utjecati na vašu sigurnost. Nakon promjene ove postavke potrebno je ponovno pokretanje aplikacije.</string>
|
||||
<string name="disable_connectivity_check_summary">Preskočite provjeru povezivanja u slučaju da imate problema s njom (npr. odlazak u izvanmrežni način rada iako je mreža povezana)</string>
|
||||
<string name="disable_nsfw_notifications">Onemogući NSFW obavijesti</string>
|
||||
<string name="disable_nsfw_notifications_summary">Ne prikazuj obavijesti o ažuriranjima NSFW mange</string>
|
||||
<string name="tracker_debug_info">Provjera zapisnika novih poglavlja</string>
|
||||
<string name="tracker_debug_info_summary">Informacije o otklanjanju pogrešaka o pozadinskim provjerama za nova poglavlja</string>
|
||||
<string name="_new">Novo</string>
|
||||
<string name="all_languages">Svi jezici</string>
|
||||
<string name="screenshots_block_incognito">Blokiraj u anonimnom načinu rada</string>
|
||||
<string name="processing_">Obrada…</string>
|
||||
<string name="download_complete">Preuzeto</string>
|
||||
<string name="by_name">Ime</string>
|
||||
<string name="newest">Najnoviji</string>
|
||||
<string name="sort_order">Redoslijed sortiranja</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="follow_system">Slijedite sustav</string>
|
||||
<string name="pages">Stranice</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="standard">Standard</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="remove_category">Ukloni</string>
|
||||
<string name="done">Gotovo</string>
|
||||
<string name="read_later">Pročitaj kasnije</string>
|
||||
<string name="size_s">Veličina: %s</string>
|
||||
<string name="feed_will_update_soon">Ažuriranje sažetka sadržaja počet će uskoro</string>
|
||||
<string name="track_sources">Potražite ažuriranja</string>
|
||||
<string name="clear_search_history">Obriši povijest pretraživanja</string>
|
||||
<string name="open_in_browser">Otvorite u web pregledniku</string>
|
||||
<string name="text_empty_holder_primary">Ovdje je nekako prazno…</string>
|
||||
<string name="updates">Ažuriranja</string>
|
||||
<string name="downloads">Preuzimanja</string>
|
||||
<string name="popular">Popularno</string>
|
||||
<string name="updated">Ažurirano</string>
|
||||
<string name="by_rating">Ocjena</string>
|
||||
<string name="save_page">Spremi stranicu</string>
|
||||
<string name="notifications">Obavijesti</string>
|
||||
<string name="update">Ažuriraj</string>
|
||||
<string name="history_is_empty">Još nema povijesti</string>
|
||||
<string name="clear_updates_feed">Očisti sažetak ažuriranja</string>
|
||||
<string name="updates_feed_cleared">Očišćeno</string>
|
||||
<string name="rotate_screen">Zakreni zaslon</string>
|
||||
<string name="preparing_">Priprema…</string>
|
||||
<string name="restore_backup">Vrati iz sigurnosne kopije</string>
|
||||
<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="yesterday">Jučer</string>
|
||||
<string name="long_ago">Davno</string>
|
||||
<string name="data_restored">Vraćeno</string>
|
||||
<string name="just_now">Upravo sada</string>
|
||||
<string name="screenshots_allow">Dopusti</string>
|
||||
<string name="screenshots_block_all">Uvijek blokiraj</string>
|
||||
<string name="suggestions">Prijedlozi</string>
|
||||
<string name="chapters_empty">Nema poglavlja u ovoj mangi</string>
|
||||
<string name="appearance">Izgled</string>
|
||||
<string name="suggestions_updating">Ažuriranje prijedloga</string>
|
||||
<string name="suggestions_excluded_genres_summary">Navedite žanrove koje ne želite vidjeti u prijedlozima</string>
|
||||
<string name="download_slowdown_summary">Pomaže u izbjegavanju blokiranja vaše IP adrese</string>
|
||||
<string name="local_manga_processing">Obrada spremljene mange</string>
|
||||
<string name="bookmarks">Zabilješke</string>
|
||||
<string name="bookmark_removed">Zabilješka uklonjena</string>
|
||||
<string name="dns_over_https">DNS preko HTTPS-a</string>
|
||||
<string name="disable_battery_optimization">Onemogući optimizaciju baterije</string>
|
||||
<string name="status_dropped">Izbačeno</string>
|
||||
<string name="appwidget_shelf_description">Manga iz vaših favorita</string>
|
||||
<string name="no_bookmarks_summary">Možete stvoriti zabilješku dok čitate mangu</string>
|
||||
<string name="empty">Prazno</string>
|
||||
<string name="exit_confirmation_summary">Dvaput pritisnite Natrag za izlaz iz aplikacije</string>
|
||||
<string name="exit_confirmation">Potvrda izlaza</string>
|
||||
<string name="storage_usage">Korištenje pohrane</string>
|
||||
<string name="text_clear_search_history_prompt">Želite li trajno ukloniti sve nedavne upite za pretraživanje?</string>
|
||||
<string name="welcome">Dobrodošli</string>
|
||||
<string name="backup_saved">Sigurnosna kopija spremljena</string>
|
||||
<string name="enabled">Omogućeno</string>
|
||||
<string name="preload_pages">Prethodno učitavanje stranica</string>
|
||||
<string name="empty_favourite_categories">Nema omiljenih kategorija</string>
|
||||
<string name="undo">Poništi</string>
|
||||
<string name="disable_all">Onemogući sve</string>
|
||||
<string name="use_fingerprint">Koristite otisak prsta ako je dostupan</string>
|
||||
<string name="appwidget_recent_description">Vaša nedavno pročitana manga</string>
|
||||
<string name="explore">Istraži</string>
|
||||
<string name="confirm_exit">Ponovno pritisnite Natrag za izlaz</string>
|
||||
<string name="saved_manga">Spremljena manga</string>
|
||||
<string name="pages_cache">Predmemorija stranica</string>
|
||||
<string name="other_cache">Drugi cache</string>
|
||||
<string name="available">Dostupno</string>
|
||||
<string name="disabled">Onemogućeno</string>
|
||||
<string name="default_mode">Zadani način rada</string>
|
||||
<string name="detect_reader_mode_summary">Automatski otkrij je li manga webtoon</string>
|
||||
<string name="removed_from_favourites">Uklonjeno iz favorita</string>
|
||||
<string name="not_found_404">Sadržaj nije pronađen ili uklonjen</string>
|
||||
<string name="no_chapters">Nema poglavlja</string>
|
||||
<string name="options">Opcije</string>
|
||||
<string name="download_slowdown">Usporavanje preuzimanja</string>
|
||||
<string name="logout">Odjavite se</string>
|
||||
<string name="reorder">Rasporedi</string>
|
||||
<string name="incognito_mode">Anonimni način rada</string>
|
||||
<string name="suggestions_excluded_genres">Ispostavite žanrove</string>
|
||||
<string name="text_delete_local_manga_batch">Trajno izbrisati odabrane stavke s uređaja?</string>
|
||||
<string name="removal_completed">Uklanjanje dovršeno</string>
|
||||
<string name="automatic_scroll">Automatsko pomicanje</string>
|
||||
<string name="screenshots_block_nsfw">Blokiraj na NSFW-u</string>
|
||||
<string name="suggestions_enable">Omogući prijedloge</string>
|
||||
<string name="suggestions_summary">Predložiti mangu na temelju vaših preferencija</string>
|
||||
<string name="theme_name_kanade">Kanade</string>
|
||||
<string name="find_similar">Pronađite slično</string>
|
||||
<string name="scrobbling_empty_hint">Za praćenje napretka čitanja odaberite Meni → Prati na zaslonu s detaljima mange.</string>
|
||||
<string name="sources_reorder_tip">Dodirnite i držite stavku da biste im promijenili redoslijed</string>
|
||||
<string name="settings_apply_restart_required">Ponovno pokrenite aplikaciju kako biste primijenili ove promjene</string>
|
||||
<string name="folder_with_images_import_description">Možete odabrati direktorij s arhivama ili slikama. Svaka arhiva (ili poddirektorij) bit će prepoznata kao poglavlje.</string>
|
||||
<string name="sync_settings">Postavke sinkronizacije</string>
|
||||
<string name="nothing_here">Ovdje nema ničega</string>
|
||||
<string name="user_agent">Zaglavlje UserAgent-a</string>
|
||||
<string name="comics_archive_import_description">Možete odabrati jednu ili više .cbz ili .zip datoteka, svaka će datoteka biti prepoznata kao zasebna manga.</string>
|
||||
<string name="show_on_shelf">Prikaži na polici</string>
|
||||
<string name="sync_auth_hint">Možete se prijaviti na postojeći račun ili stvoriti novi</string>
|
||||
<string name="text_downloads_list_holder">Nemate preuzimanja</string>
|
||||
<string name="downloads_cancelled">Preuzimanja su otkazana</string>
|
||||
<string name="clear_network_cache">Očisti mrežnu predmemoriju</string>
|
||||
<string name="address">Adresa</string>
|
||||
<string name="images_proxy_title">Proxy za optimizaciju slika</string>
|
||||
<string name="webtoon_zoom_summary">Dopusti gestu zumiranja u načinu webtoona</string>
|
||||
<string name="download_option_all_chapters">Sva poglavlja s prijevodom %s</string>
|
||||
<string name="this_month">Ovaj mjesec</string>
|
||||
<string name="color_light">Svjetla</string>
|
||||
<string name="color_white">Bijela</string>
|
||||
<string name="background">Pozadina</string>
|
||||
<string name="data_not_restored_text">Provjerite jeste li odabrali ispravnu datoteku sigurnosne kopije</string>
|
||||
<string name="manage_categories">Upravljanje kategorijama</string>
|
||||
<string name="suggestions_wifi_only_summary">Nemojte ažurirati prijedloge pomoću mrežnih veza s ograničenim protokom</string>
|
||||
<string name="languages">Jezici</string>
|
||||
<string name="directories">Direktorij</string>
|
||||
<string name="main_screen_sections">Odjeljci glavnog zaslona</string>
|
||||
<string name="items_limit_exceeded">Nije moguće dodati više stavki</string>
|
||||
<string name="moved_to_top">Premješteno na vrh</string>
|
||||
<string name="frequency_every_2_days">Svaka 2 dana</string>
|
||||
<string name="manual">Ručno</string>
|
||||
<string name="error_search_not_supported">Ovaj izvor mange ne podržava pretraživanje</string>
|
||||
<string name="skip">Preskoči</string>
|
||||
<string name="grayscale">Sivi tonovi</string>
|
||||
<string name="globally">Globalno</string>
|
||||
<string name="this_manga">Ova manga</string>
|
||||
<string name="color_correction_apply_text">Ove postavke mogu se primijeniti globalno ili samo na trenutnu mangu. Ako se primjenjuju globalno, pojedinačne postavke neće biti zamjenjene.</string>
|
||||
<string name="welcome_text">Odaberite koje izvore sadržaja želite omogućiti. To se također može konfigurirati kasnije u postavkama</string>
|
||||
<string name="sync_auth">Prijavite se za sinkronizaciju računa</string>
|
||||
<string name="prev_page">Prethodna stranica</string>
|
||||
<string name="reader_actions">Radnje čitatelja</string>
|
||||
<string name="reader_actions_summary">Konfigurirajte radnje za dodirna područja zaslona</string>
|
||||
<string name="switch_pages_volume_buttons_summary">Koristite tipke za glasnoću za prebacivanje stranica</string>
|
||||
<string name="tap_action">Radnja pri dodiru</string>
|
||||
<string name="long_tap_action">Radnja dugog dodira</string>
|
||||
<string name="use_two_pages_landscape">Koristi izgled dvije stranice u pejzažnoj orijentaciji (beta)</string>
|
||||
<string name="download_option_next_unread_n_chapters">Sljedeći nepročitani %s</string>
|
||||
<string name="download_option_all_unread">Sva nepročitana poglavlja</string>
|
||||
<string name="download_option_all_unread_b">Sva nepročitana poglavlja (%s)</string>
|
||||
<string name="download_option_manual_selection">Odaberite poglavlja ručno</string>
|
||||
<string name="pick_custom_directory">Odaberite prilagođeni direktorij</string>
|
||||
<string name="no_access_to_file">Nemate pristup ovoj datoteci ili direktoriju</string>
|
||||
<string name="local_manga_directories">Lokalni direktoriji mange</string>
|
||||
<string name="voice_search">Glasovno pretraživanje</string>
|
||||
<string name="related_manga">Povezana manga</string>
|
||||
<string name="color_dark">Tamna</string>
|
||||
<string name="color_black">Crna</string>
|
||||
<string name="data_not_restored">Podaci nisu vraćeni</string>
|
||||
<string name="on_device">Na uređaju</string>
|
||||
<string name="frequency_once_per_week">Jednom tjedno</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="manage_sources">Upravljanje izvorima</string>
|
||||
<string name="error_multiple_states_not_supported">Ovaj izvor mange ne podržava filtriranje prema višestrukim stanjima</string>
|
||||
<string name="downloads_settings_info">Možete omogućiti usporavanje preuzimanja za svaki izvor mange zasebno u postavkama izvora ako imate problema s blokiranjem na strani poslužitelja</string>
|
||||
<string name="restore">Vrati</string>
|
||||
<string name="switch_pages_volume_buttons">Omogući tipke za glasnoću</string>
|
||||
<string name="none">Nijedan</string>
|
||||
<string name="config_reset_confirm">Vratiti postavke na zadane vrijednosti? Ova se radnja ne može poništiti.</string>
|
||||
<string name="clear_stats_confirm">Želite li stvarno izbrisati sve statistike čitanja? Ova se radnja ne može poništiti.</string>
|
||||
<string name="month">Mjesec</string>
|
||||
<string name="pages_read_s">Pročitane stranice: %s</string>
|
||||
<string name="no_chapters_deleted">Nijedno poglavlje nije izbrisano</string>
|
||||
<string name="downloaded">Preuzeto</string>
|
||||
<string name="images_procy_description">Koristite uslugu wsrv.nl kako biste smanjili promet i ubrzali učitavanje slika ako je moguće</string>
|
||||
<string name="unknown">Nepoznato</string>
|
||||
<string name="too_many_requests_message">Previše zahtjeva. Pokušajte ponovno kasnije</string>
|
||||
<string name="manga_list">Popis mangi</string>
|
||||
<string name="error_corrupted_file">Vraćeni su nevažeći podaci ili je datoteka oštećena</string>
|
||||
<string name="frequency_twice_per_month">Dva puta mjesečno</string>
|
||||
<string name="frequency_once_per_month">Jednom mjesečno</string>
|
||||
<string name="available_d">Dostupno: %1$d</string>
|
||||
<string name="genres_exclude">Isključi žanrove</string>
|
||||
<string name="week">Tjedan</string>
|
||||
<string name="day">Dan</string>
|
||||
<string name="alternatives">Alternative</string>
|
||||
<string name="periodic_backups_enable">Omogućite povremene sigurnosne kopije</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Moglo bi vam pomoći pri započinjanju preuzimanja ako imate problema s njim</string>
|
||||
<string name="all_time">Cijelo vrijeme</string>
|
||||
<string name="manga_migration">Migracija mange</string>
|
||||
<string name="migration_completed">Migracija dovršena</string>
|
||||
<string name="in_progress">U toku</string>
|
||||
<string name="disable_nsfw">Onemogući NSFW</string>
|
||||
<string name="related_manga_summary">Prikaži popis povezanih mangi. U nekim slučajevima može biti netočan ili nedostajati</string>
|
||||
<string name="advanced">Napredno</string>
|
||||
<string name="backups_output_directory">Izlazni direktorij sigurnosne kopije</string>
|
||||
<string name="three_months">Tri mjeseca</string>
|
||||
<string name="empty_stats_text">Nema statistike za odabrano razdoblje</string>
|
||||
<string name="last_successful_backup">Zadnja uspješna sigurnosna kopija: %s</string>
|
||||
<string name="migrate">Migrirati</string>
|
||||
<string name="chapters_deleted_pattern">Uklonjeno %1$s, izbrisano %2$s</string>
|
||||
<string name="lock_screen_rotation">Zaključavanje zakretanja zaslona</string>
|
||||
<string name="migrate_confirmation">Manga \"%1$s\" iz \"%2$s\" bit će zamijenjena s \"%3$s\" iz \"%4$s\" u vašoj povijesti i favoritima (ako postoje)</string>
|
||||
<string name="delete_read_chapters">Brisanje pročitanih poglavlja</string>
|
||||
</resources>
|
||||
@@ -581,4 +581,64 @@
|
||||
<string name="reader_fullscreen_summary">Sembunyikan status sistem dan bilah navigasi</string>
|
||||
<string name="automatic">Otomatis</string>
|
||||
<string name="default_webtoon_zoom_out">Zoom webtoon default</string>
|
||||
<string name="last_used">Terakhir digunakan</string>
|
||||
<string name="_new">Terbaru</string>
|
||||
<string name="hours_short">%d j</string>
|
||||
<string name="minutes_short">%d m</string>
|
||||
<string name="hours_minutes_short">%1$d j %2$d m</string>
|
||||
<string name="default_page_save_dir">Direktori penyimpanan halaman default</string>
|
||||
<string name="clear_stats">Bersihkan statistik</string>
|
||||
<string name="delete_read_chapters_prompt">Hal ini akan menghapus secara permanen semua bab yang ditandai sebagai telah dibaca dari penyimpanan lokal Anda. Anda dapat mengunduh ulang nanti, tetapi bab yang diimpor mungkin akan hilang selamanya</string>
|
||||
<string name="reading_stats">Statistik membaca</string>
|
||||
<string name="other_manga">Manga lainnya</string>
|
||||
<string name="show_pages_thumbs">Menampilkan gambar mini halaman</string>
|
||||
<string name="show_updated">Tampilkan yang diperbarui</string>
|
||||
<string name="ignore_ssl_errors_summary">Anda dapat menonaktifkan verifikasi sertifikat SSL jika Anda menghadapi masalah terkait SSL saat mengakses sumber daya jaringan. Hal ini dapat memengaruhi keamanan Anda. Diperlukan pengaktifan ulang aplikasi setelah mengubah pengaturan ini.</string>
|
||||
<string name="multiple_cbz_files">Beberapa file CBZ</string>
|
||||
<string name="pages_read_s">Halaman yang dibaca: %s</string>
|
||||
<string name="migrate_confirmation">Manga \"%1$s\" dari \"%2$s\" akan diganti dengan \"%3$s\" dari \"%4$s\" di riwayat dan favorit Anda (jika ada)</string>
|
||||
<string name="unread">Belum dibaca</string>
|
||||
<string name="show_pages_thumbs_summary">Aktifkan tab \"Halaman\" pada layar detail</string>
|
||||
<string name="unsupported_backup_message">Pilih file cadangan Kotatsu yang tepat</string>
|
||||
<string name="fix">Memperbaiki</string>
|
||||
<string name="missing_storage_permission">Tidak ada izin untuk mengakses manga di penyimpanan eksternal</string>
|
||||
<string name="disable_nsfw_notifications_summary">Jangan tampilkan pemberitahuan tentang pembaruan manga NSFW</string>
|
||||
<string name="search_suggestions">Saran pencarian</string>
|
||||
<string name="recent_queries">Pertanyaan terbaru</string>
|
||||
<string name="authors">Penulis</string>
|
||||
<string name="single_cbz_file">File CBZ tunggal</string>
|
||||
<string name="empty_stats_text">Tidak ada statistik untuk periode yang dipilih</string>
|
||||
<string name="preferred_download_format">Format unduhan yang disukai</string>
|
||||
<string name="migration_completed">Migrasi selesai</string>
|
||||
<string name="delete_read_chapters_summary">Menghapus bab yang telah Anda baca dari penyimpanan lokal untuk mengosongkan ruang</string>
|
||||
<string name="migrate">Migrasi</string>
|
||||
<string name="manga_migration">Manga migration</string>
|
||||
<string name="delete_read_chapters_auto">Menghapus bab yang sudah dibaca secara otomatis</string>
|
||||
<string name="runs_on_app_start">Berjalan saat aplikasi dimulai</string>
|
||||
<string name="delete_read_chapters">Menghapus bab yang sudah dibaca</string>
|
||||
<string name="no_chapters_deleted">Tidak ada bab yang telah dihapus</string>
|
||||
<string name="split_by_translations">Dibagi berdasarkan terjemahan</string>
|
||||
<string name="split_by_translations_summary">Menampilkan bab dengan terjemahan yang berbeda secara terpisah, bukan dalam satu daftar</string>
|
||||
<string name="order_oldest">Terlama</string>
|
||||
<string name="long_ago_read">Dibaca lama sekali</string>
|
||||
<string name="webtoon_gaps">Celah dalam mode webtoon</string>
|
||||
<string name="more_frequently">Lebih sering</string>
|
||||
<string name="webtoon_gaps_summary">Menampilkan celah vertikal di antara halaman dalam mode webtoon</string>
|
||||
<string name="frequency_of_check">Frekuensi pemeriksaan</string>
|
||||
<string name="pin_navigation_ui">Pin UI navigasi</string>
|
||||
<string name="tracker_debug_info">Memeriksa log bab baru</string>
|
||||
<string name="disable_connectivity_check">Menonaktifkan pemeriksaan konektivitas</string>
|
||||
<string name="disable_connectivity_check_summary">Lewati pemeriksaan konektivitas jika Anda mengalami masalah dengan konektivitas (misalnya, masuk ke mode offline meskipun jaringan tersambung)</string>
|
||||
<string name="disable_nsfw_notifications">Menonaktifkan notifikasi NSFW</string>
|
||||
<string name="tracker_debug_info_summary">Informasi debug tentang pemeriksaan latar belakang untuk bab baru</string>
|
||||
<string name="all_languages">Semua bahasa</string>
|
||||
<string name="screenshots_block_incognito">Blokir saat mode penyamaran</string>
|
||||
<string name="error_no_data_received">Tidak ada data yang diterima dari server</string>
|
||||
<string name="disable">Nonaktifkan</string>
|
||||
<string name="sources_disabled">Sumber dinonaktifkan</string>
|
||||
<string name="enable_source">Aktifkan sumber</string>
|
||||
<string name="unsupported_source">Sumber manga ini tidak didukung</string>
|
||||
<string name="blocked_by_server_message">Anda diblokir oleh server. Coba gunakan koneksi jaringan yang berbeda (VPN, Proxy, dll.)</string>
|
||||
<string name="less_frequently">Lebih jarang</string>
|
||||
<string name="pin_navigation_ui_summary">Jangan sembunyikan bilah navigasi dan tampilan pencarian saat menggulir</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,7 @@
|
||||
<string name="disable_nsfw_notifications_summary">Nie pokazuj powiadomień o aktualizacjach mangi NSFW</string>
|
||||
<string name="tracker_debug_info_summary">Debuguj informacje o sprawdzaniu dostępności nowych rozdziałów</string>
|
||||
<string name="tracker_debug_info">Dziennik sprawdzania nowych rozdziałów</string>
|
||||
<string name="_new">Nowy</string>
|
||||
<string name="screenshots_block_incognito">Blokuj w trybie incognito</string>
|
||||
<string name="all_languages">Wszystkie języki</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,5 @@
|
||||
<string name="disable_nsfw_notifications_summary">Не показывать уведомления об обновлениях манги NSFW</string>
|
||||
<string name="disable">Откл.</string>
|
||||
<string name="sources_disabled">Источники отключены</string>
|
||||
<string name="_new">Новое</string>
|
||||
</resources>
|
||||
@@ -243,7 +243,7 @@
|
||||
<string name="invalid_value_message">Погрешна вредност</string>
|
||||
<string name="downloads_cancelled">Преузимања су отказана</string>
|
||||
<string name="webtoon_zoom">Webtoon увећање</string>
|
||||
<string name="various_languages">Сви језици</string>
|
||||
<string name="various_languages">Разни језици</string>
|
||||
<string name="removal_completed">Уклањање је завршено</string>
|
||||
<string name="theme_name_miku">Мику</string>
|
||||
<string name="edit">Уреди</string>
|
||||
@@ -637,4 +637,11 @@
|
||||
<string name="disable_connectivity_check">Онемогућите проверу везе</string>
|
||||
<string name="disable_nsfw_notifications">Онемогућите НСФВ обавештења</string>
|
||||
<string name="disable_nsfw_notifications_summary">Не приказуј обавештења за ажурирања НСФВ манге</string>
|
||||
<string name="ignore_ssl_errors_summary">Можеш да онемогућиш верификацију ССЛ сертификата у случају да се суочиш са проблемима везаним за ССЛ када приступаш мрежним ресурсима. Ово може утицати на твоју безбедност. Након промене овог подешавања потребно је поновно покретање апликације.</string>
|
||||
<string name="disable_connectivity_check_summary">Прескочи проверу повезивања у случају да имаш проблема са њом (нпр. прелазак у режим ван мреже иако је мрежа повезана)</string>
|
||||
<string name="tracker_debug_info">Провера дневника нових поглавља</string>
|
||||
<string name="tracker_debug_info_summary">Информације о отклањању грешака о позадинским проверама за нова поглавља</string>
|
||||
<string name="_new">Нови</string>
|
||||
<string name="all_languages">Сви језици</string>
|
||||
<string name="screenshots_block_incognito">Блокирај у режиму без архивирања</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,7 @@
|
||||
<string name="disable_nsfw_notifications_summary">Uygunsuz manga güncellemeleri hakkında bildirim gösterilmesin</string>
|
||||
<string name="tracker_debug_info">Yeni bölümler günlüğü denetleniyor</string>
|
||||
<string name="tracker_debug_info_summary">Yeni bölümler için arka plan denetimleri hakkında hata ayıklama bilgileri</string>
|
||||
<string name="_new">Yeni</string>
|
||||
<string name="all_languages">Tüm diller</string>
|
||||
<string name="screenshots_block_incognito">Gizli moddayken engelle</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,5 @@
|
||||
<string name="disable_nsfw_notifications_summary">Не відображати повідомлення про оновлення манґи NSFW</string>
|
||||
<string name="disable">Вимкнути</string>
|
||||
<string name="sources_disabled">Джерела вимкнено</string>
|
||||
<string name="_new">Нове</string>
|
||||
</resources>
|
||||
@@ -640,5 +640,8 @@
|
||||
<string name="disable_nsfw_notifications">关闭成人内容提醒</string>
|
||||
<string name="disable_nsfw_notifications_summary">不显示成人漫画的更新提醒</string>
|
||||
<string name="tracker_debug_info">漫画更新日志</string>
|
||||
<string name="tracker_debug_info_summary">记录后台漫画更新时的调试信息</string>
|
||||
<string name="tracker_debug_info_summary">记录漫画后台更新时的调试日志</string>
|
||||
<string name="_new">最新</string>
|
||||
<string name="screenshots_block_incognito">开启无痕模式时禁止</string>
|
||||
<string name="all_languages">所有语言</string>
|
||||
</resources>
|
||||
@@ -23,6 +23,7 @@
|
||||
<string-array name="screenshots_policy" translatable="false">
|
||||
<item>@string/screenshots_allow</item>
|
||||
<item>@string/screenshots_block_nsfw</item>
|
||||
<item>@string/screenshots_block_incognito</item>
|
||||
<item>@string/screenshots_block_all</item>
|
||||
</string-array>
|
||||
<string-array name="network_policy" translatable="false">
|
||||
|
||||
@@ -30,11 +30,6 @@
|
||||
<string-array name="values_track_sources_default" translatable="false">
|
||||
<item>favourites</item>
|
||||
</string-array>
|
||||
<string-array name="values_screenshots_policy" translatable="false">
|
||||
<item>allow</item>
|
||||
<item>block_nsfw</item>
|
||||
<item>block_all</item>
|
||||
</string-array>
|
||||
<string-array name="values_network_policy" translatable="false">
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<dimen name="toolbar_button_margin">10dp</dimen>
|
||||
<dimen name="widget_cover_height">116dp</dimen>
|
||||
<dimen name="widget_cover_width">84dp</dimen>
|
||||
<dimen name="reader_bar_inset_fallback">8dp</dimen>
|
||||
<dimen name="scrobbling_list_spacing">12dp</dimen>
|
||||
<dimen name="explore_grid_width">120dp</dimen>
|
||||
<dimen name="chapter_grid_width">80dp</dimen>
|
||||
|
||||
@@ -650,4 +650,8 @@
|
||||
<string name="disable_nsfw_notifications_summary">Do not show notifications about NSFW manga updates</string>
|
||||
<string name="tracker_debug_info">Checking for new chapters log</string>
|
||||
<string name="tracker_debug_info_summary">Debug information about background checks for new chapters</string>
|
||||
<!-- In plural, used for filter -->
|
||||
<string name="_new">New</string>
|
||||
<string name="all_languages">All languages</string>
|
||||
<string name="screenshots_block_incognito">Block when incognito mode</string>
|
||||
</resources>
|
||||
|
||||
@@ -125,14 +125,6 @@
|
||||
android:title="@string/keep_screen_on"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="allow"
|
||||
android:entries="@array/screenshots_policy"
|
||||
android:entryValues="@array/values_screenshots_policy"
|
||||
android:key="screenshots_policy"
|
||||
android:title="@string/screenshots_policy"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="2"
|
||||
android:entries="@array/network_policy"
|
||||
|
||||
@@ -31,10 +31,4 @@
|
||||
android:summary="@string/disable_nsfw_summary"
|
||||
android:title="@string/disable_nsfw" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="sources_new"
|
||||
android:summary="@string/suggest_new_sources_summary"
|
||||
android:title="@string/suggest_new_sources" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
android:summary="@string/protect_application_summary"
|
||||
android:title="@string/protect_application" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="allow"
|
||||
android:entries="@array/screenshots_policy"
|
||||
android:key="screenshots_policy"
|
||||
android:title="@string/screenshots_policy"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="history_exclude_nsfw"
|
||||
android:summary="@string/exclude_nsfw_from_history_summary"
|
||||
|
||||
Reference in New Issue
Block a user