Compare commits

..

21 Commits
v7.1.2 ... v7.2

Author SHA1 Message Date
Koitharu
4502ffb6d2 Update parsers 2024-06-07 09:06:59 +03:00
maryush
b6f9ce824e Translated using Weblate (Polish)
Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
gallegonovato
d33081c1c7 Translated using Weblate (Spanish)
Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Eduardo Malaspina
76c08535d6 Translated using Weblate (Spanish)
Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Макар Разин
b55fef67e1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Scrambled777
56798677d5 Translated using Weblate (Hindi)
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
gekka
ff30b9c225 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Oğuz Ersen
5c3293ec44 Translated using Weblate (Turkish)
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Anon
1b17605e0e Translated using Weblate (Serbian)
Currently translated at 99.8% (645 of 646 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (645 of 645 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Naga
ba4e4dcf56 Translated using Weblate (French)
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (French)

Currently translated at 99.6% (643 of 645 strings)

Co-authored-by: Naga <yz2000.pro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Infy's Tagalog Translations
b35d5d4779 Translated using Weblate (Filipino)
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (645 of 645 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-06-07 08:27:24 +03:00
Koitharu
124f31ebe1 Global screenshot policy #920 #138 2024-06-06 11:09:44 +03:00
Koitharu
173087ee19 Sources catalog improvements 2024-06-06 11:09:44 +03:00
Koitharu
8d7bad97de Merge pull request #917 from galpt/devel 2024-06-03 17:27:12 +03:00
nya~
188fbfbb95 0ms DNS Large variant
To prevent users from getting rate limited while still receiving the benefits of OISD Big and other security filters, using the "Large" variant is highly recommended for mission-critical applications or big networks.

Recommended to always use DNS-over-HTTPS since Plain is not safe.
2024-06-02 12:15:07 +07:00
nya~
3498a54bdf Change 0ms DNS to Large variant
To prevent users from getting rate limited while still receiving the benefits of OISD Big and other security filters, using the "Large" variant is highly recommended for mission-critical applications or big networks.
2024-06-02 12:11:40 +07:00
Koitharu
18169c2355 Update sources catalog and repository 2024-06-01 17:13:27 +03:00
Koitharu
87beb9442f Respect rounded corners in reader bar #900 2024-06-01 13:09:45 +03:00
Koitharu
e642d54929 Reapply "Update sources catalog ui"
This reverts commit 8d5bde6e60.
2024-06-01 11:55:52 +03:00
Koitharu
59ce5d5e67 Skip hidden files on local storage #910 2024-06-01 08:55:21 +03:00
Koitharu
58d5237692 Update dependencies 2024-06-01 08:50:56 +03:00
77 changed files with 643 additions and 668 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 645 versionCode = 648
versionName = '7.1.2' versionName = '7.2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:26be293f24') { implementation('com.github.KotatsuApp:kotatsu-parsers:56fd22b43f') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -90,15 +90,15 @@ dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' 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.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.0' implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'androidx.fragment:fragment-ktx:1.7.1' implementation 'androidx.fragment:fragment-ktx:1.7.1'
implementation 'androidx.transition:transition-ktx:1.5.0' implementation 'androidx.transition:transition-ktx:1.5.0'
implementation 'androidx.collection:collection-ktx:1.4.0' implementation 'androidx.collection:collection-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.1'
implementation 'androidx.lifecycle:lifecycle-service:2.8.0' implementation 'androidx.lifecycle:lifecycle-service:2.8.1'
implementation 'androidx.lifecycle:lifecycle-process:2.8.0' implementation 'androidx.lifecycle:lifecycle-process:2.8.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
@@ -106,7 +106,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.12.0' 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.1'
implementation 'androidx.webkit:webkit:1.11.0' implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.work:work-runtime:2.9.0' implementation 'androidx.work:work-runtime:2.9.0'

View File

@@ -69,6 +69,7 @@ class MigrateUseCase @Inject constructor(
lastCheckTime = System.currentTimeMillis(), lastCheckTime = System.currentTimeMillis(),
lastChapterDate = lastChapter?.uploadDate ?: 0L, lastChapterDate = lastChapter?.uploadDate ?: 0L,
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
lastError = null,
) )
tracksDao.delete(oldDetails.id) tracksDao.delete(oldDetails.id)
tracksDao.upsert(newTrack) tracksDao.upsert(newTrack)

View File

@@ -48,6 +48,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper 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.parsers.MangaLoaderContext
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver import org.koitharu.kotatsu.settings.backup.BackupObserver
@@ -152,10 +153,12 @@ interface AppModule {
appProtectHelper: AppProtectHelper, appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle, activityRecreationHandle: ActivityRecreationHandle,
acraScreenLogger: AcraScreenLogger, acraScreenLogger: AcraScreenLogger,
screenshotPolicyHelper: ScreenshotPolicyHelper,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf( ): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper, appProtectHelper,
activityRecreationHandle, activityRecreationHandle,
acraScreenLogger, acraScreenLogger,
screenshotPolicyHelper,
) )
@Provides @Provides

View File

@@ -84,6 +84,7 @@ class JsonDeserializer(private val json: JSONObject) {
source = json.getString("source"), source = json.getString("source"),
isEnabled = json.getBoolean("enabled"), isEnabled = json.getBoolean("enabled"),
sortKey = json.getInt("sort_key"), sortKey = json.getInt("sort_key"),
addedIn = json.getIntOrDefault("added_in", 0),
) )
fun toMap(): Map<String, Any?> { fun toMap(): Map<String, Any?> {

View File

@@ -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.Migration18To19
import org.koitharu.kotatsu.core.db.migrations.Migration19To20 import org.koitharu.kotatsu.core.db.migrations.Migration19To20
import org.koitharu.kotatsu.core.db.migrations.Migration1To2 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.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5 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.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 20 const val DATABASE_VERSION = 21
@Database( @Database(
entities = [ entities = [
@@ -118,6 +119,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration17To18(), Migration17To18(),
Migration18To19(), Migration18To19(),
Migration19To20(), Migration19To20(),
Migration20To21(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -11,6 +11,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.explore.data.SourcesSortOrder import org.koitharu.kotatsu.explore.data.SourcesSortOrder
@@ -23,6 +24,9 @@ abstract class MangaSourcesDao {
@Query("SELECT source FROM sources WHERE enabled = 1") @Query("SELECT source FROM sources WHERE enabled = 1")
abstract suspend fun findAllEnabledNames(): List<String> 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") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>> abstract fun observeAll(): Flow<List<MangaSourceEntity>>
@@ -68,6 +72,7 @@ abstract class MangaSourcesDao {
source = source, source = source,
isEnabled = isEnabled, isEnabled = isEnabled,
sortKey = getMaxSortKey() + 1, sortKey = getMaxSortKey() + 1,
addedIn = BuildConfig.VERSION_CODE,
) )
upsert(entity) upsert(entity)
} }

View File

@@ -14,4 +14,5 @@ data class MangaSourceEntity(
val source: String, val source: String,
@ColumnInfo(name = "enabled") val isEnabled: Boolean, @ColumnInfo(name = "enabled") val isEnabled: Boolean,
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int, @ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
@ColumnInfo(name = "added_in") val addedIn: Int,
) )

View File

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

View File

@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.toLocale import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource 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 import com.google.android.material.R as materialR
fun MangaSource(name: String): MangaSource { fun MangaSource(name: String): MangaSource {
@@ -39,7 +37,7 @@ val ContentType.titleResId
fun MangaSource.getSummary(context: Context): String { fun MangaSource.getSummary(context: Context): String {
val type = context.getString(contentType.titleResId) 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) return context.getString(R.string.source_summary_pattern, type, locale)
} }

View File

@@ -85,7 +85,7 @@ class DoHManager(
).build() ).build()
DoHProvider.ZERO_MS -> DnsOverHttps.Builder().client(bootstrapClient) 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) .resolvePublicAddresses(true)
.build() .build()
} }

View File

@@ -290,17 +290,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_SOURCES_GRID, true) get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) } set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
val isNewSourcesTipEnabled: Boolean var sourcesVersion: Int
get() = prefs.getBoolean(KEY_SOURCES_NEW, true) get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
val isPagesNumbersEnabled: Boolean val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false) get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
val screenshotsPolicy: ScreenshotsPolicy val screenshotsPolicy: ScreenshotsPolicy
get() = runCatching { get() = prefs.getEnumValue(KEY_SCREENSHOTS_POLICY, ScreenshotsPolicy.ALLOW)
val key = prefs.getString(KEY_SCREENSHOTS_POLICY, null)?.uppercase(Locale.ROOT)
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
}.getOrDefault(ScreenshotsPolicy.ALLOW)
var userSpecifiedMangaDirectories: Set<File> var userSpecifiedMangaDirectories: Set<File>
get() { get() {
@@ -653,7 +651,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_APP_LOCALE = "app_locale" const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging" const val KEY_LOGGING_ENABLED = "logging"
const val KEY_SOURCES_GRID = "sources_grid" const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_SOURCES_NEW = "sources_new"
const val KEY_UPDATES_UNSTABLE = "updates_unstable" const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed" const val KEY_TIPS_CLOSED = "tips_closed"
const val KEY_SSL_BYPASS = "ssl_bypass" 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_STATS_ENABLED = "stats_on"
const val KEY_FEED_HEADER = "feed_header" const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types" const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
const val KEY_SOURCES_VERSION = "sources_version"
// keys for non-persistent preferences // keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version" const val KEY_APP_VERSION = "app_version"

View File

@@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.prefs
enum class ScreenshotsPolicy { enum class ScreenshotsPolicy {
// Do not rename this // Do not rename this
ALLOW, BLOCK_NSFW, BLOCK_ALL; ALLOW, BLOCK_NSFW, BLOCK_INCOGNITO, BLOCK_ALL;
} }

View File

@@ -19,6 +19,8 @@ import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver 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.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
@Suppress("LeakingThis") @Suppress("LeakingThis")
abstract class BaseActivity<B : ViewBinding> : abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(), AppCompatActivity(),
ScreenshotPolicyHelper.ContentContainer,
WindowInsetsDelegate.WindowInsetsListener { WindowInsetsDelegate.WindowInsetsListener {
private var isAmoledTheme = false 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?) { private fun putDataToExtras(intent: Intent?) {
intent?.putExtra(EXTRA_DATA, intent.data) intent?.putExtra(EXTRA_DATA, intent.data)
} }

View File

@@ -52,6 +52,16 @@ class FastScrollRecyclerView @JvmOverloads constructor(
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE 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() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
fastScroller.attachRecyclerView(this) fastScroller.attachRecyclerView(this)

View File

@@ -12,6 +12,8 @@ import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.castOrNull import org.koitharu.kotatsu.core.util.ext.castOrNull
import com.google.android.material.R as materialR
class ChipsView @JvmOverloads constructor( class ChipsView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@@ -24,7 +26,9 @@ class ChipsView @JvmOverloads constructor(
onChipClickListener?.onChipClick(it as Chip, it.tag) onChipClickListener?.onChipClick(it as Chip, it.tag)
} }
private val chipOnCloseListener = OnClickListener { 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 private val chipStyle: Int
var onChipClickListener: OnChipClickListener? = null var onChipClickListener: OnChipClickListener? = null
@@ -48,7 +52,7 @@ class ChipsView @JvmOverloads constructor(
if (isInEditMode) { if (isInEditMode) {
setChips( setChips(
List(5) { 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.isChipIconVisible = true
} }
chip.isChecked = model.isChecked 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 chip.tag = model.data
} }
@@ -106,12 +119,11 @@ class ChipsView @JvmOverloads constructor(
val chip = Chip(context) val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle) val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
chip.setChipDrawable(drawable) chip.setChipDrawable(drawable)
chip.isCheckedIconVisible = true
chip.isChipIconVisible = false chip.isChipIconVisible = false
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener) chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false) chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener) chip.setOnClickListener(chipOnClickListener)
chip.isElegantTextHeight = false
addView(chip) addView(chip)
return chip return chip
} }
@@ -127,11 +139,12 @@ class ChipsView @JvmOverloads constructor(
} }
data class ChipModel( data class ChipModel(
@ColorRes val tint: Int,
val title: CharSequence, val title: CharSequence,
@DrawableRes val icon: Int, @DrawableRes val icon: Int = 0,
val isCheckable: Boolean, val isCheckable: Boolean = false,
val isChecked: Boolean, @ColorRes val tint: Int = 0,
val isChecked: Boolean = false,
val isDropdown: Boolean = false,
val data: Any? = null, val data: Any? = null,
) )

View File

@@ -1,14 +1,27 @@
package org.koitharu.kotatsu.core.util package org.koitharu.kotatsu.core.util
import androidx.core.os.LocaleListCompat 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 import java.util.Locale
class LocaleComparator : Comparator<Locale> { class LocaleComparator : Comparator<Locale> {
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context) private val deviceLocales: List<String>
.map { it.language }
.distinct() 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 { override fun compare(a: Locale, b: Locale): Int {
val indexA = deviceLocales.indexOf(a.language) val indexA = deviceLocales.indexOf(a.language)

View File

@@ -22,11 +22,10 @@ fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementE
fun String.toLocale() = Locale(this) fun String.toLocale() = Locale(this)
fun Locale?.getDisplayName(context: Context): String { fun Locale?.getDisplayName(context: Context): String = when (this) {
if (this == null) { null -> context.getString(R.string.all_languages)
return context.getString(R.string.various_languages) Locale.ROOT -> context.getString(R.string.various_languages)
} else -> getDisplayLanguage(this).toTitleCase(this)
return getDisplayLanguage(this).toTitleCase(this)
} }
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> { private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {

View File

@@ -34,10 +34,12 @@ import coil.util.CoilUtils
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -197,6 +199,8 @@ class DetailsActivity :
addMenuProvider(menuProvider) addMenuProvider(menuProvider)
} }
override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.isNsfw == true }
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false) R.id.button_read -> openReader(isIncognitoMode = false)
@@ -612,10 +616,7 @@ class DetailsActivity :
ChipsView.ChipModel( ChipsView.ChipModel(
title = tag.title, title = tag.title,
tint = tagHighlighter.getTagTint(tag), tint = tagHighlighter.getTagTint(tag),
icon = 0,
data = tag, data = tag,
isCheckable = false,
isChecked = false,
) )
}, },
) )

View File

@@ -6,21 +6,22 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity 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.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle 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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.Collections import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
@Reusable @Reusable
@@ -29,6 +30,7 @@ class MangaSourcesRepository @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
) { ) {
private val isNewSourcesAssimilated = AtomicBoolean(false)
private val dao: MangaSourcesDao private val dao: MangaSourcesDao
get() = db.getSourcesDao() get() = db.getSourcesDao()
@@ -43,25 +45,62 @@ class MangaSourcesRepository @Inject constructor(
get() = Collections.unmodifiableSet(remoteSources) get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List<MangaSource> { suspend fun getEnabledSources(): List<MangaSource> {
assimilateNewSources()
val order = settings.sourcesSortOrder val order = settings.sourcesSortOrder
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order) return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
} }
suspend fun getDisabledSources(): Set<MangaSource> { suspend fun getDisabledSources(): Set<MangaSource> {
assimilateNewSources()
val result = EnumSet.copyOf(remoteSources) val result = EnumSet.copyOf(remoteSources)
val enabled = dao.findAllEnabledNames() val enabled = dao.findAllEnabledNames()
for (name in enabled) { for (name in enabled) {
val source = MangaSource(name) val source = name.toMangaSourceOrNull() ?: continue
result.remove(source) result.remove(source)
} }
if (settings.isNsfwContentDisabled) {
result.removeAll { it.isNsfw() }
}
return result 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> { fun observeIsEnabled(source: MangaSource): Flow<Boolean> {
return dao.observeIsEnabled(source.name) return dao.observeIsEnabled(source.name).onStart { assimilateNewSources() }
} }
fun observeEnabledSourcesCount(): Flow<Int> { fun observeEnabledSourcesCount(): Flow<Int> {
@@ -69,8 +108,10 @@ class MangaSourcesRepository @Inject constructor(
observeIsNsfwDisabled(), observeIsNsfwDisabled(),
dao.observeEnabled(SourcesSortOrder.MANUAL), dao.observeEnabled(SourcesSortOrder.MANUAL),
) { skipNsfw, sources -> ) { skipNsfw, sources ->
sources.count { !skipNsfw || !MangaSource(it.source).isNsfw() } sources.count {
}.distinctUntilChanged() it.source.toMangaSourceOrNull()?.let { s -> !skipNsfw || !s.isNsfw() } == true
}
}.distinctUntilChanged().onStart { assimilateNewSources() }
} }
fun observeAvailableSourcesCount(): Flow<Int> { fun observeAvailableSourcesCount(): Flow<Int> {
@@ -82,7 +123,7 @@ class MangaSourcesRepository @Inject constructor(
allMangaSources.count { x -> allMangaSources.count { x ->
x.name !in enabled && (!skipNsfw || !x.isNsfw()) x.name !in enabled && (!skipNsfw || !x.isNsfw())
} }
}.distinctUntilChanged() }.distinctUntilChanged().onStart { assimilateNewSources() }
} }
fun observeEnabledSources(): Flow<List<MangaSource>> = combine( fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
@@ -92,18 +133,18 @@ class MangaSourcesRepository @Inject constructor(
dao.observeEnabled(order).map { dao.observeEnabled(order).map {
it.toSources(skipNsfw, order) it.toSources(skipNsfw, order)
} }
}.flatMapLatest { it } }.flatMapLatest { it }.onStart { assimilateNewSources() }
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities -> fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size) val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
for (entity in entities) { for (entity in entities) {
val source = MangaSource(entity.source) val source = entity.source.toMangaSourceOrNull() ?: continue
if (source in remoteSources) { if (source in remoteSources) {
result.add(source to entity.isEnabled) result.add(source to entity.isEnabled)
} }
} }
result result
} }.onStart { assimilateNewSources() }
suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle { suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle {
setSourcesEnabledImpl(sources, isEnabled) setSourcesEnabledImpl(sources, isEnabled)
@@ -114,6 +155,7 @@ class MangaSourcesRepository @Inject constructor(
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) { suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
db.withTransaction { db.withTransaction {
assimilateNewSources()
for (s in remoteSources) { for (s in remoteSources) {
dao.setEnabled(s.name, s in sources) dao.setEnabled(s.name, s in sources)
} }
@@ -135,31 +177,34 @@ class MangaSourcesRepository @Inject constructor(
} }
} }
fun observeNewSources(): Flow<Set<MangaSource>> = observeIsNewSourcesEnabled().flatMapLatest { fun observeHasNewSources(): Flow<Boolean> = observeIsNsfwDisabled().map { skipNsfw ->
if (it) { val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null)
combine( sources.isNotEmpty() && sources.size != remoteSources.size
dao.observeAll(), }.onStart { assimilateNewSources() }
observeIsNsfwDisabled(),
) { entities, skipNsfw -> fun observeHasNewSourcesForBadge(): Flow<Boolean> = combine(
val result = EnumSet.copyOf(remoteSources) settings.observeAsFlow(AppSettings.KEY_SOURCES_VERSION) { sourcesVersion },
for (e in entities) { observeIsNsfwDisabled(),
result.remove(MangaSource(e.source)) ) { version, skipNsfw ->
} if (version < BuildConfig.VERSION_CODE) {
if (skipNsfw) { val sources = dao.findAllFromVersion(version).toSources(skipNsfw, null)
result.removeAll { x -> x.isNsfw() } sources.isNotEmpty()
}
result
}.distinctUntilChanged()
} else { } else {
assimilateNewSources() false
flowOf(emptySet())
} }
}.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() val new = getNewSources()
if (new.isEmpty()) { if (new.isEmpty()) {
return emptySet() return false
} }
var maxSortKey = dao.getMaxSortKey() var maxSortKey = dao.getMaxSortKey()
val entities = new.map { x -> val entities = new.map { x ->
@@ -167,17 +212,15 @@ class MangaSourcesRepository @Inject constructor(
source = x.name, source = x.name,
isEnabled = false, isEnabled = false,
sortKey = ++maxSortKey, sortKey = ++maxSortKey,
addedIn = BuildConfig.VERSION_CODE,
) )
} }
dao.insertIfAbsent(entities) dao.insertIfAbsent(entities)
if (settings.isNsfwContentDisabled) { return true
new.removeAll { x -> x.isNsfw() }
}
return new
} }
suspend fun isSetupRequired(): Boolean { suspend fun isSetupRequired(): Boolean {
return dao.findAll().isEmpty() return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty()
} }
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) { private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
@@ -196,7 +239,7 @@ class MangaSourcesRepository @Inject constructor(
val entities = dao.findAll() val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources) val result = EnumSet.copyOf(remoteSources)
for (e in entities) { for (e in entities) {
result.remove(MangaSource(e.source)) result.remove(e.source.toMangaSourceOrNull() ?: continue)
} }
return result return result
} }
@@ -204,10 +247,10 @@ class MangaSourcesRepository @Inject constructor(
private fun List<MangaSourceEntity>.toSources( private fun List<MangaSourceEntity>.toSources(
skipNsfwSources: Boolean, skipNsfwSources: Boolean,
sortOrder: SourcesSortOrder?, sortOrder: SourcesSortOrder?,
): List<MangaSource> { ): MutableList<MangaSource> {
val result = ArrayList<MangaSource>(size) val result = ArrayList<MangaSource>(size)
for (entity in this) { for (entity in this) {
val source = MangaSource(entity.source) val source = entity.source.toMangaSourceOrNull() ?: continue
if (skipNsfwSources && source.isNsfw()) { if (skipNsfwSources && source.isNsfw()) {
continue continue
} }
@@ -225,11 +268,9 @@ class MangaSourcesRepository @Inject constructor(
isNsfwContentDisabled isNsfwContentDisabled
} }
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
isNewSourcesTipEnabled
}
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) { private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
sourcesSortOrder sourcesSortOrder
} }
private fun String.toMangaSourceOrNull(): MangaSource? = MangaSource.entries.find { it.name == this }
} }

View File

@@ -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.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver 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.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe 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.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity 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.settings.sources.catalog.SourcesCatalogActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import javax.inject.Inject import javax.inject.Inject
@@ -56,7 +53,7 @@ class ExploreFragment :
BaseFragment<FragmentExploreBinding>(), BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner, RecyclerViewOwner,
ExploreListEventListener, ExploreListEventListener,
OnListItemClickListener<MangaSourceItem>, TipView.OnButtonClickListener, ListSelectionController.Callback2 { OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback2 {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@@ -74,7 +71,7 @@ class ExploreFragment :
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) 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)) startActivity(DetailsActivity.newIntent(view.context, manga))
} }
sourceSelectionController = ListSelectionController( 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) { override fun onClick(v: View) {
val intent = when (v.id) { val intent = when (v.id) {
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL) R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)

View File

@@ -102,12 +102,6 @@ class ExploreViewModel @Inject constructor(
} }
} }
fun discardNewSources() {
launchJob(Dispatchers.Default) {
sourcesRepository.assimilateNewSources()
}
}
fun requestPinShortcut(source: MangaSource) { fun requestPinShortcut(source: MangaSource) {
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
shortcutManager.requestPinShortcut(source) shortcutManager.requestPinShortcut(source)
@@ -124,7 +118,7 @@ class ExploreViewModel @Inject constructor(
getSuggestionFlow(), getSuggestionFlow(),
isGrid, isGrid,
isRandomLoading, isRandomLoading,
sourcesRepository.observeNewSources(), sourcesRepository.observeHasNewSourcesForBadge(),
) { content, suggestions, grid, randomLoading, newSources -> ) { content, suggestions, grid, randomLoading, newSources ->
buildList(content, suggestions, grid, randomLoading, newSources) buildList(content, suggestions, grid, randomLoading, newSources)
}.withErrorHandling() }.withErrorHandling()
@@ -134,7 +128,7 @@ class ExploreViewModel @Inject constructor(
recommendation: List<Manga>, recommendation: List<Manga>,
isGrid: Boolean, isGrid: Boolean,
randomLoading: Boolean, randomLoading: Boolean,
newSources: Set<MangaSource>, hasNewSources: Boolean,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 3) val result = ArrayList<ListModel>(sources.size + 3)
result += ExploreButtons(randomLoading) result += ExploreButtons(randomLoading)
@@ -146,7 +140,7 @@ class ExploreViewModel @Inject constructor(
result += ListHeader( result += ListHeader(
textRes = R.string.remote_sources, textRes = R.string.remote_sources,
buttonTextRes = R.string.catalog, buttonTextRes = R.string.catalog,
badge = if (newSources.isNotEmpty()) "" else null, badge = if (hasNewSources) "" else null,
) )
sources.mapTo(result) { MangaSourceItem(it, isGrid) } sources.mapTo(result) { MangaSourceItem(it, isGrid) }
} else { } else {
@@ -191,6 +185,5 @@ class ExploreViewModel @Inject constructor(
private const val TIP_SUGGESTIONS = "suggestions" private const val TIP_SUGGESTIONS = "suggestions"
private const val SUGGESTIONS_COUNT = 8 private const val SUGGESTIONS_COUNT = 8
const val TIP_NEW_SOURCES = "new_sources"
} }
} }

View File

@@ -4,13 +4,11 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener 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.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD 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.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -18,7 +16,6 @@ class ExploreAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: ExploreListEventListener, listener: ExploreListEventListener,
tipClickListener: TipView.OnButtonClickListener,
clickListener: OnListItemClickListener<MangaSourceItem>, clickListener: OnListItemClickListener<MangaSourceItem>,
mangaClickListener: OnListItemClickListener<Manga>, mangaClickListener: OnListItemClickListener<Manga>,
) : BaseListAdapter<ListModel>() { ) : BaseListAdapter<ListModel>() {
@@ -34,6 +31,5 @@ class ExploreAdapter(
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner)) addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.TIP, tipAD(tipClickListener))
} }
} }

View File

@@ -257,7 +257,7 @@ class FilterCoordinator @Inject constructor(
} }
oldValue.copy( oldValue.copy(
tagsExclude = newTags, tagsExclude = newTags,
tags = oldValue.tags - newTags tags = oldValue.tags - newTags,
) )
} }
} }
@@ -308,7 +308,7 @@ class FilterCoordinator @Inject constructor(
currentState.update { oldValue -> currentState.update { oldValue ->
oldValue.copy( oldValue.copy(
tags = tags, tags = tags,
tagsExclude = oldValue.tagsExclude - tags tagsExclude = oldValue.tagsExclude - tags,
) )
} }
} }
@@ -391,9 +391,7 @@ class FilterCoordinator @Inject constructor(
val result = LinkedList<ChipsView.ChipModel>() val result = LinkedList<ChipsView.ChipModel>()
for (tag in tags) { for (tag in tags) {
val model = ChipsView.ChipModel( val model = ChipsView.ChipModel(
tint = 0,
title = tag.title, title = tag.title,
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = selectedTags.remove(tag), isChecked = selectedTags.remove(tag),
data = tag, data = tag,
@@ -406,9 +404,7 @@ class FilterCoordinator @Inject constructor(
} }
for (tag in selectedTags) { for (tag in selectedTags) {
val model = ChipsView.ChipModel( val model = ChipsView.ChipModel(
tint = 0,
title = tag.title, title = tag.title,
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = true, isChecked = true,
data = tag, data = tag,

View File

@@ -61,10 +61,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
} }
private fun moreTagsChip() = ChipsView.ChipModel( private fun moreTagsChip() = ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more), title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material, icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
) )
} }

View File

@@ -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.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage 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.observe
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.showDistinct 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.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale import java.util.Locale
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -122,10 +122,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
b.spinnerLocale.context, b.spinnerLocale.context,
android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_dropdown_item,
android.R.id.text1, android.R.id.text1,
value.availableItems.map { value.availableItems.map { it.getDisplayName(b.spinnerLocale.context) },
it?.getDisplayLanguage(it)?.toTitleCase(it)
?: b.spinnerLocale.context.getString(R.string.various_languages)
},
) )
val selectedIndex = value.availableItems.indexOf(selected) val selectedIndex = value.availableItems.indexOf(selected)
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
@@ -144,9 +141,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1) val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
value.selectedItems.mapTo(chips) { tag -> value.selectedItems.mapTo(chips) { tag ->
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = tag.title, title = tag.title,
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = true, isChecked = true,
data = tag, data = tag,
@@ -155,9 +150,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
value.availableItems.mapNotNullTo(chips) { tag -> value.availableItems.mapNotNullTo(chips) { tag ->
if (tag !in value.selectedItems) { if (tag !in value.selectedItems) {
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = tag.title, title = tag.title,
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = false, isChecked = false,
data = tag, data = tag,
@@ -168,12 +161,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
chips.add( chips.add(
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more), title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material, icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
data = null,
), ),
) )
b.chipsGenres.setChips(chips) b.chipsGenres.setChips(chips)
@@ -200,9 +189,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
value.availableItems.mapNotNullTo(chips) { tag -> value.availableItems.mapNotNullTo(chips) { tag ->
if (tag !in value.selectedItems) { if (tag !in value.selectedItems) {
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = tag.title, title = tag.title,
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = false, isChecked = false,
data = tag, data = tag,
@@ -213,12 +200,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
chips.add( chips.add(
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more), title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material, icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
data = null,
), ),
) )
b.chipsGenresExclude.setChips(chips) b.chipsGenresExclude.setChips(chips)
@@ -233,9 +216,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
val chips = value.availableItems.map { state -> val chips = value.availableItems.map { state ->
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = getString(state.titleResId), title = getString(state.titleResId),
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = state in value.selectedItems, isChecked = state in value.selectedItems,
data = state, data = state,
@@ -253,9 +234,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
val chips = value.availableItems.map { contentRating -> val chips = value.availableItems.map { contentRating ->
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = getString(contentRating.titleResId), title = getString(contentRating.titleResId),
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = contentRating in value.selectedItems, isChecked = contentRating in value.selectedItems,
data = contentRating, data = contentRating,

View File

@@ -37,9 +37,6 @@ suspend fun Manga.toListDetailedModel(
ChipsView.ChipModel( ChipsView.ChipModel(
tint = extraProvider?.getTagTint(it) ?: 0, tint = extraProvider?.getTagTint(it) ?: 0,
title = it.title, title = it.title,
icon = 0,
isCheckable = false,
isChecked = false,
data = it, data = it,
) )
}, },

View File

@@ -85,10 +85,7 @@ class PreviewViewModel @Inject constructor(
ChipsView.ChipModel( ChipsView.ChipModel(
title = tag.title, title = tag.title,
tint = extraProvider.getTagTint(tag), tint = extraProvider.getTagTint(tag),
icon = 0,
data = tag, data = tag,
isCheckable = false,
isChecked = false,
) )
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())

View File

@@ -227,9 +227,11 @@ class LocalMangaRepository @Inject constructor(
}.filterNotNullTo(ArrayList(files.size)) }.filterNotNullTo(ArrayList(files.size))
} }
private suspend fun getAllFiles() = storageManager.getReadableDirs().asSequence().flatMap { dir -> private suspend fun getAllFiles() = storageManager.getReadableDirs()
dir.children() .asSequence()
} .flatMap { dir ->
dir.children().filterNot { it.isHidden }
}
private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga } private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }
} }

View File

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

View File

@@ -18,13 +18,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView 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.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.SheetWelcomeBinding import org.koitharu.kotatsu.databinding.SheetWelcomeBinding
import org.koitharu.kotatsu.filter.ui.model.FilterProperty import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
import java.util.Locale import java.util.Locale
@@ -58,7 +58,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
override fun onChipClick(chip: Chip, data: Any?) { override fun onChipClick(chip: Chip, data: Any?) {
when (data) { when (data) {
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked) 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 val chips = viewBinding?.chipsLocales ?: return
chips.setChips( chips.setChips(
value.availableItems.map { value.availableItems.map {
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0, title = it.getDisplayName(chips.context),
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = it in value.selectedItems, isChecked = it in value.selectedItems,
data = it, data = it,
@@ -107,9 +105,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
chips.setChips( chips.setChips(
value.availableItems.map { value.availableItems.map {
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = getString(it.titleResId), title = getString(it.titleResId),
icon = 0,
isCheckable = true, isCheckable = true,
isChecked = it in value.selectedItems, isChecked = it in value.selectedItems,
data = it, data = it,

View File

@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.LocaleComparator import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
import org.koitharu.kotatsu.core.util.ext.toList 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.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.ui.model.FilterProperty import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
@@ -27,14 +28,14 @@ class WelcomeViewModel @Inject constructor(
) : BaseViewModel() { ) : BaseViewModel() {
private val allSources = repository.allMangaSources 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 private var updateJob: Job
val locales = MutableStateFlow( val locales = MutableStateFlow(
FilterProperty<Locale?>( FilterProperty<Locale>(
availableItems = listOf(null), availableItems = listOf(Locale.ROOT),
selectedItems = setOf(null), selectedItems = setOf(Locale.ROOT),
isLoading = true, isLoading = true,
error = null, error = null,
), ),
@@ -51,22 +52,23 @@ class WelcomeViewModel @Inject constructor(
init { init {
updateJob = launchJob(Dispatchers.Default) { updateJob = launchJob(Dispatchers.Default) {
val languages = localesGroups.keys.associateBy { x -> x?.language } val languages = localesGroups.keys.associateBy { x -> x.language }
val selectedLocales = HashSet<Locale?>(2) val selectedLocales = HashSet<Locale>(2)
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList() ConfigurationCompat.getLocales(context.resources.configuration).toList()
.firstNotNullOfOrNull { lc -> languages[lc.language] } .firstNotNullOfOrNull { lc -> languages[lc.language] }
selectedLocales += null ?.let { selectedLocales += it }
selectedLocales += Locale.ROOT
locales.value = locales.value.copy( locales.value = locales.value.copy(
availableItems = localesGroups.keys.sortedWithSafe(nullsFirst(LocaleComparator())), availableItems = localesGroups.keys.sortedWithSafe(LocaleComparator()),
selectedItems = selectedLocales, selectedItems = selectedLocales,
isLoading = false, isLoading = false,
) )
repository.assimilateNewSources() repository.clearNewSourcesBadge()
commit() commit()
} }
} }
fun setLocaleChecked(locale: Locale?, isChecked: Boolean) { fun setLocaleChecked(locale: Locale, isChecked: Boolean) {
val snapshot = locales.value val snapshot = locales.value
locales.value = snapshot.copy( locales.value = snapshot.copy(
selectedItems = if (isChecked) { selectedItems = if (isChecked) {
@@ -99,7 +101,7 @@ class WelcomeViewModel @Inject constructor(
} }
private suspend fun commit() { 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 types = types.value.selectedItems
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x -> val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.contentType in types && x.locale in languages x.contentType in types && x.locale in languages

View File

@@ -28,6 +28,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
@@ -142,7 +143,6 @@ class ReaderActivity :
viewModel.content.observe(this) { viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value) onLoadingStateChanged(viewModel.isLoading.value)
} }
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn) viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged) viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this)) viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
@@ -179,6 +179,8 @@ class ReaderActivity :
viewModel.onPause() viewModel.onPause()
} }
override fun isNsfwContent(): Flow<Boolean> = viewModel.isMangaNsfw
override fun onIdle() { override fun onIdle() {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
} }
@@ -297,14 +299,6 @@ class ReaderActivity :
.show() .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) { private fun setKeepScreenOn(isKeep: Boolean) {
if (isKeep) { if (isKeep) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

View File

@@ -11,7 +11,9 @@ import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.BatteryManager import android.os.BatteryManager
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.RoundedCorner
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
@@ -46,8 +48,10 @@ class ReaderInfoBarView @JvmOverloads constructor(
private var insetLeft: Int = 0 private var insetLeft: Int = 0
private var insetRight: Int = 0 private var insetRight: Int = 0
private var insetTop: Int = 0 private var insetTop: Int = 0
private var cutoutInsetLeft = 0 private val insetLeftFallback: Int
private var cutoutInsetRight = 0 private val insetRightFallback: Int
private val insetTopFallback: Int
private val insetCornerFallback = getSystemUiDimensionOffset("rounded_corner_content_padding")
private val colorText = ColorUtils.setAlphaComponent( private val colorText = ColorUtils.setAlphaComponent(
context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK), context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK),
200, 200,
@@ -80,14 +84,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
paint.strokeWidth = getDimension(R.styleable.ReaderInfoBarView_android_strokeWidth, 2f) paint.strokeWidth = getDimension(R.styleable.ReaderInfoBarView_android_strokeWidth, 2f)
paint.textSize = getDimension(R.styleable.ReaderInfoBarView_android_textSize, 16f) paint.textSize = getDimension(R.styleable.ReaderInfoBarView_android_textSize, 16f)
} }
val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding") val insetStart = getSystemUiDimensionOffset("status_bar_padding_start")
val fallbackInset = resources.getDimensionPixelOffset(R.dimen.reader_bar_inset_fallback) val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end")
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start", fallbackInset) + insetCorner
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end", fallbackInset) + insetCorner
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
insetLeft = if (isRtl) insetEnd else insetStart insetLeftFallback = if (isRtl) insetEnd else insetStart
insetRight = if (isRtl) insetStart else insetEnd insetRightFallback = if (isRtl) insetStart else insetEnd
insetTop = minOf(insetLeft, insetRight) insetTopFallback = minOf(insetLeftFallback, insetRightFallback)
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -110,12 +112,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
paint.textAlign = Paint.Align.LEFT paint.textAlign = Paint.Align.LEFT
canvas.drawTextOutline( canvas.drawTextOutline(
text, text,
(paddingLeft + insetLeft + cutoutInsetLeft).toFloat(), (paddingLeft + insetLeft).toFloat(),
paddingTop + insetTop + ty, paddingTop + insetTop + ty,
) )
if (isTimeVisible) { if (isTimeVisible) {
paint.textAlign = Paint.Align.RIGHT 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) canvas.drawTextOutline(timeText, endX, paddingTop + insetTop + ty)
if (batteryText.isNotEmpty()) { if (batteryText.isNotEmpty()) {
paint.getTextBounds(timeText, 0, timeText.length, textBounds) paint.getTextBounds(timeText, 0, timeText.length, textBounds)
@@ -221,15 +223,29 @@ class ReaderInfoBarView @JvmOverloads constructor(
} }
private fun updateCutoutInsets(insetsCompat: WindowInsetsCompat?) { private fun updateCutoutInsets(insetsCompat: WindowInsetsCompat?) {
val cutouts = (insetsCompat ?: return).displayCutout?.boundingRects.orEmpty() insetLeft = insetLeftFallback
cutoutInsetLeft = 0 insetRight = insetRightFallback
cutoutInsetRight = 0 insetTop = insetTopFallback
for (rect in cutouts) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && insetsCompat != null) {
if (rect.left <= paddingLeft) { val nativeInsets = insetsCompat.toWindowInsets()
cutoutInsetLeft += rect.width() nativeInsets?.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.let { corner ->
insetLeft += corner.radius
} }
if (rect.right >= width - paddingRight) { nativeInsets?.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.let { corner ->
cutoutInsetRight += rect.width() 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()
}
} }
} }
} }

View File

@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -38,7 +39,6 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode 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.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -166,13 +166,9 @@ class ReaderViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null), }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
) )
val isScreenshotsBlockEnabled = combine( val isMangaNsfw = mangaFlow.map {
mangaFlow, it?.isNsfw == true
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 isBookmarkAdded = currentState.flatMapLatest { state -> val isBookmarkAdded = currentState.flatMapLatest { state ->
val manga = mangaData.value?.toManga() val manga = mangaData.value?.toManga()

View File

@@ -15,11 +15,14 @@ import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource 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.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
@@ -58,6 +61,8 @@ class MangaListActivity :
"Cannot find FilterOwner fragment in ${supportFragmentManager.fragments}" "Cannot find FilterOwner fragment in ${supportFragmentManager.fragments}"
}.filter }.filter
private var source: MangaSource? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMangaListBinding.inflate(layoutInflater)) setContentView(ActivityMangaListBinding.inflate(layoutInflater))
@@ -66,16 +71,19 @@ class MangaListActivity :
if (viewBinding.containerFilterHeader != null) { if (viewBinding.containerFilterHeader != null) {
viewBinding.appbar.addOnOffsetChangedListener(this) viewBinding.appbar.addOnOffsetChangedListener(this)
} }
val source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
if (source == null) { val src = source
if (src == null) {
finishAfterTransition() 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) { override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding( viewBinding.root.updatePadding(
left = insets.left, left = insets.left,

View File

@@ -172,12 +172,8 @@ class SearchSuggestionViewModel @Inject constructor(
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag -> private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0,
title = tag.title, title = tag.title,
icon = 0,
data = tag, data = tag,
isCheckable = false,
isChecked = false,
) )
} }
} }

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ sealed interface SourceCatalogItem : ListModel {
data class Source( data class Source(
val source: MangaSource, val source: MangaSource,
val showSummary: Boolean,
) : SourceCatalogItem { ) : SourceCatalogItem {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.catalog package org.koitharu.kotatsu.settings.sources.catalog
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding 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.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.crossfade 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.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible 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.ItemCatalogPageBinding
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
fun sourceCatalogItemSourceAD( fun sourceCatalogItemSourceAD(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: OnListItemClickListener<SourceCatalogItem.Source> listener: OnListItemClickListener<SourceCatalogItem.Source>
) = adapterDelegateViewBinding<SourceCatalogItem.Source, SourceCatalogItem, ItemSourceCatalogBinding>( ) = adapterDelegateViewBinding<SourceCatalogItem.Source, ListModel, ItemSourceCatalogBinding>(
{ layoutInflater, parent -> { layoutInflater, parent ->
ItemSourceCatalogBinding.inflate(layoutInflater, parent, false) ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
}, },
) { ) {
binding.imageViewAdd.setOnClickListener { v -> binding.imageViewAdd.setOnClickListener { v ->
listener.onItemLongClick(item, v)
}
binding.root.setOnClickListener { v ->
listener.onItemClick(item, v) listener.onItemClick(item, v)
} }
bind { bind {
binding.textViewTitle.text = item.source.getTitle(context) binding.textViewTitle.text = item.source.getTitle(context)
if (item.showSummary) { binding.textViewDescription.text = item.source.getSummary(context)
binding.textViewDescription.text = item.source.getSummary(context) binding.textViewDescription.drawableStart = if (item.source.isBroken) {
binding.textViewDescription.isVisible = true ContextCompat.getDrawable(context, R.drawable.ic_off_small)
} else { } else {
binding.textViewDescription.isVisible = false null
} }
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
@@ -61,7 +67,7 @@ fun sourceCatalogItemSourceAD(
fun sourceCatalogItemHintAD( fun sourceCatalogItemHintAD(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, SourceCatalogItem, ItemEmptyHintBinding>( ) = adapterDelegateViewBinding<SourceCatalogItem.Hint, ListModel, ItemEmptyHintBinding>(
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
) { ) {

View File

@@ -1,41 +1,46 @@
package org.koitharu.kotatsu.settings.sources.catalog package org.koitharu.kotatsu.settings.sources.catalog
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.chip.Chip
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R 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.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver 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.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.toLocale import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding 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.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 import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(), class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
OnListItemClickListener<SourceCatalogItem.Source>, OnListItemClickListener<SourceCatalogItem.Source>,
AppBarOwner, MenuItem.OnActionExpandListener { AppBarOwner, MenuItem.OnActionExpandListener, ChipsView.OnChipClickListener {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
private var newSourcesSnackbar: Snackbar? = null
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = viewBinding.appbar get() = viewBinding.appbar
@@ -45,18 +50,20 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater)) setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val pagerAdapter = SourcesCatalogPagerAdapter(this, coil, this) val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
viewBinding.pager.adapter = pagerAdapter with(viewBinding.recyclerView) {
val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter) setHasFixedSize(true)
tabMediator.attach() addItemDecoration(TypedListSpacingDecoration(context, false))
viewModel.content.observe(this, pagerAdapter) adapter = sourcesAdapter
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged) }
viewBinding.chipsFilter.onChipClickListener = this
viewModel.content.observe(this, sourcesAdapter)
viewModel.onActionDone.observeEvent( viewModel.onActionDone.observeEvent(
this, this,
ReversibleActionObserver(viewBinding.pager), ReversibleActionObserver(viewBinding.recyclerView),
) )
viewModel.locale.observe(this) { combine(viewModel.appliedFilter, viewModel.hasNewSources, ::Pair).observe(this) {
supportActionBar?.subtitle = it?.toLocale().getDisplayName(this) updateFilers(it.first, it.second)
} }
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this)) addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
} }
@@ -66,51 +73,85 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
left = insets.left, left = insets.left,
right = insets.right, 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) { 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) viewModel.addSource(item.source)
return false
} }
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = false
viewBinding.pager.isUserInputEnabled = false
val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty() val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty()
viewModel.performSearch(sq) viewModel.performSearch(sq)
return true return true
} }
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = true
viewBinding.pager.isUserInputEnabled = true
viewModel.performSearch(null) viewModel.performSearch(null)
return true 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 (hasNewSources) {
if (newSourcesSnackbar?.isShownOrQueued == true) { chips += ChipModel(
return title = getString(R.string._new),
} icon = R.drawable.ic_updated_selector,
val snackbar = Snackbar.make(viewBinding.pager, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE) isCheckable = true,
snackbar.setAction(R.string.explore) { isChecked = appliedFilter.isNewOnly,
NewSourcesDialogFragment.show(supportFragmentManager) data = true,
}
snackbar.addCallback(
object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (event == DISMISS_EVENT_SWIPE) {
viewModel.skipNewSources()
}
}
},
) )
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()
} }
} }

View File

@@ -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.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListItemType 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( class SourcesCatalogAdapter(
listener: OnListItemClickListener<SourceCatalogItem.Source>, listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<SourceCatalogItem>(), FastScroller.SectionIndexer { ) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init { init {
addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner)) addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
} }
override fun getSectionText(context: Context, position: Int): CharSequence? { override fun getSectionText(context: Context, position: Int): CharSequence? {

View File

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

View File

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

View File

@@ -4,14 +4,9 @@ import android.app.Activity
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R 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 import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
class SourcesCatalogMenuProvider( class SourcesCatalogMenuProvider(
@@ -32,14 +27,7 @@ class SourcesCatalogMenuProvider(
searchView.queryHint = searchMenuItem.title searchView.queryHint = searchMenuItem.title
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
R.id.action_locales -> {
showLocalesMenu()
true
}
else -> false
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true) (activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
@@ -57,24 +45,4 @@ class SourcesCatalogMenuProvider(
viewModel.performSearch(newText?.trim().orEmpty()) viewModel.performSearch(newText?.trim().orEmpty())
return true 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()
}
} }

View File

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

View File

@@ -1,29 +1,29 @@
package org.koitharu.kotatsu.settings.sources.catalog package org.koitharu.kotatsu.settings.sources.catalog
import androidx.annotation.MainThread
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl import androidx.room.invalidationTrackerFlow
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R 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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository 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.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource 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.EnumSet
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@@ -31,41 +31,47 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SourcesCatalogViewModel @Inject constructor( class SourcesCatalogViewModel @Inject constructor(
private val repository: MangaSourcesRepository, private val repository: MangaSourcesRepository,
private val listProducerFactory: SourcesCatalogListProducer.Factory, db: MangaDatabase,
private val settings: AppSettings, settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
private val lifecycle = RetainedLifecycleImpl()
private var searchQuery: String? = null
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
val locales = repository.allMangaSources.mapToSet { it.locale } val locales: Set<String?> = repository.allMangaSources.mapTo(HashSet<String?>()) { it.locale }.also {
val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales }) it.add(null)
}
val hasNewSources = repository.observeNewSources() private val searchQuery = MutableStateFlow<String?>(null)
.map { it.isNotEmpty() } 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) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
private val listProducers = locale.map { lc -> val content: StateFlow<List<ListModel>> = combine(
createListProducers(lc) searchQuery,
}.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value)) 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 { init {
val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } } repository.clearNewSourcesBadge()
combine<SourceCatalogPage, List<SourceCatalogPage>>(flows, Array<SourceCatalogPage>::toList)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
override fun onCleared() {
super.onCleared()
lifecycle.dispatchOnCleared()
} }
fun performSearch(query: String?) { fun performSearch(query: String?) {
searchQuery = query searchQuery.value = query?.trim()
listProducers.value.forEach { (_, v) -> v.setQuery(query) }
} }
fun setLocale(value: String?) { fun setLocale(value: String?) {
locale.value = value appliedFilter.value = appliedFilter.value.copy(locale = value)
} }
fun addSource(source: MangaSource) { fun addSource(source: MangaSource) {
@@ -75,21 +81,53 @@ class SourcesCatalogViewModel @Inject constructor(
} }
} }
fun skipNewSources() { fun setContentType(value: ContentType, isAdd: Boolean) {
launchJob { val filter = appliedFilter.value
repository.assimilateNewSources() 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 fun setNewOnly(value: Boolean) {
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> { appliedFilter.value = appliedFilter.value.copy(isNewOnly = value)
val types = EnumSet.allOf(ContentType::class.java) }
if (settings.isNsfwContentDisabled) {
types.remove(ContentType.HENTAI) private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List<SourceCatalogItem> {
} val sources = repository.getAvailableSources(
return types.associateWithTo(EnumMap(ContentType::class.java)) { type -> isDisabledOnly = true,
listProducerFactory.create(lc, type, lifecycle).also { isNewOnly = filter.isNewOnly,
it.setQuery(searchQuery) 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)
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.TwoStatePreference 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.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings 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.prefs.SearchSuggestionType
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle 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.FileSize
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent 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.core.util.ext.tryLaunch
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -63,6 +66,10 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
appShortcutManager.isDynamicShortcutsAvailable() appShortcutManager.isDynamicShortcutsAvailable()
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP) findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty() ?.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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -173,7 +173,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
.setIcon(R.drawable.ic_delete) .setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.clear) { _, _ -> .setPositiveButton(R.string.clear) { _, _ ->
viewModel.clear() viewModel.clearStats()
}.show() }.show()
} }

View File

@@ -1,21 +1,14 @@
package org.koitharu.kotatsu.stats.ui package org.koitharu.kotatsu.stats.ui
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.BaseViewModel 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.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
@@ -63,7 +56,7 @@ class StatsViewModel @Inject constructor(
selectedCategories.value = snapshot selectedCategories.value = snapshot
} }
fun clear() { fun clearStats() {
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
repository.clearStats() repository.clearStats()
readingStats.value = emptyList() readingStats.value = emptyList()

View File

@@ -25,6 +25,7 @@ class TrackEntity(
@ColumnInfo(name = "last_check_time") val lastCheckTime: Long, @ColumnInfo(name = "last_check_time") val lastCheckTime: Long,
@ColumnInfo(name = "last_chapter_date") val lastChapterDate: Long, @ColumnInfo(name = "last_chapter_date") val lastChapterDate: Long,
@ColumnInfo(name = "last_result") val lastResult: Int, @ColumnInfo(name = "last_result") val lastResult: Int,
@ColumnInfo(name = "last_error") val lastError: String?,
) { ) {
companion object { companion object {
@@ -42,6 +43,7 @@ class TrackEntity(
lastCheckTime = 0L, lastCheckTime = 0L,
lastChapterDate = 0, lastChapterDate = 0,
lastResult = RESULT_NONE, lastResult = RESULT_NONE,
lastError = null,
) )
} }
} }

View File

@@ -174,6 +174,7 @@ class TrackingRepository @Inject constructor(
lastCheckTime = tracking.lastCheck?.toEpochMilli() ?: 0L, lastCheckTime = tracking.lastCheck?.toEpochMilli() ?: 0L,
lastChapterDate = tracking.lastChapterDate?.toEpochMilli() ?: 0L, lastChapterDate = tracking.lastChapterDate?.toEpochMilli() ?: 0L,
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
lastError = null,
) )
db.getTracksDao().upsert(entity) db.getTracksDao().upsert(entity)
} }
@@ -230,6 +231,7 @@ class TrackingRepository @Inject constructor(
lastCheckTime = System.currentTimeMillis(), lastCheckTime = System.currentTimeMillis(),
lastChapterDate = lastChapterDate, lastChapterDate = lastChapterDate,
lastResult = TrackEntity.RESULT_FAILED, lastResult = TrackEntity.RESULT_FAILED,
lastError = updates.error?.toString(),
) )
is MangaUpdates.Success -> TrackEntity( is MangaUpdates.Success -> TrackEntity(
@@ -239,6 +241,7 @@ class TrackingRepository @Inject constructor(
lastCheckTime = System.currentTimeMillis(), lastCheckTime = System.currentTimeMillis(),
lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate }, lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate },
lastResult = if (updates.isNotEmpty()) TrackEntity.RESULT_HAS_UPDATE else TrackEntity.RESULT_NO_UPDATE, lastResult = if (updates.isNotEmpty()) TrackEntity.RESULT_HAS_UPDATE else TrackEntity.RESULT_NO_UPDATE,
lastError = null,
) )
} }
} }

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

View File

@@ -18,6 +18,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top" android:layout_gravity="top"
android:paddingHorizontal="6dp" android:paddingHorizontal="6dp"
android:paddingTop="8dp"
android:textSize="12sp" android:textSize="12sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@@ -28,6 +28,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top" android:layout_gravity="top"
android:padding="6dp"
android:textSize="12sp" android:textSize="12sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@@ -17,21 +17,39 @@
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar" android:id="@id/toolbar"
android:layout_width="match_parent" 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 <HorizontalScrollView
android:id="@+id/tabs" android:id="@+id/scrollView_chips"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tabGravity="start" android:clipToPadding="false"
app:tabMode="scrollable" /> 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> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2 <org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/pager" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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" /> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:windowBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall" android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal" android:orientation="horizontal"
@@ -45,17 +45,27 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:drawablePadding="4dp"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
tools:drawableStart="@drawable/ic_off_small"
tools:text="English" /> tools:text="English" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginVertical="4dp"
android:background="?colorOutline" />
<ImageView <ImageView
android:id="@+id/imageView_add" android:id="@+id/imageView_add"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/list_spacing_small"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/add" android:contentDescription="@string/add"
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"

View File

@@ -3,12 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> 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 <item
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="?actionModeWebSearchDrawable" android:icon="?actionModeWebSearchDrawable"

View File

@@ -641,4 +641,5 @@
<string name="disable_nsfw_notifications">Адключыць апавяшчэння NSFW</string> <string name="disable_nsfw_notifications">Адключыць апавяшчэння NSFW</string>
<string name="disable">Адкл.</string> <string name="disable">Адкл.</string>
<string name="sources_disabled">Крыніцы адключаны</string> <string name="sources_disabled">Крыніцы адключаны</string>
<string name="_new">Новае</string>
</resources> </resources>

View File

@@ -641,4 +641,7 @@
<string name="disable_nsfw_notifications_summary">No mostrar notificaciones sobre actualizaciones de manga NSFW</string> <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">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="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> </resources>

View File

@@ -637,4 +637,9 @@
<string name="disable_connectivity_check">Di paganahin ang pagtingin sa koneksyon</string> <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="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="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>
</resources> </resources>

View File

@@ -631,4 +631,15 @@
<string name="less_frequently">Moins souvent</string> <string name="less_frequently">Moins souvent</string>
<string name="new_chapters_pattern">%1$s: %2$d</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="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> </resources>

View File

@@ -641,4 +641,7 @@
<string name="disable_nsfw_notifications_summary">NSFW मंगा अपडेट के बारे में सूचनाएं न दिखाएं</string> <string name="disable_nsfw_notifications_summary">NSFW मंगा अपडेट के बारे में सूचनाएं न दिखाएं</string>
<string name="tracker_debug_info">नए अध्याय लॉग की जांच की जा रही है</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="all_languages">सभी भाषाएं</string>
<string name="screenshots_block_incognito">गुप्त मोड में ब्लॉक करें</string>
</resources> </resources>

View File

@@ -641,4 +641,7 @@
<string name="disable_nsfw_notifications_summary">Nie pokazuj powiadomień o aktualizacjach mangi NSFW</string> <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_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="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> </resources>

View File

@@ -641,4 +641,5 @@
<string name="disable_nsfw_notifications_summary">Не показывать уведомления об обновлениях манги NSFW</string> <string name="disable_nsfw_notifications_summary">Не показывать уведомления об обновлениях манги NSFW</string>
<string name="disable">Откл.</string> <string name="disable">Откл.</string>
<string name="sources_disabled">Источники отключены</string> <string name="sources_disabled">Источники отключены</string>
<string name="_new">Новое</string>
</resources> </resources>

View File

@@ -637,4 +637,9 @@
<string name="disable_connectivity_check">Онемогућите проверу везе</string> <string name="disable_connectivity_check">Онемогућите проверу везе</string>
<string name="disable_nsfw_notifications">Онемогућите НСФВ обавештења</string> <string name="disable_nsfw_notifications">Онемогућите НСФВ обавештења</string>
<string name="disable_nsfw_notifications_summary">Не приказуј обавештења за ажурирања НСФВ манге</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>
</resources> </resources>

View File

@@ -641,4 +641,7 @@
<string name="disable_nsfw_notifications_summary">Uygunsuz manga güncellemeleri hakkında bildirim gösterilmesin</string> <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">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="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> </resources>

View File

@@ -641,4 +641,5 @@
<string name="disable_nsfw_notifications_summary">Не відображати повідомлення про оновлення манґи NSFW</string> <string name="disable_nsfw_notifications_summary">Не відображати повідомлення про оновлення манґи NSFW</string>
<string name="disable">Вимкнути</string> <string name="disable">Вимкнути</string>
<string name="sources_disabled">Джерела вимкнено</string> <string name="sources_disabled">Джерела вимкнено</string>
<string name="_new">Нове</string>
</resources> </resources>

View File

@@ -640,5 +640,8 @@
<string name="disable_nsfw_notifications">关闭成人内容提醒</string> <string name="disable_nsfw_notifications">关闭成人内容提醒</string>
<string name="disable_nsfw_notifications_summary">不显示成人漫画的更新提醒</string> <string name="disable_nsfw_notifications_summary">不显示成人漫画的更新提醒</string>
<string name="tracker_debug_info">漫画更新日志</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> </resources>

View File

@@ -23,6 +23,7 @@
<string-array name="screenshots_policy" translatable="false"> <string-array name="screenshots_policy" translatable="false">
<item>@string/screenshots_allow</item> <item>@string/screenshots_allow</item>
<item>@string/screenshots_block_nsfw</item> <item>@string/screenshots_block_nsfw</item>
<item>@string/screenshots_block_incognito</item>
<item>@string/screenshots_block_all</item> <item>@string/screenshots_block_all</item>
</string-array> </string-array>
<string-array name="network_policy" translatable="false"> <string-array name="network_policy" translatable="false">

View File

@@ -30,11 +30,6 @@
<string-array name="values_track_sources_default" translatable="false"> <string-array name="values_track_sources_default" translatable="false">
<item>favourites</item> <item>favourites</item>
</string-array> </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"> <string-array name="values_network_policy" translatable="false">
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>

View File

@@ -29,7 +29,6 @@
<dimen name="toolbar_button_margin">10dp</dimen> <dimen name="toolbar_button_margin">10dp</dimen>
<dimen name="widget_cover_height">116dp</dimen> <dimen name="widget_cover_height">116dp</dimen>
<dimen name="widget_cover_width">84dp</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="scrobbling_list_spacing">12dp</dimen>
<dimen name="explore_grid_width">120dp</dimen> <dimen name="explore_grid_width">120dp</dimen>
<dimen name="chapter_grid_width">80dp</dimen> <dimen name="chapter_grid_width">80dp</dimen>

View File

@@ -650,4 +650,8 @@
<string name="disable_nsfw_notifications_summary">Do not show notifications about NSFW manga updates</string> <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">Checking for new chapters log</string>
<string name="tracker_debug_info_summary">Debug information about background checks for new chapters</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> </resources>

View File

@@ -125,14 +125,6 @@
android:title="@string/keep_screen_on" android:title="@string/keep_screen_on"
app:allowDividerAbove="true" /> 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 <ListPreference
android:defaultValue="2" android:defaultValue="2"
android:entries="@array/network_policy" android:entries="@array/network_policy"

View File

@@ -31,10 +31,4 @@
android:summary="@string/disable_nsfw_summary" android:summary="@string/disable_nsfw_summary"
android:title="@string/disable_nsfw" /> 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> </androidx.preference.PreferenceScreen>

View File

@@ -9,6 +9,13 @@
android:summary="@string/protect_application_summary" android:summary="@string/protect_application_summary"
android:title="@string/protect_application" /> 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 <SwitchPreferenceCompat
android:key="history_exclude_nsfw" android:key="history_exclude_nsfw"
android:summary="@string/exclude_nsfw_from_history_summary" android:summary="@string/exclude_nsfw_from_history_summary"