Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3b7d83dd6f
|
|||
|
877a018ced
|
|||
|
2e80b330e9
|
|||
|
42ca38e693
|
|||
|
|
d2fc3354af | ||
| 2a870e6167 | |||
|
|
393a9c2791 | ||
|
|
4c69839076 | ||
|
e37455e790
|
|||
|
|
36259ba901 | ||
|
|
5b041b9a49 | ||
|
1734e888d6
|
|||
|
9108646cea
|
|||
|
|
c6d1cf2f72 | ||
|
|
a317236cb0 | ||
|
|
3703c07a98 | ||
|
b7b32dd447
|
|||
|
c103386dc5
|
|||
|
a9d6ee4a95
|
|||
|
71f2c91e5a
|
|||
|
b878f358ff
|
|||
|
|
4c9989da78 | ||
|
|
8e8424022a | ||
|
86504b8bde
|
|||
|
f082fa084f
|
|||
| 040fe258e9 | |||
|
1076009572
|
|||
|
40dde71a1d
|
|||
|
|
9503aabf78 | ||
|
|
4ee16cfa2f | ||
|
f1c9eacaf0
|
|||
|
920e16be10
|
|||
|
4e0e5be726
|
|||
|
|
f18eca52af
|
||
|
451a155e08
|
|||
|
|
0612a7ad2c | ||
| 9c34f25eda | |||
| 42360c678f | |||
|
|
04a3d02aa9 | ||
|
808fd13ad0
|
|||
|
88c8dc4761
|
|||
|
a7eba67a97
|
|||
|
c27586231a
|
|||
|
db3db4637c
|
|||
|
bb2294f248
|
|||
|
afd56c02e6
|
|||
| dcf1ffc976 | |||
|
|
91b7028b1a | ||
|
|
734c217c03 | ||
|
|
de18c6eb71 | ||
|
|
034be6b44e | ||
|
|
995ff5a764 | ||
|
|
102bec04d6 | ||
| d05d807614 | |||
|
bffd75f4d9
|
|||
|
bdaf3da7e0
|
|||
|
353d856bf5
|
|||
| fca9ba98cd | |||
| 5df76fd881 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
/.idea/compiler.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/ktlint-plugin.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/kotlinScripting.xml
|
||||
/.idea/kotlinc.xml
|
||||
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 603
|
||||
versionName = '6.5-b1'
|
||||
versionCode = 610
|
||||
versionName = '6.5.4'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,17 +82,17 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:d6b578acb0') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:ea095084cc') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.21'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
|
||||
@@ -103,7 +103,7 @@ dependencies {
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
|
||||
|
||||
implementation 'androidx.work:work-runtime:2.9.0'
|
||||
@@ -120,13 +120,13 @@ dependencies {
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
||||
implementation 'com.squareup.okio:okio:3.6.0'
|
||||
implementation 'com.squareup.okio:okio:3.7.0'
|
||||
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||
|
||||
implementation 'com.google.dagger:hilt-android:2.49'
|
||||
kapt 'com.google.dagger:hilt-compiler:2.49'
|
||||
implementation 'com.google.dagger:hilt-android:2.50'
|
||||
kapt 'com.google.dagger:hilt-compiler:2.50'
|
||||
implementation 'androidx.hilt:hilt-work:1.1.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.1.0'
|
||||
|
||||
@@ -155,6 +155,6 @@ dependencies {
|
||||
androidTestImplementation 'androidx.room:room-testing:2.6.1'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.49'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.49'
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.50'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.50'
|
||||
}
|
||||
|
||||
@@ -61,13 +61,20 @@ class CaptchaNotifier(
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
super.onError(request, result)
|
||||
val e = result.throwable
|
||||
if (e is CloudFlareProtectedException) {
|
||||
if (e is CloudFlareProtectedException && request.parameters.value<Boolean>(PARAM_IGNORE_CAPTCHA) != true) {
|
||||
notify(e)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
companion object {
|
||||
|
||||
fun ImageRequest.Builder.ignoreCaptchaErrors() = setParameter(
|
||||
key = PARAM_IGNORE_CAPTCHA,
|
||||
value = true,
|
||||
memoryCacheKey = null,
|
||||
)
|
||||
|
||||
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
|
||||
private const val CHANNEL_ID = "captcha"
|
||||
private const val TAG = CHANNEL_ID
|
||||
}
|
||||
|
||||
@@ -3,17 +3,20 @@ package org.koitharu.kotatsu.core.backup
|
||||
import org.json.JSONArray
|
||||
|
||||
class BackupEntry(
|
||||
val name: String,
|
||||
val name: Name,
|
||||
val data: JSONArray
|
||||
) {
|
||||
|
||||
companion object Names {
|
||||
enum class Name(
|
||||
val key: String,
|
||||
) {
|
||||
|
||||
const val INDEX = "index"
|
||||
const val HISTORY = "history"
|
||||
const val CATEGORIES = "categories"
|
||||
const val FAVOURITES = "favourites"
|
||||
const val SETTINGS = "settings"
|
||||
const val BOOKMARKS = "bookmarks"
|
||||
INDEX("index"),
|
||||
HISTORY("history"),
|
||||
CATEGORIES("categories"),
|
||||
FAVOURITES("favourites"),
|
||||
SETTINGS("settings"),
|
||||
BOOKMARKS("bookmarks"),
|
||||
SOURCES("sources"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
||||
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PAGE_SIZE = 10
|
||||
@@ -20,7 +22,7 @@ class BackupRepository @Inject constructor(
|
||||
|
||||
suspend fun dumpHistory(): BackupEntry {
|
||||
var offset = 0
|
||||
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
|
||||
while (true) {
|
||||
val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
|
||||
if (history.isEmpty()) {
|
||||
@@ -41,7 +43,7 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun dumpCategories(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.CATEGORIES, JSONArray())
|
||||
val categories = db.getFavouriteCategoriesDao().findAll()
|
||||
for (item in categories) {
|
||||
entry.data.put(JsonSerializer(item).toJson())
|
||||
@@ -51,7 +53,7 @@ class BackupRepository @Inject constructor(
|
||||
|
||||
suspend fun dumpFavourites(): BackupEntry {
|
||||
var offset = 0
|
||||
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
|
||||
while (true) {
|
||||
val favourites = db.getFavouritesDao().findAll(offset, PAGE_SIZE)
|
||||
if (favourites.isEmpty()) {
|
||||
@@ -72,7 +74,7 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun dumpBookmarks(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
|
||||
val all = db.getBookmarksDao().findAll()
|
||||
for ((m, b) in all) {
|
||||
val json = JSONObject()
|
||||
@@ -90,7 +92,7 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
|
||||
fun dumpSettings(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.SETTINGS, JSONArray())
|
||||
val settingsDump = settings.getAllValues().toMutableMap()
|
||||
settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
|
||||
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
|
||||
@@ -101,8 +103,18 @@ class BackupRepository @Inject constructor(
|
||||
return entry
|
||||
}
|
||||
|
||||
suspend fun dumpSources(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
|
||||
val all = db.getSourcesDao().findAll()
|
||||
for (source in all) {
|
||||
val json = JsonSerializer(source).toJson()
|
||||
entry.data.put(json)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
fun createIndex(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.INDEX, JSONArray())
|
||||
val json = JSONObject()
|
||||
json.put("app_id", BuildConfig.APPLICATION_ID)
|
||||
json.put("app_version", BuildConfig.VERSION_CODE)
|
||||
@@ -111,6 +123,11 @@ class BackupRepository @Inject constructor(
|
||||
return entry
|
||||
}
|
||||
|
||||
fun getBackupDate(entry: BackupEntry?): Date? {
|
||||
val timestamp = entry?.data?.optJSONObject(0)?.getLongOrDefault("created_at", 0) ?: 0
|
||||
return if (timestamp == 0L) null else Date(timestamp)
|
||||
}
|
||||
|
||||
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
@@ -184,6 +201,17 @@ class BackupRepository @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun restoreSources(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val source = JsonDeserializer(item).toMangaSourceEntity()
|
||||
result += runCatchingCancellable {
|
||||
db.getSourcesDao().upsert(source)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun restoreSettings(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.Closeable
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import java.io.File
|
||||
import java.util.EnumSet
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class BackupZipInput(val file: File) : Closeable {
|
||||
|
||||
private val zipFile = ZipFile(file)
|
||||
|
||||
suspend fun getEntry(name: String): BackupEntry? = runInterruptible(Dispatchers.IO) {
|
||||
val entry = zipFile.getEntry(name) ?: return@runInterruptible null
|
||||
suspend fun getEntry(name: BackupEntry.Name): BackupEntry? = runInterruptible(Dispatchers.IO) {
|
||||
val entry = zipFile.getEntry(name.key) ?: return@runInterruptible null
|
||||
val json = zipFile.getInputStream(entry).use {
|
||||
JSONArray(it.bufferedReader().readText())
|
||||
}
|
||||
BackupEntry(name, json)
|
||||
}
|
||||
|
||||
suspend fun entries(): Set<BackupEntry.Name> = runInterruptible(Dispatchers.IO) {
|
||||
zipFile.entries().toList().mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { ze ->
|
||||
BackupEntry.Name.entries.find { it.key == ze.name }
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
zipFile.close()
|
||||
}
|
||||
|
||||
fun cleanupAsync() {
|
||||
processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
|
||||
runCatching {
|
||||
close()
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class BackupZipOutput(val file: File) : Closeable {
|
||||
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
|
||||
|
||||
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
|
||||
output.put(entry.name, entry.data.toString(2))
|
||||
output.put(entry.name.key, entry.data.toString(2))
|
||||
}
|
||||
|
||||
suspend fun finish() = runInterruptible(Dispatchers.IO) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.backup
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
@@ -78,6 +79,12 @@ class JsonDeserializer(private val json: JSONObject) {
|
||||
percent = json.getDouble("percent").toFloat(),
|
||||
)
|
||||
|
||||
fun toMangaSourceEntity() = MangaSourceEntity(
|
||||
source = json.getString("source"),
|
||||
isEnabled = json.getBoolean("enabled"),
|
||||
sortKey = json.getInt("sort_key"),
|
||||
)
|
||||
|
||||
fun toMap(): Map<String, Any?> {
|
||||
val map = mutableMapOf<String, Any?>()
|
||||
val keys = json.keys()
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.backup
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
@@ -82,6 +83,14 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
},
|
||||
)
|
||||
|
||||
constructor(e: MangaSourceEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("source", e.source)
|
||||
put("enabled", e.isEnabled)
|
||||
put("sort_key", e.sortKey)
|
||||
},
|
||||
)
|
||||
|
||||
constructor(m: Map<String, *>) : this(
|
||||
JSONObject(m),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@JvmName("mangaIds")
|
||||
fun Collection<Manga>.ids() = mapToSet { it.id }
|
||||
@@ -42,15 +43,17 @@ val MangaState.titleResId: Int
|
||||
MangaState.FINISHED -> R.string.state_finished
|
||||
MangaState.ABANDONED -> R.string.state_abandoned
|
||||
MangaState.PAUSED -> R.string.state_paused
|
||||
MangaState.UPCOMING -> R.string.state_upcoming
|
||||
}
|
||||
|
||||
@get:DrawableRes
|
||||
val MangaState.iconResId: Int
|
||||
get() = when (this) {
|
||||
MangaState.ONGOING -> R.drawable.ic_state_ongoing
|
||||
MangaState.ONGOING -> R.drawable.ic_play
|
||||
MangaState.FINISHED -> R.drawable.ic_state_finished
|
||||
MangaState.ABANDONED -> R.drawable.ic_state_abandoned
|
||||
MangaState.PAUSED -> R.drawable.ic_action_pause
|
||||
MangaState.UPCOMING -> materialR.drawable.ic_clock_black_24dp
|
||||
}
|
||||
|
||||
fun Manga.findChapter(id: Long): MangaChapter? {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.SuperscriptSpan
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import java.util.Locale
|
||||
|
||||
fun MangaSource.getLocaleTitle(): String? {
|
||||
val lc = Locale(locale ?: return null)
|
||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||
}
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
fun MangaSource(name: String): MangaSource {
|
||||
MangaSource.entries.forEach {
|
||||
@@ -33,6 +39,24 @@ val ContentType.titleResId
|
||||
|
||||
fun MangaSource.getSummary(context: Context): String {
|
||||
val type = context.getString(contentType.titleResId)
|
||||
val locale = getLocaleTitle() ?: context.getString(R.string.various_languages)
|
||||
val locale = locale?.toLocale().getDisplayName(context)
|
||||
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||
}
|
||||
|
||||
fun MangaSource.getTitle(context: Context): CharSequence = if (isNsfw()) {
|
||||
buildSpannedString {
|
||||
append(title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
title
|
||||
}
|
||||
|
||||
private fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
|
||||
RelativeSizeSpan(0.74f),
|
||||
SuperscriptSpan(),
|
||||
) {
|
||||
append(context.getString(R.string.nsfw))
|
||||
}
|
||||
|
||||
@@ -295,13 +295,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
|
||||
|
||||
var readerColorFilter: ReaderColorFilter?
|
||||
get() {
|
||||
get() = runCatching {
|
||||
val brightness = prefs.getFloat(KEY_CF_BRIGHTNESS, ReaderColorFilter.EMPTY.brightness)
|
||||
val contrast = prefs.getFloat(KEY_CF_CONTRAST, ReaderColorFilter.EMPTY.contrast)
|
||||
val inverted = prefs.getBoolean(KEY_CF_INVERTED, ReaderColorFilter.EMPTY.isInverted)
|
||||
val grayscale = prefs.getBoolean(KEY_CF_GRAYSCALE, ReaderColorFilter.EMPTY.isGrayscale)
|
||||
return ReaderColorFilter(brightness, contrast, inverted, grayscale).takeUnless { it.isEmpty }
|
||||
}
|
||||
ReaderColorFilter(brightness, contrast, inverted, grayscale).takeUnless { it.isEmpty }
|
||||
}.getOrNull()
|
||||
set(value) {
|
||||
prefs.edit {
|
||||
val cf = value ?: ReaderColorFilter.EMPTY
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
@@ -8,7 +10,9 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
@@ -62,4 +66,12 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
protected fun setTitle(title: CharSequence?) {
|
||||
(activity as? SettingsActivity)?.setSectionTitle(title)
|
||||
}
|
||||
|
||||
protected fun startActivitySafe(intent: Intent) {
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,5 @@ val SortOrder.titleRes: Int
|
||||
SortOrder.RATING -> R.string.by_rating
|
||||
SortOrder.NEWEST -> R.string.newest
|
||||
SortOrder.ALPHABETICAL -> R.string.by_name
|
||||
SortOrder.ALPHABETICAL_DESC -> R.string.by_name_reverse
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeLis
|
||||
lastInsets = null
|
||||
}
|
||||
|
||||
interface WindowInsetsListener {
|
||||
fun interface WindowInsetsListener {
|
||||
|
||||
fun onWindowInsetsChanged(insets: Insets)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.core.view.children
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.theme.overlay.MaterialThemeOverlay
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
||||
|
||||
@@ -42,9 +41,10 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
chipStyle = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0).use {
|
||||
it.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
|
||||
}
|
||||
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
|
||||
chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
|
||||
ta.recycle()
|
||||
|
||||
if (isInEditMode) {
|
||||
setChips(
|
||||
List(5) {
|
||||
@@ -103,14 +103,12 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun addChip(): Chip {
|
||||
val themedContext = MaterialThemeOverlay.wrap(context, null, 0, chipStyle)
|
||||
val chip = Chip(themedContext, null)
|
||||
val drawable = ChipDrawable.createFromAttributes(themedContext, null, 0, chipStyle)
|
||||
val chip = Chip(context)
|
||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
||||
chip.setChipDrawable(drawable)
|
||||
chip.isCheckedIconVisible = true
|
||||
chip.isChipIconVisible = false
|
||||
chip.setCheckedIconResource(R.drawable.ic_check)
|
||||
// chip.checkedIconTint = chip.ic
|
||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
|
||||
@@ -130,7 +130,7 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
|
||||
} else {
|
||||
// Set navbar scrim 70% of navigationBarColor
|
||||
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
|
||||
context.getThemeColor(android.R.attr.navigationBarColor, alphaFactor),
|
||||
context.getThemeColor(R.attr.m3ColorBottomMenuBackground, alphaFactor),
|
||||
elevation,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): St
|
||||
val length = ArrayReflect.getLength(checkNotNull(result))
|
||||
(0 until length).firstNotNullOfOrNull { i ->
|
||||
val storageVolumeElement = ArrayReflect.get(result, i)
|
||||
val uuid = getUuid.invoke(storageVolumeElement) as String
|
||||
val uuid = getUuid.invoke(storageVolumeElement) as String?
|
||||
val primary = isPrimary.invoke(storageVolumeElement) as Boolean
|
||||
when {
|
||||
primary && volumeId == PRIMARY_VOLUME_NAME -> getPath.invoke(storageVolumeElement) as String
|
||||
|
||||
@@ -20,12 +20,13 @@ inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
|
||||
|
||||
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
|
||||
|
||||
fun String?.getLocaleDisplayName(context: Context): String {
|
||||
fun String.toLocale() = Locale(this)
|
||||
|
||||
fun Locale?.getDisplayName(context: Context): String {
|
||||
if (this == null) {
|
||||
return context.getString(R.string.various_languages)
|
||||
}
|
||||
val lc = Locale(this)
|
||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||
return getDisplayLanguage(this).toTitleCase(this)
|
||||
}
|
||||
|
||||
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
|
||||
|
||||
@@ -91,7 +91,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_DETAILS
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga))
|
||||
context.startService(intent)
|
||||
tryStart(context, intent)
|
||||
}
|
||||
|
||||
fun prefetchPages(context: Context, chapter: MangaChapter) {
|
||||
@@ -99,19 +99,14 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_PAGES
|
||||
intent.putExtra(EXTRA_CHAPTER, ParcelableChapter(chapter))
|
||||
try {
|
||||
context.startService(intent)
|
||||
} catch (e: IllegalStateException) {
|
||||
// probably app is in background
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
tryStart(context, intent)
|
||||
}
|
||||
|
||||
fun prefetchLast(context: Context) {
|
||||
if (!isPrefetchAvailable(context, null)) return
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_LAST
|
||||
context.startService(intent)
|
||||
tryStart(context, intent)
|
||||
}
|
||||
|
||||
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||
@@ -127,5 +122,14 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
)
|
||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
|
||||
}
|
||||
|
||||
private fun tryStart(context: Context, intent: Intent) {
|
||||
try {
|
||||
context.startService(intent)
|
||||
} catch (e: IllegalStateException) {
|
||||
// probably app is in background
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet
|
||||
import org.koitharu.kotatsu.core.model.countChaptersByBranch
|
||||
import org.koitharu.kotatsu.core.model.iconResId
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
@@ -66,7 +68,6 @@ import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
|
||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
@@ -181,28 +182,13 @@ class DetailsFragment :
|
||||
ratingBar.isVisible = false
|
||||
}
|
||||
|
||||
when (manga.state) {
|
||||
MangaState.FINISHED -> infoLayout.textViewState.apply {
|
||||
textAndVisible = resources.getString(R.string.state_finished)
|
||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
|
||||
infoLayout.textViewState.apply {
|
||||
manga.state?.let { state ->
|
||||
textAndVisible = resources.getString(state.titleResId)
|
||||
drawableTop = ContextCompat.getDrawable(context, state.iconResId)
|
||||
} ?: run {
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
MangaState.ONGOING -> infoLayout.textViewState.apply {
|
||||
textAndVisible = resources.getString(R.string.state_ongoing)
|
||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
|
||||
}
|
||||
|
||||
MangaState.ABANDONED -> infoLayout.textViewState.apply {
|
||||
textAndVisible = resources.getString(R.string.state_abandoned)
|
||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_abandoned)
|
||||
}
|
||||
|
||||
MangaState.PAUSED -> infoLayout.textViewState.apply {
|
||||
textAndVisible = resources.getString(R.string.state_paused)
|
||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_action_pause)
|
||||
}
|
||||
|
||||
null -> infoLayout.textViewState.isVisible = false
|
||||
}
|
||||
if (manga.source == MangaSource.LOCAL) {
|
||||
infoLayout.textViewSource.isVisible = false
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
@@ -134,8 +135,14 @@ class DetailsViewModel @Inject constructor(
|
||||
.map { it?.local }
|
||||
.distinctUntilChanged()
|
||||
.map { local ->
|
||||
local?.file?.computeSize() ?: 0L
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
|
||||
if (local != null) {
|
||||
runCatchingCancellable {
|
||||
local.file.computeSize()
|
||||
}.getOrDefault(0L)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), 0L)
|
||||
|
||||
@Deprecated("")
|
||||
val description = details
|
||||
|
||||
@@ -160,7 +160,7 @@ class ExploreFragment :
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
|
||||
override fun onEmptyActionClick() {
|
||||
startActivity(SettingsActivity.newManageSourcesIntent(context ?: return))
|
||||
startActivity(Intent(context ?: return, SourcesCatalogActivity::class.java))
|
||||
}
|
||||
|
||||
private fun onOpenManga(manga: Manga) {
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
@@ -30,7 +29,6 @@ import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.TipModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
@@ -56,8 +54,6 @@ class ExploreViewModel @Inject constructor(
|
||||
valueProducer = { isSuggestionsEnabled },
|
||||
)
|
||||
|
||||
val sortOrder = MutableStateFlow(SourcesSortOrder.MANUAL) // TODO
|
||||
|
||||
val onOpenManga = MutableEventFlow<Manga>()
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onShowSuggestionsTip = MutableEventFlow<Unit>()
|
||||
@@ -129,31 +125,25 @@ class ExploreViewModel @Inject constructor(
|
||||
randomLoading: Boolean,
|
||||
newSources: Set<MangaSource>,
|
||||
): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(sources.size + 4)
|
||||
val result = ArrayList<ListModel>(sources.size + 3)
|
||||
result += ExploreButtons(randomLoading)
|
||||
if (recommendation != null) {
|
||||
result += ListHeader(R.string.suggestions)
|
||||
result += RecommendationsItem(recommendation)
|
||||
}
|
||||
if (sources.isNotEmpty()) {
|
||||
result += ListHeader(R.string.remote_sources, R.string.catalog)
|
||||
if (newSources.isNotEmpty()) {
|
||||
result += TipModel(
|
||||
key = TIP_NEW_SOURCES,
|
||||
title = R.string.new_sources_text,
|
||||
text = R.string.new_sources_text,
|
||||
icon = R.drawable.ic_explore_normal,
|
||||
primaryButtonText = R.string.manage,
|
||||
secondaryButtonText = R.string.discard,
|
||||
)
|
||||
}
|
||||
result += ListHeader(
|
||||
textRes = R.string.remote_sources,
|
||||
buttonTextRes = R.string.catalog,
|
||||
badge = if (newSources.isNotEmpty()) "" else null,
|
||||
)
|
||||
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
|
||||
} else {
|
||||
result += EmptyHint(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.no_manga_sources,
|
||||
textSecondary = R.string.no_manga_sources_text,
|
||||
actionStringRes = R.string.manage,
|
||||
actionStringRes = R.string.catalog,
|
||||
)
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -8,6 +8,7 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getSummary
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
@@ -112,7 +113,7 @@ fun exploreSourceListItemAD(
|
||||
binding.root.setOnContextClickListenerCompat(eventListener)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.title
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
binding.textViewSubtitle.text = item.source.getSummary(context)
|
||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||
@@ -147,7 +148,7 @@ fun exploreSourceGridItemAD(
|
||||
binding.root.setOnContextClickListenerCompat(eventListener)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.title
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name)
|
||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||
fallback(fallbackIcon)
|
||||
|
||||
@@ -65,10 +65,7 @@ fun categoryAD(
|
||||
binding.imageViewEdit.setOnClickListener(eventListener)
|
||||
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||
|
||||
bind { payloads ->
|
||||
if (payloads.isNotEmpty()) {
|
||||
return@bind
|
||||
}
|
||||
bind {
|
||||
binding.textViewTitle.text = item.category.title
|
||||
binding.textViewSubtitle.text = if (item.mangaCount == 0) {
|
||||
getString(R.string.empty)
|
||||
|
||||
@@ -15,13 +15,13 @@ fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, List
|
||||
|
||||
val onClickListener = View.OnClickListener { v ->
|
||||
val intent = when (v.id) {
|
||||
R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.chip_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
v.context.startActivity(intent)
|
||||
}
|
||||
|
||||
binding.buttonCreate.setOnClickListener(onClickListener)
|
||||
binding.buttonManage.setOnClickListener(onClickListener)
|
||||
binding.chipCreate.setOnClickListener(onClickListener)
|
||||
binding.chipManage.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -39,8 +40,11 @@ class FilterSheetFragment :
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
}
|
||||
val filter = requireFilter()
|
||||
filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
|
||||
@@ -14,22 +14,37 @@ import com.google.android.material.R as materialR
|
||||
|
||||
@CheckResult
|
||||
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
|
||||
return if (counter > 0) {
|
||||
val badgeDrawable = badge ?: initBadge(this)
|
||||
badgeDrawable.number = counter
|
||||
badgeDrawable.isVisible = true
|
||||
badgeDrawable.align(this)
|
||||
badgeDrawable
|
||||
} else {
|
||||
badge?.isVisible = false
|
||||
badge
|
||||
}
|
||||
return bindBadgeImpl(badge, null, counter)
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun View.bindBadge(badge: BadgeDrawable?, text: String?): BadgeDrawable? {
|
||||
return bindBadgeImpl(badge, text, 0)
|
||||
}
|
||||
|
||||
fun View.clearBadge(badge: BadgeDrawable?) {
|
||||
BadgeUtils.detachBadgeDrawable(badge, this)
|
||||
}
|
||||
|
||||
private fun View.bindBadgeImpl(
|
||||
badge: BadgeDrawable?,
|
||||
text: String?,
|
||||
counter: Int,
|
||||
): BadgeDrawable? = if (text != null || counter > 0) {
|
||||
val badgeDrawable = badge ?: initBadge(this)
|
||||
if (counter > 0) {
|
||||
badgeDrawable.number = counter
|
||||
} else {
|
||||
badgeDrawable.text = text?.takeUnless { it.isEmpty() }
|
||||
}
|
||||
badgeDrawable.isVisible = true
|
||||
badgeDrawable.align(this)
|
||||
badgeDrawable
|
||||
} else {
|
||||
badge?.isVisible = false
|
||||
badge
|
||||
}
|
||||
|
||||
private fun initBadge(anchor: View): BadgeDrawable {
|
||||
val badge = BadgeDrawable.create(anchor.context)
|
||||
val resources = anchor.resources
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
@@ -12,6 +13,8 @@ fun listHeaderAD(
|
||||
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
|
||||
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
var badge: BadgeDrawable? = null
|
||||
|
||||
if (listener != null) {
|
||||
binding.buttonMore.setOnClickListener {
|
||||
listener.onListHeaderClick(item, it)
|
||||
@@ -23,9 +26,11 @@ fun listHeaderAD(
|
||||
if (item.buttonTextRes == 0) {
|
||||
binding.buttonMore.isInvisible = true
|
||||
binding.buttonMore.text = null
|
||||
binding.buttonMore.clearBadge(badge)
|
||||
} else {
|
||||
binding.buttonMore.setText(item.buttonTextRes)
|
||||
binding.buttonMore.isVisible = true
|
||||
badge = itemView.bindBadge(badge, item.badge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,38 +6,41 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||
|
||||
@Suppress("DataClassPrivateConstructor")
|
||||
data class ListHeader private constructor(
|
||||
private val text: CharSequence?,
|
||||
@StringRes private val textRes: Int,
|
||||
private val dateTimeAgo: DateTimeAgo?,
|
||||
private val textRaw: Any,
|
||||
@StringRes val buttonTextRes: Int,
|
||||
val payload: Any?,
|
||||
val badge: String?,
|
||||
) : ListModel {
|
||||
|
||||
constructor(
|
||||
text: CharSequence,
|
||||
@StringRes buttonTextRes: Int = 0,
|
||||
payload: Any? = null,
|
||||
) : this(text, 0, null, buttonTextRes, payload)
|
||||
badge: String? = null,
|
||||
) : this(textRaw = text, buttonTextRes, payload, badge)
|
||||
|
||||
constructor(
|
||||
@StringRes textRes: Int,
|
||||
@StringRes buttonTextRes: Int = 0,
|
||||
payload: Any? = null,
|
||||
) : this(null, textRes, null, buttonTextRes, payload)
|
||||
badge: String? = null,
|
||||
) : this(textRaw = textRes, buttonTextRes, payload, badge)
|
||||
|
||||
constructor(
|
||||
dateTimeAgo: DateTimeAgo,
|
||||
@StringRes buttonTextRes: Int = 0,
|
||||
payload: Any? = null,
|
||||
) : this(null, 0, dateTimeAgo, buttonTextRes, payload)
|
||||
badge: String? = null,
|
||||
) : this(textRaw = dateTimeAgo, buttonTextRes, payload, badge)
|
||||
|
||||
fun getText(context: Context): CharSequence? = when {
|
||||
text != null -> text
|
||||
textRes != 0 -> context.getString(textRes)
|
||||
else -> dateTimeAgo?.format(context.resources)
|
||||
fun getText(context: Context): CharSequence? = when (textRaw) {
|
||||
is CharSequence -> textRaw
|
||||
is Int -> if (textRaw != 0) context.getString(textRaw) else null
|
||||
is DateTimeAgo -> textRaw.format(context.resources)
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is ListHeader && text == other.text && dateTimeAgo == other.dateTimeAgo && textRes == other.textRes
|
||||
return other is ListHeader && textRaw == other.textRaw
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import javax.inject.Inject
|
||||
@@ -56,8 +57,10 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
binding.textViewAuthor.setOnClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
binding.buttonOpen.setOnClickListener(this)
|
||||
binding.buttonRead.setOnClickListener(this)
|
||||
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.footer.observe(viewLifecycleOwner, ::onFooterUpdated)
|
||||
viewModel.tagsChips.observe(viewLifecycleOwner, ::onTagsChipsChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
}
|
||||
@@ -70,6 +73,14 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
DetailsActivity.newIntent(v.context, manga),
|
||||
)
|
||||
|
||||
R.id.button_read -> {
|
||||
startActivity(
|
||||
ReaderActivity.IntentBuilder(v.context)
|
||||
.manga(manga)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.textView_author -> startActivity(
|
||||
SearchActivity.newIntent(
|
||||
context = v.context,
|
||||
@@ -118,6 +129,43 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFooterUpdated(footer: PreviewViewModel.FooterInfo?) {
|
||||
with(requireViewBinding()) {
|
||||
toolbarBottom.isVisible = footer != null
|
||||
if (footer == null) {
|
||||
return
|
||||
}
|
||||
toolbarBottom.title = when {
|
||||
footer.isInProgress() -> {
|
||||
getString(R.string.chapter_d_of_d, footer.currentChapter, footer.totalChapters)
|
||||
}
|
||||
|
||||
footer.totalChapters > 0 -> {
|
||||
resources.getQuantityString(R.plurals.chapters, footer.totalChapters, footer.totalChapters)
|
||||
}
|
||||
|
||||
else -> {
|
||||
getString(R.string.no_chapters)
|
||||
}
|
||||
}
|
||||
buttonRead.isEnabled = footer.totalChapters > 0
|
||||
buttonRead.setIconResource(
|
||||
when {
|
||||
footer.isIncognito -> R.drawable.ic_incognito
|
||||
footer.isInProgress() -> R.drawable.ic_play
|
||||
else -> R.drawable.ic_read
|
||||
},
|
||||
)
|
||||
buttonRead.setText(
|
||||
if (footer.isInProgress()) {
|
||||
R.string._continue
|
||||
} else {
|
||||
R.string.read
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = viewBinding?.textViewDescription ?: return
|
||||
when {
|
||||
|
||||
@@ -13,11 +13,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -25,6 +28,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -33,6 +37,7 @@ class PreviewViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val extraProvider: ListExtraProvider,
|
||||
private val repositoryFactory: MangaRepository.Factory,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
@@ -40,6 +45,26 @@ class PreviewViewModel @Inject constructor(
|
||||
savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga,
|
||||
)
|
||||
|
||||
val footer = combine(
|
||||
manga,
|
||||
historyRepository.observeOne(manga.value.id),
|
||||
manga.flatMapLatest { historyRepository.observeShouldSkip(it) }.distinctUntilChanged(),
|
||||
) { m, history, incognito ->
|
||||
if (m.chapters == null) {
|
||||
return@combine null
|
||||
}
|
||||
val b = m.getPreferredBranch(history)
|
||||
val chapters = m.getChapters(b).orEmpty()
|
||||
FooterInfo(
|
||||
branch = b,
|
||||
currentChapter = history?.chapterId?.let {
|
||||
chapters.indexOfFirst { x -> x.id == it }
|
||||
} ?: -1,
|
||||
totalChapters = chapters.size,
|
||||
isIncognito = incognito,
|
||||
)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||
|
||||
val description = manga
|
||||
.distinctUntilChangedBy { it.description.orEmpty() }
|
||||
.transformLatest {
|
||||
@@ -82,4 +107,14 @@ class PreviewViewModel @Inject constructor(
|
||||
}
|
||||
return spannable.trim()
|
||||
}
|
||||
|
||||
data class FooterInfo(
|
||||
val branch: String?,
|
||||
val currentChapter: Int,
|
||||
val totalChapters: Int,
|
||||
val isIncognito: Boolean,
|
||||
) {
|
||||
|
||||
fun isInProgress() = currentChapter >= 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ class LocalMangaRepository @Inject constructor(
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
|
||||
suspend fun findSavedManga(remoteManga: Manga): LocalManga? = runCatchingCancellable {
|
||||
// fast path
|
||||
LocalMangaInput.find(storageManager.getReadableDirs(), remoteManga)?.let {
|
||||
return it.getManga()
|
||||
@@ -155,7 +155,9 @@ class LocalMangaRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
}.firstOrNull()?.getManga()
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage) = page.url
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
||||
import java.io.File
|
||||
import java.util.TreeMap
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
@@ -49,8 +50,15 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
url = mangaUri,
|
||||
coverUrl = cover,
|
||||
largeCoverUrl = cover,
|
||||
chapters = info.chapters?.mapIndexed { i, c ->
|
||||
c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL)
|
||||
chapters = info.chapters?.mapIndexedNotNull { i, c ->
|
||||
val fileName = index.getChapterFileName(c.id)
|
||||
val file = if (fileName != null) {
|
||||
chapterFiles[fileName]
|
||||
} else {
|
||||
// old downloads
|
||||
chapterFiles.values.elementAtOrNull(i)
|
||||
} ?: return@mapIndexedNotNull null
|
||||
c.copy(url = file.toUri().toString(), source = MangaSource.LOCAL)
|
||||
},
|
||||
) ?: Manga(
|
||||
id = root.absolutePath.longHashCode(),
|
||||
@@ -59,7 +67,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
publicUrl = mangaUri,
|
||||
source = MangaSource.LOCAL,
|
||||
coverUrl = findFirstImageEntry().orEmpty(),
|
||||
chapters = chapterFiles.mapIndexed { i, f ->
|
||||
chapters = chapterFiles.values.mapIndexed { i, f ->
|
||||
MangaChapter(
|
||||
id = "$i${f.name}".longHashCode(),
|
||||
name = f.nameWithoutExtension.toHumanReadable(),
|
||||
@@ -120,9 +128,9 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
|
||||
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
|
||||
|
||||
private fun getChaptersFiles(): List<File> = root.walkCompat()
|
||||
private fun getChaptersFiles() = root.walkCompat()
|
||||
.filter { it.hasCbzExtension() }
|
||||
.toListSorted(compareBy(AlphanumComparator()) { it.name })
|
||||
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
|
||||
|
||||
private fun findFirstImageEntry(): String? {
|
||||
return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||
import java.io.File
|
||||
|
||||
@@ -54,7 +55,8 @@ sealed class LocalMangaInput(
|
||||
zip.isFile -> LocalMangaZipInput(zip)
|
||||
else -> null
|
||||
}
|
||||
if (input?.getMangaInfo()?.id == manga.id) {
|
||||
val info = runCatchingCancellable { input?.getMangaInfo() }.getOrNull()
|
||||
if (info?.id == manga.id) {
|
||||
send(input)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.Closeable
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||
import java.io.File
|
||||
|
||||
@@ -86,7 +88,11 @@ sealed class LocalMangaOutput(
|
||||
}
|
||||
|
||||
private suspend fun canWriteTo(file: File, manga: Manga): Boolean {
|
||||
val info = LocalMangaInput.of(file).getMangaInfo() ?: return false
|
||||
val info = runCatchingCancellable {
|
||||
LocalMangaInput.of(file).getMangaInfo()
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull() ?: return false
|
||||
return info.id == manga.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -47,7 +45,6 @@ import org.koitharu.kotatsu.core.util.ext.hideKeyboard
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
@@ -55,6 +52,7 @@ import org.koitharu.kotatsu.history.ui.HistoryListFragment
|
||||
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
@@ -66,7 +64,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateDialog
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -98,17 +95,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityMainBinding.inflate(layoutInflater))
|
||||
|
||||
if (bottomNav != null) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.root) { _, insets ->
|
||||
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
|
||||
val elevation = bottomNav?.elevation ?: 0f
|
||||
window.setNavigationBarTransparentCompat(this@MainActivity, elevation)
|
||||
}
|
||||
insets
|
||||
}
|
||||
ViewCompat.requestApplyInsets(viewBinding.root)
|
||||
}
|
||||
|
||||
with(viewBinding.searchView) {
|
||||
onFocusChangeListener = this@MainActivity
|
||||
searchSuggestionListener = this@MainActivity
|
||||
@@ -139,10 +125,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null))
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
||||
viewModel.counters.observe(this, ::onCountersChanged)
|
||||
viewModel.feedCounter.observe(this, ::onFeedCounterChanged)
|
||||
viewModel.appUpdate.observe(this, MenuInvalidator(this))
|
||||
viewModel.onFirstStart.observeEvent(this) {
|
||||
OnboardDialogFragment.show(supportFragmentManager)
|
||||
WelcomeSheet.show(supportFragmentManager)
|
||||
}
|
||||
viewModel.isIncognitoMode.observe(this) {
|
||||
adjustSearchUI(isSearchOpened(), false)
|
||||
@@ -292,10 +278,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
startActivity(IntentBuilder(this).manga(manga).build(), options)
|
||||
}
|
||||
|
||||
private fun onCountersChanged(counters: Map<NavItem, Int>) {
|
||||
counters.forEach { (navItem, counter) ->
|
||||
navigationDelegate.setCounter(navItem, counter)
|
||||
}
|
||||
private fun onFeedCounterChanged(counter: Int) {
|
||||
navigationDelegate.setCounter(NavItem.FEED, counter)
|
||||
}
|
||||
|
||||
private fun onIncognitoModeChanged(isIncognito: Boolean) {
|
||||
|
||||
@@ -4,15 +4,11 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.NavItem
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
@@ -22,7 +18,6 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.main.domain.ReadingResumeEnabledUseCase
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -52,19 +47,8 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
val appUpdate = appUpdateRepository.observeAvailableUpdate()
|
||||
|
||||
val counters = combine(
|
||||
trackingRepository.observeUpdatedMangaCount(),
|
||||
observeNewSourcesCount(),
|
||||
) { tracks, newSources ->
|
||||
val em = EnumMap<NavItem, Int>(NavItem::class.java)
|
||||
em[NavItem.EXPLORE] = newSources
|
||||
em[NavItem.FEED] = tracks
|
||||
em
|
||||
}.stateIn(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = emptyMap<NavItem, Int>(),
|
||||
)
|
||||
val feedCounter = trackingRepository.observeUpdatedMangaCount()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, 0)
|
||||
|
||||
init {
|
||||
launchJob {
|
||||
@@ -87,8 +71,4 @@ class MainViewModel @Inject constructor(
|
||||
fun setIncognitoMode(isEnabled: Boolean) {
|
||||
settings.isIncognitoModeEnabled = isEnabled
|
||||
}
|
||||
|
||||
private fun observeNewSourcesCount() = sourcesRepository.observeNewSources()
|
||||
.map { if (sourcesRepository.isSetupRequired()) 0 else it.size }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.koitharu.kotatsu.main.ui.welcome
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.isGone
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.databinding.SheetWelcomeBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
import java.util.Locale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipClickListener, View.OnClickListener,
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
private val viewModel by viewModels<WelcomeViewModel>()
|
||||
|
||||
private val backupSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument(),
|
||||
this,
|
||||
)
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetWelcomeBinding {
|
||||
return SheetWelcomeBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetWelcomeBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.textViewWelcomeTitle.isGone = resources.getBoolean(R.bool.is_tablet)
|
||||
binding.chipsLocales.onChipClickListener = this
|
||||
binding.chipsType.onChipClickListener = this
|
||||
binding.chipBackup.setOnClickListener(this)
|
||||
binding.chipSync.setOnClickListener(this)
|
||||
|
||||
viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged)
|
||||
viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged)
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked)
|
||||
is Locale? -> viewModel.setLocaleChecked(data, chip.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.chip_backup -> {
|
||||
if (!backupSelectCall.tryLaunch(arrayOf("*/*"))) {
|
||||
Snackbar.make(
|
||||
v, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
R.id.chip_sync -> {
|
||||
val am = AccountManager.get(v.context)
|
||||
val accountType = getString(R.string.account_type_sync)
|
||||
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
if (result != null) {
|
||||
RestoreDialogFragment.show(parentFragmentManager, result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalesChanged(value: FilterProperty<Locale?>) {
|
||||
val chips = viewBinding?.chipsLocales ?: return
|
||||
chips.setChips(
|
||||
value.availableItems.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = it in value.selectedItems,
|
||||
data = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun onTypesChanged(value: FilterProperty<ContentType>) {
|
||||
val chips = viewBinding?.chipsType ?: return
|
||||
chips.setChips(
|
||||
value.availableItems.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(it.titleResId),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = it in value.selectedItems,
|
||||
data = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "WelcomeSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = WelcomeSheet().showDistinct(fm, TAG)
|
||||
|
||||
fun dismiss(fm: FragmentManager): Boolean {
|
||||
val sheet = fm.findFragmentByTag(TAG) as? WelcomeSheet ?: return false
|
||||
sheet.dismissAllowingStateLoss()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.koitharu.kotatsu.main.ui.welcome
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class WelcomeViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val allSources = repository.allMangaSources
|
||||
private val localesGroups by lazy { allSources.groupBy { it.locale?.let { x -> Locale(x) } } }
|
||||
|
||||
private var updateJob: Job
|
||||
|
||||
val locales = MutableStateFlow(
|
||||
FilterProperty<Locale?>(
|
||||
availableItems = listOf(null),
|
||||
selectedItems = setOf(null),
|
||||
isLoading = true,
|
||||
error = null,
|
||||
),
|
||||
)
|
||||
|
||||
val types = MutableStateFlow(
|
||||
FilterProperty(
|
||||
availableItems = ContentType.entries.toList(),
|
||||
selectedItems = setOf(ContentType.MANGA),
|
||||
isLoading = false,
|
||||
error = null,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
val languages = localesGroups.keys.associateBy { x -> x?.language }
|
||||
val selectedLocales = HashSet<Locale?>(2)
|
||||
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
|
||||
.firstNotNullOfOrNull { lc -> languages[lc.language] }
|
||||
selectedLocales += null
|
||||
locales.value = locales.value.copy(
|
||||
availableItems = localesGroups.keys.sortedWithSafe(nullsFirst(LocaleComparator())),
|
||||
selectedItems = selectedLocales,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocaleChecked(locale: Locale?, isChecked: Boolean) {
|
||||
val snapshot = locales.value
|
||||
locales.value = snapshot.copy(
|
||||
selectedItems = if (isChecked) {
|
||||
snapshot.selectedItems + locale
|
||||
} else {
|
||||
snapshot.selectedItems - locale
|
||||
},
|
||||
)
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTypeChecked(type: ContentType, isChecked: Boolean) {
|
||||
val snapshot = types.value
|
||||
types.value = snapshot.copy(
|
||||
selectedItems = if (isChecked) {
|
||||
snapshot.selectedItems + type
|
||||
} else {
|
||||
snapshot.selectedItems - type
|
||||
},
|
||||
)
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun commit() {
|
||||
val languages = locales.value.selectedItems.mapToSet { it?.language }
|
||||
val types = types.value.selectedItems
|
||||
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
||||
x.contentType in types && x.locale in languages
|
||||
}
|
||||
repository.setSourcesEnabledExclusive(enabledSources)
|
||||
}
|
||||
}
|
||||
@@ -119,9 +119,9 @@ class ColorFilterConfigActivity :
|
||||
.setMessage(R.string.color_correction_apply_text)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.this_manga) { _, _ ->
|
||||
viewModel.saveGlobally()
|
||||
}.setNeutralButton(R.string.globally) { _, _ ->
|
||||
viewModel.save()
|
||||
}.setNeutralButton(R.string.globally) { _, _ ->
|
||||
viewModel.saveGlobally()
|
||||
}.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ class WebtoonFrameLayout @JvmOverloads constructor(
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
|
||||
findViewById(R.id.ssiv)
|
||||
}
|
||||
private var _target: WebtoonImageView? = null
|
||||
val target: WebtoonImageView
|
||||
get() = _target ?: findViewById<WebtoonImageView?>(R.id.ssiv).also {
|
||||
_target = it
|
||||
}
|
||||
|
||||
fun dispatchVerticalScroll(dy: Int): Int {
|
||||
if (dy == 0) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
|
||||
private const val SCROLL_UNKNOWN = -1
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class WebtoonImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -18,7 +20,14 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
private val ct = PointF()
|
||||
|
||||
private var scrollPos = 0
|
||||
private var scrollRange = SCROLL_UNKNOWN
|
||||
private var debugPaint: Paint? = null
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (BuildConfig.DEBUG) {
|
||||
drawDebug(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollBy(delta: Int) {
|
||||
val maxScroll = getScrollRange()
|
||||
@@ -41,14 +50,14 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
fun getScroll() = scrollPos
|
||||
|
||||
fun getScrollRange(): Int {
|
||||
if (scrollRange == SCROLL_UNKNOWN) {
|
||||
computeScrollRange()
|
||||
if (!isReady) {
|
||||
return 0
|
||||
}
|
||||
return scrollRange.coerceAtLeast(0)
|
||||
val totalHeight = (sHeight * width / sWidth.toFloat()).roundToInt()
|
||||
return (totalHeight - height).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
scrollRange = SCROLL_UNKNOWN
|
||||
scrollPos = 0
|
||||
super.recycle()
|
||||
}
|
||||
@@ -91,8 +100,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
override fun onDownsamplingChanged() {
|
||||
super.onDownsamplingChanged()
|
||||
adjustScale()
|
||||
computeScrollRange()
|
||||
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
@@ -102,13 +109,10 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return
|
||||
|
||||
computeScrollRange()
|
||||
val container = ancestors.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return
|
||||
val parentHeight = parentHeight()
|
||||
if (scrollPos != 0 && container.bottom < parentHeight) {
|
||||
scrollTo(scrollRange)
|
||||
if (oldh != h && oldw != 0 && oldh != 0 && isReady) {
|
||||
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +124,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
setScaleAndCenter(minScale, ct)
|
||||
}
|
||||
|
||||
private fun computeScrollRange() {
|
||||
if (!isReady) {
|
||||
return
|
||||
}
|
||||
val totalHeight = (sHeight * minScale).toIntUp()
|
||||
scrollRange = (totalHeight - height).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun adjustScale() {
|
||||
minScale = width / sWidth.toFloat()
|
||||
maxScale = minScale
|
||||
@@ -137,4 +133,18 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
private fun parentHeight(): Int {
|
||||
return ancestors.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
|
||||
}
|
||||
|
||||
private fun drawDebug(canvas: Canvas) {
|
||||
val paint = debugPaint ?: Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = android.graphics.Color.RED
|
||||
strokeWidth = context.resources.resolveDp(2f)
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
textSize = context.resources.resolveDp(14f)
|
||||
debugPaint = this
|
||||
}
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawRect(1f, 1f, width.toFloat() - 1f, height.toFloat() - 1f, paint)
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawText("${getScroll()} / ${getScrollRange()}", 100f, 100f, paint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.iterator
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||
import java.util.LinkedList
|
||||
import java.util.WeakHashMap
|
||||
@@ -18,6 +17,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
|
||||
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
|
||||
private val detachedViews = WeakHashMap<View, Unit>()
|
||||
private var isFixingScroll: Boolean = false
|
||||
|
||||
override fun onChildDetachedFromWindow(child: View) {
|
||||
super.onChildDetachedFromWindow(child)
|
||||
@@ -56,6 +56,13 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
return consumedY != 0 || dy == 0
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(state: Int) {
|
||||
super.onScrollStateChanged(state)
|
||||
if (state == SCROLL_STATE_IDLE) {
|
||||
updateChildrenScroll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeVerticalScroll(dy: Int): Int {
|
||||
if (childCount == 0) {
|
||||
return 0
|
||||
@@ -106,16 +113,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun notifyScrollChanged(dy: Int) {
|
||||
updateChildrenScroll()
|
||||
val listeners = onPageScrollListeners
|
||||
if (listeners.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val centerPosition = findCenterViewPosition()
|
||||
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
|
||||
if (BuildConfig.DEBUG) {
|
||||
validateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun relayoutChildren() {
|
||||
@@ -128,29 +131,35 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun updateChildrenScroll() {
|
||||
forEach { child ->
|
||||
if (isFixingScroll) {
|
||||
return
|
||||
}
|
||||
isFixingScroll = true
|
||||
for (child in this) {
|
||||
val ssiv = (child as WebtoonFrameLayout).target
|
||||
when {
|
||||
child.top < 0 -> ssiv.scrollTo(ssiv.getScrollRange())
|
||||
child.top > 0 -> ssiv.scrollTo(0)
|
||||
else -> ssiv.scrollBy(0)
|
||||
if (adjustScroll(child, ssiv)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
isFixingScroll = false
|
||||
}
|
||||
|
||||
private fun validateLayout() {
|
||||
forEach { child ->
|
||||
val ssiv = (child as WebtoonFrameLayout).target
|
||||
val scroll = ssiv.getScroll()
|
||||
val assertion = when {
|
||||
child.top < 0 -> scroll == ssiv.getScrollRange()
|
||||
child.top > 0 -> scroll == 0
|
||||
else -> true
|
||||
}
|
||||
if (!assertion) {
|
||||
Toast.makeText(context, "Scroll = $scroll for view with top: ${child.top}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
private fun adjustScroll(child: View, ssiv: WebtoonImageView): Boolean = when {
|
||||
child.bottom < height && ssiv.getScroll() < ssiv.getScrollRange() -> {
|
||||
val distance = minOf(height - child.bottom, ssiv.getScrollRange() - ssiv.getScroll())
|
||||
scrollBy(0, -distance)
|
||||
ssiv.scrollBy(distance)
|
||||
true
|
||||
}
|
||||
|
||||
child.top > 0 && ssiv.getScroll() > 0 -> {
|
||||
val distance = minOf(child.top, ssiv.getScroll())
|
||||
scrollBy(0, distance)
|
||||
ssiv.scrollBy(-distance)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
abstract class OnPageScrollListener {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getSummary
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
@@ -14,7 +14,6 @@ import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
|
||||
|
||||
fun searchSuggestionSourceAD(
|
||||
coil: ImageLoader,
|
||||
@@ -32,15 +31,7 @@ fun searchSuggestionSourceAD(
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
binding.textViewSubtitle.text = item.source.getSummary(context)
|
||||
binding.switchLocal.isChecked = item.isEnabled
|
||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionTagsBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
|
||||
fun searchSuggestionTagsAD(
|
||||
listener: SearchSuggestionListener,
|
||||
) = adapterDelegate<SearchSuggestionItem.Tags, SearchSuggestionItem>(R.layout.item_search_suggestion_tags) {
|
||||
) = adapterDelegateViewBinding<SearchSuggestionItem.Tags, SearchSuggestionItem, ItemSearchSuggestionTagsBinding>(
|
||||
{ layoutInflater, parent -> ItemSearchSuggestionTagsBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
val chipGroup = itemView as ChipsView
|
||||
|
||||
chipGroup.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
|
||||
binding.chipsGenres.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
|
||||
listener.onTagClick(data as? MangaTag ?: return@OnChipClickListener)
|
||||
}
|
||||
|
||||
bind {
|
||||
chipGroup.setChips(item.tags)
|
||||
binding.chipsGenres.setChips(item.tags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +110,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
if (account == null) {
|
||||
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
|
||||
} else {
|
||||
try {
|
||||
startActivity(SyncSettingsIntent(account))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
startActivitySafe(SyncSettingsIntent(account))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
|
||||
private fun openLink(url: String, title: CharSequence?) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url.toUri()
|
||||
startActivity(
|
||||
startActivitySafe(
|
||||
if (title != null) {
|
||||
Intent.createChooser(intent, title)
|
||||
} else {
|
||||
|
||||
@@ -54,7 +54,11 @@ class AppBackupAgent : BackupAgent() {
|
||||
mtime: Long
|
||||
) {
|
||||
if (destination?.name?.endsWith(".bk.zip") == true) {
|
||||
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
|
||||
restoreBackupFile(
|
||||
data.fileDescriptor,
|
||||
size,
|
||||
BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)),
|
||||
)
|
||||
destination.delete()
|
||||
} else {
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime)
|
||||
@@ -69,6 +73,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
@@ -86,11 +91,12 @@ class AppBackupAgent : BackupAgent() {
|
||||
val backup = BackupZipInput(tempFile)
|
||||
try {
|
||||
runBlocking {
|
||||
backup.getEntry(BackupEntry.HISTORY)?.let { repository.restoreHistory(it) }
|
||||
backup.getEntry(BackupEntry.CATEGORIES)?.let { repository.restoreCategories(it) }
|
||||
backup.getEntry(BackupEntry.FAVOURITES)?.let { repository.restoreFavourites(it) }
|
||||
backup.getEntry(BackupEntry.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
|
||||
backup.getEntry(BackupEntry.SETTINGS)?.let { repository.restoreSettings(it) }
|
||||
backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it) }
|
||||
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) }
|
||||
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it) }
|
||||
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
|
||||
backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) }
|
||||
backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) }
|
||||
}
|
||||
} finally {
|
||||
backup.close()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_CHECKED_CHANGED
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
|
||||
class BackupEntriesAdapter(
|
||||
clickListener: OnListItemClickListener<BackupEntryModel>,
|
||||
) : BaseListAdapter<BackupEntryModel>() {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.NAV_ITEM, backupEntryAD(clickListener))
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupEntryAD(
|
||||
clickListener: OnListItemClickListener<BackupEntryModel>,
|
||||
) = adapterDelegateViewBinding<BackupEntryModel, BackupEntryModel, ItemCheckableMultipleBinding>(
|
||||
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener { v ->
|
||||
clickListener.onItemClick(item, v)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
with(binding.root) {
|
||||
setText(item.titleResId)
|
||||
setChecked(item.isChecked, PAYLOAD_CHECKED_CHANGED in payloads)
|
||||
isEnabled = item.isEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class BackupEntryModel(
|
||||
val name: BackupEntry.Name,
|
||||
val isChecked: Boolean,
|
||||
val isEnabled: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
@get:StringRes
|
||||
val titleResId: Int
|
||||
get() = when (name) {
|
||||
BackupEntry.Name.INDEX -> 0 // should not appear here
|
||||
BackupEntry.Name.HISTORY -> R.string.history
|
||||
BackupEntry.Name.CATEGORIES -> R.string.favourites_categories
|
||||
BackupEntry.Name.FAVOURITES -> R.string.favourites
|
||||
BackupEntry.Name.SETTINGS -> R.string.settings
|
||||
BackupEntry.Name.BOOKMARKS -> R.string.bookmarks
|
||||
BackupEntry.Name.SOURCES -> R.string.remote_sources
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is BackupEntryModel && other.name == name
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
if (previousState !is BackupEntryModel) {
|
||||
return null
|
||||
}
|
||||
return if (previousState.isEnabled != isEnabled) {
|
||||
ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
|
||||
} else if (previousState.isChecked != isChecked) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,21 +24,25 @@ class BackupViewModel @Inject constructor(
|
||||
init {
|
||||
launchLoadingJob {
|
||||
val file = BackupZipOutput(context).use { backup ->
|
||||
val step = 1f / 6f
|
||||
backup.put(repository.createIndex())
|
||||
|
||||
progress.value = 0f
|
||||
backup.put(repository.dumpHistory())
|
||||
|
||||
progress.value = 0.2f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpCategories())
|
||||
|
||||
progress.value = 0.4f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpFavourites())
|
||||
|
||||
progress.value = 0.6f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpBookmarks())
|
||||
|
||||
progress.value = 0.8f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpSources())
|
||||
|
||||
progress.value += step
|
||||
backup.put(repository.dumpSettings())
|
||||
|
||||
backup.finish()
|
||||
|
||||
@@ -42,6 +42,7 @@ class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
|
||||
@@ -3,40 +3,58 @@ package org.koitharu.kotatsu.settings.backup
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
||||
import org.koitharu.kotatsu.databinding.DialogRestoreBinding
|
||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
|
||||
View.OnClickListener {
|
||||
|
||||
private val viewModel: RestoreViewModel by viewModels()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = DialogProgressBinding.inflate(inflater, container, false)
|
||||
) = DialogRestoreBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewBindingCreated(binding: DialogProgressBinding, savedInstanceState: Bundle?) {
|
||||
override fun onViewBindingCreated(binding: DialogRestoreBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.textViewTitle.setText(R.string.restore_backup)
|
||||
binding.textViewSubtitle.setText(R.string.preparing_)
|
||||
|
||||
val adapter = BackupEntriesAdapter(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.buttonCancel.setOnClickListener(this)
|
||||
binding.buttonRestore.setOnClickListener(this)
|
||||
viewModel.availableEntries.observe(viewLifecycleOwner, adapter)
|
||||
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
|
||||
viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
|
||||
combine(
|
||||
viewModel.isLoading,
|
||||
viewModel.availableEntries,
|
||||
viewModel.backupDate,
|
||||
::Triple,
|
||||
).observe(viewLifecycleOwner, this::onLoadingChanged)
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
@@ -44,6 +62,40 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
.setCancelable(false)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_cancel -> dismiss()
|
||||
R.id.button_restore -> viewModel.restore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: BackupEntryModel, view: View) {
|
||||
viewModel.onItemClick(item)
|
||||
}
|
||||
|
||||
private fun onLoadingChanged(value: Triple<Boolean, List<BackupEntryModel>, Date?>) {
|
||||
val (isLoading, entries, backupDate) = value
|
||||
val hasEntries = entries.isNotEmpty()
|
||||
with(requireViewBinding()) {
|
||||
progressBar.isVisible = isLoading
|
||||
recyclerView.isGone = isLoading
|
||||
textViewSubtitle.textAndVisible =
|
||||
when {
|
||||
!isLoading -> backupDate?.formatBackupDate()
|
||||
hasEntries -> getString(R.string.processing_)
|
||||
else -> getString(R.string.loading_)
|
||||
}
|
||||
buttonRestore.isEnabled = !isLoading && entries.any { it.isChecked }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Date.formatBackupDate(): String {
|
||||
return getString(
|
||||
R.string.backup_date_,
|
||||
SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(this),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
@@ -89,6 +141,9 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
if (!result.isEmpty && !result.isAllFailed) {
|
||||
WelcomeSheet.dismiss(parentFragmentManager)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,12 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.Date
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -26,57 +30,127 @@ class RestoreViewModel @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val backupInput = SuspendLazy {
|
||||
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
|
||||
?.toUriOrNull() ?: throw FileNotFoundException()
|
||||
val contentResolver = context.contentResolver
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
BackupZipInput(tempFile)
|
||||
}
|
||||
}
|
||||
|
||||
val progress = MutableStateFlow(-1f)
|
||||
val onRestoreDone = MutableEventFlow<CompositeResult>()
|
||||
|
||||
val availableEntries = MutableStateFlow<List<BackupEntryModel>>(emptyList())
|
||||
val backupDate = MutableStateFlow<Date?>(null)
|
||||
|
||||
init {
|
||||
launchLoadingJob {
|
||||
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
|
||||
?.toUriOrNull() ?: throw FileNotFoundException()
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
val backup = runInterruptible(Dispatchers.IO) {
|
||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val backup = backupInput.get()
|
||||
val entries = backup.entries()
|
||||
availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry ->
|
||||
if (entry == BackupEntry.Name.INDEX || entry !in entries) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
BackupZipInput(tempFile)
|
||||
BackupEntryModel(
|
||||
name = entry,
|
||||
isChecked = true,
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
try {
|
||||
val result = CompositeResult()
|
||||
backupDate.value = repository.getBackupDate(backup.getEntry(BackupEntry.Name.INDEX))
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0f
|
||||
backup.getEntry(BackupEntry.HISTORY)?.let {
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
backupInput.peek()?.cleanupAsync()
|
||||
}
|
||||
|
||||
fun onItemClick(item: BackupEntryModel) {
|
||||
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
|
||||
map[item.name] = item.copy(isChecked = !item.isChecked)
|
||||
map.validate()
|
||||
availableEntries.value = map.values.sortedBy { it.name.ordinal }
|
||||
}
|
||||
|
||||
fun restore() {
|
||||
launchLoadingJob {
|
||||
val backup = backupInput.get()
|
||||
val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
|
||||
if (it.isChecked) it.name else null
|
||||
}
|
||||
val result = CompositeResult()
|
||||
val step = 1f / 6f
|
||||
|
||||
progress.value = 0f
|
||||
if (BackupEntry.Name.HISTORY in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
|
||||
result += repository.restoreHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.2f
|
||||
backup.getEntry(BackupEntry.CATEGORIES)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.CATEGORIES in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
|
||||
result += repository.restoreCategories(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.4f
|
||||
backup.getEntry(BackupEntry.FAVOURITES)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.FAVOURITES in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
|
||||
result += repository.restoreFavourites(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.6f
|
||||
backup.getEntry(BackupEntry.BOOKMARKS)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.BOOKMARKS in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
|
||||
result += repository.restoreBookmarks(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.8f
|
||||
backup.getEntry(BackupEntry.SETTINGS)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.SOURCES in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
|
||||
result += repository.restoreSources(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.SETTINGS in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.SETTINGS)?.let {
|
||||
result += repository.restoreSettings(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 1f
|
||||
onRestoreDone.call(result)
|
||||
} finally {
|
||||
backup.close()
|
||||
backup.file.delete()
|
||||
progress.value = 1f
|
||||
onRestoreDone.call(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for inconsistent user selection
|
||||
* Favorites cannot be restored without categories
|
||||
*/
|
||||
private fun MutableMap<BackupEntry.Name, BackupEntryModel>.validate() {
|
||||
val favorites = this[BackupEntry.Name.FAVOURITES] ?: return
|
||||
val categories = this[BackupEntry.Name.CATEGORIES]
|
||||
if (categories?.isChecked == true) {
|
||||
if (!favorites.isEnabled) {
|
||||
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = true)
|
||||
}
|
||||
} else {
|
||||
if (favorites.isEnabled) {
|
||||
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = false, isChecked = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard
|
||||
|
||||
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 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.core.util.ext.showAllowStateLoss
|
||||
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocaleListener
|
||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OnboardDialogFragment :
|
||||
AlertDialogFragment<DialogOnboardBinding>(),
|
||||
DialogInterface.OnClickListener, SourceLocaleListener {
|
||||
|
||||
private val viewModel by viewModels<OnboardViewModel>()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = DialogOnboardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
super.onBuildDialog(builder)
|
||||
.setPositiveButton(R.string.done, this)
|
||||
.setCancelable(false)
|
||||
builder.setTitle(R.string.welcome)
|
||||
return builder
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val adapter = SourceLocalesAdapter(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.textViewTitle.setText(R.string.onboard_text)
|
||||
viewModel.list.observe(viewLifecycleOwner, adapter)
|
||||
}
|
||||
|
||||
override fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean) {
|
||||
viewModel.setItemChecked(item.key, isChecked)
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OnboardDialog"
|
||||
|
||||
fun show(fm: FragmentManager) = OnboardDialogFragment().showAllowStateLoss(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class OnboardViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val allSources = repository.allMangaSources
|
||||
private val locales = allSources.groupBy { it.locale }
|
||||
private val selectedLocales = HashSet<String?>()
|
||||
val list = MutableStateFlow<List<SourceLocale>?>(null)
|
||||
private var updateJob: Job
|
||||
|
||||
init {
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
if (repository.isSetupRequired()) {
|
||||
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
|
||||
.firstOrNull { lc -> lc.language in locales.keys }?.language
|
||||
selectedLocales += null
|
||||
} else {
|
||||
selectedLocales.addAll(
|
||||
repository.getEnabledSources().mapNotNullToSet { x -> x.locale },
|
||||
)
|
||||
}
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setItemChecked(key: String?, isChecked: Boolean) {
|
||||
val isModified = if (isChecked) {
|
||||
selectedLocales.add(key)
|
||||
} else {
|
||||
selectedLocales.remove(key)
|
||||
}
|
||||
if (isModified) {
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun commit() {
|
||||
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
||||
x.locale in selectedLocales
|
||||
}
|
||||
repository.setSourcesEnabledExclusive(enabledSources)
|
||||
list.value = locales.map { (key, srcs) ->
|
||||
val locale = key?.let { Locale(it) }
|
||||
SourceLocale(
|
||||
key = key,
|
||||
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
|
||||
summary = srcs.joinToString { it.title },
|
||||
isChecked = key in selectedLocales,
|
||||
)
|
||||
}.sortedWithSafe(SourceLocaleComparator())
|
||||
}
|
||||
|
||||
private class SourceLocaleComparator : Comparator<SourceLocale> {
|
||||
|
||||
private val delegate = nullsFirst(LocaleComparator())
|
||||
|
||||
override fun compare(a: SourceLocale, b: SourceLocale): Int {
|
||||
val localeA = a.key?.let { Locale(it) }
|
||||
val localeB = b.key?.let { Locale(it) }
|
||||
return delegate.compare(localeA, localeB)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||
|
||||
import android.widget.CompoundButton
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
fun sourceLocaleAD(
|
||||
listener: SourceLocaleListener,
|
||||
) = adapterDelegateViewBinding<SourceLocale, SourceLocale, ItemSourceLocaleBinding>(
|
||||
{ inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
val checkedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
|
||||
listener.onItemCheckedChanged(item, isChecked)
|
||||
}
|
||||
binding.switchToggle.setOnCheckedChangeListener(checkedChangeListener)
|
||||
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
|
||||
binding.textViewDescription.textAndVisible = item.summary
|
||||
binding.switchToggle.setOnCheckedChangeListener(null)
|
||||
binding.switchToggle.setChecked(item.isChecked, payloads.isNotEmpty())
|
||||
binding.switchToggle.setOnCheckedChangeListener(checkedChangeListener)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
interface SourceLocaleListener {
|
||||
|
||||
fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
class SourceLocalesAdapter(
|
||||
listener: SourceLocaleListener,
|
||||
) : BaseListAdapter<SourceLocale>() {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(sourceLocaleAD(listener))
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import java.util.Locale
|
||||
|
||||
data class SourceLocale(
|
||||
val key: String?,
|
||||
val title: String?,
|
||||
val summary: String?,
|
||||
val isChecked: Boolean,
|
||||
) : ListModel, Comparable<SourceLocale> {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is SourceLocale && key == other.key
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is SourceLocale && previousState.isChecked != isChecked) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: SourceLocale): Int {
|
||||
return when {
|
||||
this === other -> 0
|
||||
key == Locale.getDefault().language -> -2
|
||||
key == null -> 1
|
||||
other.key == null -> -1
|
||||
else -> compareValues(title, other.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
package org.koitharu.kotatsu.settings.sources.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.SuperscriptSpan
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -19,12 +11,12 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getSummary
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
|
||||
@@ -51,15 +43,7 @@ fun sourceConfigItemCheckableDelegate(
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
binding.switchToggle.isChecked = item.isEnabled
|
||||
binding.switchToggle.isEnabled = item.isAvailable
|
||||
binding.textViewDescription.text = item.source.getSummary(context)
|
||||
@@ -101,15 +85,7 @@ fun sourceConfigItemDelegate2(
|
||||
binding.imageViewMenu.setOnClickListener(eventListener)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
||||
binding.imageViewRemove.isVisible = item.isEnabled
|
||||
binding.imageViewMenu.isVisible = item.isEnabled
|
||||
@@ -147,19 +123,6 @@ fun sourceConfigEmptySearchDelegate() =
|
||||
R.layout.item_sources_empty,
|
||||
) { }
|
||||
|
||||
fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||
ForegroundColorSpan(
|
||||
context.getThemeColor(
|
||||
com.google.android.material.R.attr.colorError,
|
||||
Color.RED,
|
||||
),
|
||||
),
|
||||
RelativeSizeSpan(0.74f),
|
||||
SuperscriptSpan(),
|
||||
) {
|
||||
append(context.getString(R.string.nsfw))
|
||||
}
|
||||
|
||||
private fun showSourceMenu(
|
||||
anchor: View,
|
||||
item: SourceConfigItem.SourceItem,
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier.Companion.ignoreCaptchaErrors
|
||||
import org.koitharu.kotatsu.core.model.getSummary
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemCatalogPageBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
|
||||
|
||||
fun sourceCatalogItemSourceAD(
|
||||
coil: ImageLoader,
|
||||
@@ -35,15 +38,7 @@ fun sourceCatalogItemSourceAD(
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.source.isNsfw()) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.textViewTitle.text = item.source.getTitle(context)
|
||||
if (item.showSummary) {
|
||||
binding.textViewDescription.text = item.source.getSummary(context)
|
||||
binding.textViewDescription.isVisible = true
|
||||
@@ -57,6 +52,7 @@ fun sourceCatalogItemSourceAD(
|
||||
placeholder(fallbackIcon)
|
||||
fallback(fallbackIcon)
|
||||
source(item.source)
|
||||
ignoreCaptchaErrors()
|
||||
enqueueWith(coil)
|
||||
}
|
||||
}
|
||||
@@ -77,3 +73,30 @@ fun sourceCatalogItemHintAD(
|
||||
binding.textSecondary.setTextAndVisible(item.text)
|
||||
}
|
||||
}
|
||||
|
||||
fun sourceCatalogPageAD(
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<SourceCatalogPage, SourceCatalogPage, ItemCatalogPageBinding>(
|
||||
{ inflater, parent -> ItemCatalogPageBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
val sourcesAdapter = SourcesCatalogAdapter(listener, coil, lifecycleOwner)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = sourcesAdapter
|
||||
}
|
||||
val insetsDelegate = WindowInsetsDelegate()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(itemView, insetsDelegate)
|
||||
itemView.addOnLayoutChangeListener(insetsDelegate)
|
||||
insetsDelegate.addInsetsListener { insets ->
|
||||
binding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom + binding.recyclerView.paddingTop,
|
||||
)
|
||||
}
|
||||
|
||||
bind {
|
||||
sourcesAdapter.items = item.items
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
|
||||
data class SourceCatalogPage(
|
||||
val type: ContentType,
|
||||
val items: List<SourceCatalogItem>,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is SourceCatalogPage && other.type == type
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
|
||||
}
|
||||
}
|
||||
@@ -10,30 +10,32 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||
import org.koitharu.kotatsu.core.util.ext.getLocaleDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
TabLayout.OnTabSelectedListener,
|
||||
OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
AppBarOwner, MenuItem.OnActionExpandListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private var newSourcesSnackbar: Snackbar? = null
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = viewBinding.appbar
|
||||
|
||||
@@ -43,19 +45,18 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
initTabs()
|
||||
val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
|
||||
with(viewBinding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = sourcesAdapter
|
||||
}
|
||||
viewModel.content.observe(this, sourcesAdapter)
|
||||
val pagerAdapter = SourcesCatalogPagerAdapter(this, coil, this)
|
||||
viewBinding.pager.adapter = pagerAdapter
|
||||
val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter)
|
||||
tabMediator.attach()
|
||||
viewModel.content.observe(this, pagerAdapter)
|
||||
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged)
|
||||
viewModel.onActionDone.observeEvent(
|
||||
this,
|
||||
ReversibleActionObserver(viewBinding.recyclerView),
|
||||
ReversibleActionObserver(viewBinding.pager),
|
||||
)
|
||||
viewModel.locale.observe(this) {
|
||||
supportActionBar?.subtitle = it.getLocaleDisplayName(this)
|
||||
supportActionBar?.subtitle = it?.toLocale().getDisplayName(this)
|
||||
}
|
||||
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
|
||||
}
|
||||
@@ -65,27 +66,15 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
viewBinding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
||||
viewModel.addSource(item.source)
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
viewModel.setContentType(tab.tag as ContentType)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
viewBinding.recyclerView.firstVisibleItemPosition = 0
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
viewBinding.tabs.isVisible = false
|
||||
viewBinding.pager.isUserInputEnabled = false
|
||||
val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty()
|
||||
viewModel.performSearch(sq)
|
||||
return true
|
||||
@@ -93,21 +82,35 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
viewBinding.tabs.isVisible = true
|
||||
viewBinding.pager.isUserInputEnabled = true
|
||||
viewModel.performSearch(null)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initTabs() {
|
||||
val tabs = viewBinding.tabs
|
||||
for (type in ContentType.entries) {
|
||||
if (viewModel.isNsfwDisabled && type == ContentType.HENTAI) {
|
||||
continue
|
||||
private fun onHasNewSourcesChanged(hasNewSources: Boolean) {
|
||||
if (hasNewSources) {
|
||||
if (newSourcesSnackbar?.isShownOrQueued == true) {
|
||||
return
|
||||
}
|
||||
val tab = tabs.newTab()
|
||||
tab.setText(type.titleResId)
|
||||
tab.tag = type
|
||||
tabs.addTab(tab)
|
||||
val snackbar = Snackbar.make(viewBinding.pager, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbar.setAction(R.string.explore) {
|
||||
NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
snackbar.addCallback(
|
||||
object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
super.onDismissed(transientBottomBar, event)
|
||||
if (event == DISMISS_EVENT_SWIPE) {
|
||||
viewModel.skipNewSources()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
snackbar.show()
|
||||
newSourcesSnackbar = snackbar
|
||||
} else {
|
||||
newSourcesSnackbar?.dismiss()
|
||||
newSourcesSnackbar = null
|
||||
}
|
||||
tabs.addOnTabSelectedListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getLocaleDisplayName
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
|
||||
class SourcesCatalogMenuProvider(
|
||||
@@ -57,15 +59,17 @@ class SourcesCatalogMenuProvider(
|
||||
}
|
||||
|
||||
private fun showLocalesMenu() {
|
||||
val locales = viewModel.locales.map {
|
||||
it to it.getLocaleDisplayName(activity)
|
||||
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)
|
||||
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(activity))
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
viewModel.setLocale(locales.getOrNull(it.order)?.first)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
|
||||
class SourcesCatalogPagerAdapter(
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : BaseListAdapter<SourceCatalogPage>(), TabLayoutMediator.TabConfigurationStrategy {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(sourceCatalogPageAD(listener, coil, lifecycleOwner))
|
||||
}
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
val item = items.getOrNull(position) ?: return
|
||||
tab.setText(item.type.titleResId)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -8,9 +9,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
@@ -21,6 +23,8 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -28,30 +32,27 @@ import javax.inject.Inject
|
||||
class SourcesCatalogViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
private val listProducerFactory: SourcesCatalogListProducer.Factory,
|
||||
settings: AppSettings,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val lifecycle = RetainedLifecycleImpl()
|
||||
private var searchQuery: String? = null
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val contentType = MutableStateFlow(ContentType.entries.first())
|
||||
val locales = repository.allMangaSources.mapToSet { it.locale }
|
||||
val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales })
|
||||
|
||||
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||
val hasNewSources = repository.observeNewSources()
|
||||
.map { it.isNotEmpty() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||
|
||||
private val listProducer: StateFlow<SourcesCatalogListProducer?> = combine(
|
||||
locale,
|
||||
contentType,
|
||||
) { lc, type ->
|
||||
listProducerFactory.create(lc, type, lifecycle).also {
|
||||
it.setQuery(searchQuery)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
private val listProducers = locale.map { lc ->
|
||||
createListProducers(lc)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value))
|
||||
|
||||
val content = listProducer.flatMapLatest {
|
||||
it?.list ?: emptyFlow()
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
val content: StateFlow<List<SourceCatalogPage>> = listProducers.flatMapLatest {
|
||||
val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } }
|
||||
combine<SourceCatalogPage, List<SourceCatalogPage>>(flows, Array<SourceCatalogPage>::toList)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
@@ -60,21 +61,36 @@ class SourcesCatalogViewModel @Inject constructor(
|
||||
|
||||
fun performSearch(query: String?) {
|
||||
searchQuery = query
|
||||
listProducer.value?.setQuery(query)
|
||||
listProducers.value.forEach { (_, v) -> v.setQuery(query) }
|
||||
}
|
||||
|
||||
fun setLocale(value: String?) {
|
||||
locale.value = value
|
||||
}
|
||||
|
||||
fun setContentType(value: ContentType) {
|
||||
contentType.value = value
|
||||
}
|
||||
|
||||
fun addSource(source: MangaSource) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val rollback = repository.setSourceEnabled(source, true)
|
||||
onActionDone.call(ReversibleAction(R.string.source_enabled, rollback))
|
||||
}
|
||||
}
|
||||
|
||||
fun skipNewSources() {
|
||||
launchJob {
|
||||
repository.assimilateNewSources()
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> {
|
||||
val types = EnumSet.allOf(ContentType::class.java)
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
types.remove(ContentType.HENTAI)
|
||||
}
|
||||
return types.associateWithTo(EnumMap(ContentType::class.java)) { type ->
|
||||
listProducerFactory.create(lc, type, lifecycle).also {
|
||||
it.setQuery(searchQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +84,14 @@ class TrackerSettingsFragment :
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
startActivity(intent)
|
||||
startActivitySafe(intent)
|
||||
true
|
||||
}
|
||||
|
||||
channels.areNotificationsDisabled -> {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", requireContext().packageName, null))
|
||||
startActivity(intent)
|
||||
startActivitySafe(intent)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.CompositeMutex2
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
@@ -13,6 +14,8 @@ import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
import org.koitharu.kotatsu.tracker.work.TrackingItem
|
||||
import javax.inject.Inject
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
class Tracker @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
@@ -77,7 +80,10 @@ class Tracker @Inject constructor(
|
||||
repository.gc()
|
||||
}
|
||||
|
||||
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates.Success {
|
||||
suspend fun fetchUpdates(
|
||||
track: MangaTracking,
|
||||
commit: Boolean
|
||||
): MangaUpdates.Success = withMangaLock(track.manga.id) {
|
||||
val repo = mangaRepositoryFactory.create(track.manga.source)
|
||||
require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
|
||||
val manga = repo.getDetails(track.manga, CachePolicy.WRITE_ONLY)
|
||||
@@ -99,7 +105,7 @@ class Tracker @Inject constructor(
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun deleteTrack(mangaId: Long) {
|
||||
suspend fun deleteTrack(mangaId: Long) = withMangaLock(mangaId) {
|
||||
repository.deleteTrack(mangaId)
|
||||
}
|
||||
|
||||
@@ -137,4 +143,21 @@ class Tracker @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private val mangaMutex = CompositeMutex2<Long>()
|
||||
|
||||
suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
mangaMutex.lock(id)
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
mangaMutex.unlock(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
private const val NO_ID = 0L
|
||||
private const val MAX_QUERY_IDS = 100
|
||||
|
||||
@Reusable
|
||||
class TrackingRepository @Inject constructor(
|
||||
@@ -65,7 +66,14 @@ class TrackingRepository @Inject constructor(
|
||||
|
||||
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
|
||||
val ids = mangaList.mapToSet { it.id }
|
||||
val tracks = db.getTracksDao().findAll(ids).groupBy { it.mangaId }
|
||||
val dao = db.getTracksDao()
|
||||
val tracks = if (ids.size <= MAX_QUERY_IDS) {
|
||||
dao.findAll(ids)
|
||||
} else {
|
||||
// TODO split tracks in the worker
|
||||
ids.windowed(MAX_QUERY_IDS, MAX_QUERY_IDS, true)
|
||||
.flatMap { dao.findAll(it) }
|
||||
}.groupBy { it.mangaId }
|
||||
val idSet = HashSet<Long>()
|
||||
val result = ArrayList<MangaTracking>(mangaList.size)
|
||||
for (item in mangaList) {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/m3ColorBottomMenuBackground" />
|
||||
</selector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/kotatsu_onSecondaryContainer" android:state_checked="true" />
|
||||
<item android:color="@color/kotatsu_secondaryContainer" android:state_checked="true" />
|
||||
<item android:color="@color/kotatsu_onSurfaceVariant" android:state_checked="false" />
|
||||
</selector>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/kotatsu_m3_bottom_menu" />
|
||||
</selector>
|
||||
131
app/src/main/res/drawable-night/avd_splash.xml
Normal file
131
app/src/main/res/drawable-night/avd_splash.xml
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="splash"
|
||||
android:width="320dp"
|
||||
android:height="320dp"
|
||||
android:viewportWidth="320"
|
||||
android:viewportHeight="320">
|
||||
<group
|
||||
android:name="scaleme"
|
||||
android:pivotX="160"
|
||||
android:pivotY="160"
|
||||
android:scaleX="1"
|
||||
android:scaleY="1">
|
||||
<group android:name="bg">
|
||||
<path
|
||||
android:name="circle"
|
||||
android:pathData="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
|
||||
android:fillColor="@color/m3_sys_color_dynamic_dark_primary_container"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="fg"
|
||||
android:pivotX="160"
|
||||
android:pivotY="160"
|
||||
android:scaleX="0"
|
||||
android:scaleY="0">
|
||||
<clip-path
|
||||
android:name="mask"
|
||||
android:pathData="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z" />
|
||||
<group
|
||||
android:name="totoro_group"
|
||||
android:scaleX="0.73903004444"
|
||||
android:scaleY="0.73903004444">
|
||||
<clip-path android:pathData="M0.81,0.07h432v432h-432z" />
|
||||
<path
|
||||
android:name="totoro"
|
||||
android:pathData="M242.26,418.58c-44.77,0 -89.54,0 -134.31,0c-6.96,0 -7.12,-0.56 -6.31,-7.57c6.75,-58.64 31.14,-110.6 61.45,-160.07c6.71,-10.95 9.45,-22.06 11.05,-34.22c1.43,-10.89 5.32,-20.99 13.4,-28.93c5.67,-5.58 9.49,-5.72 14.67,0.29c6.42,7.45 12.41,15.28 18.2,23.23c2.01,2.76 3.88,4.35 7.39,4.3c9.86,-0.15 19.72,-0.13 29.58,-0.01c3.26,0.04 5.21,-1.26 7.14,-3.9c6.02,-8.18 12.19,-16.28 18.74,-24.04c4.56,-5.41 8.36,-5.29 13.65,-0.51c8.35,7.53 11.83,17.58 13.9,28.16c1.14,5.82 1.07,11.88 2.18,17.71c0.61,3.18 2.21,6.32 3.97,9.1c19.34,30.51 35.86,62.44 48.61,96.27c8.91,23.64 15.25,47.93 18.01,73.09c0.72,6.6 0.28,7.1 -6.53,7.1C332.12,418.59 287.19,418.58 242.26,418.58zM172.46,404.96c0.23,-0.04 1.58,0.09 2.4,-0.51c8.32,-6.01 16.3,-5.48 24.88,-0.06c3.28,2.07 7.64,0.92 10.26,-2.87c2.4,-3.47 2.93,-7.99 -0.37,-10.11c-6.96,-4.48 -14.69,-7.79 -22.16,-11.45c-0.71,-0.35 -1.97,0.02 -2.8,0.42c-6.1,2.95 -12.31,5.72 -18.16,9.11c-3.47,2.01 -4.86,5.62 -3.44,9.68C164.45,403.11 167.3,405.15 172.46,404.96zM218.13,370.48c-0.72,7.32 5.26,12.42 10.7,10.65c3.5,-1.14 6.64,-3.36 10.15,-4.45c2.19,-0.68 4.93,-0.73 7.1,-0.05c3.5,1.09 6.63,3.81 10.14,4.31c2.71,0.39 6.58,-0.8 8.42,-2.72c1.69,-1.77 2.38,-5.74 1.7,-8.26c-1.66,-6.13 -21.82,-15 -27.76,-12.78c-0.3,0.11 -0.6,0.25 -0.88,0.39c-4.76,2.37 -9.72,4.42 -14.2,7.23C220.99,366.38 219.28,369.23 218.13,370.48zM312.12,405.04c5.83,-0.02 8.75,-2.02 9.96,-6c1.29,-4.24 -0.29,-7.83 -4,-9.92c-5.46,-3.07 -11.09,-5.89 -16.87,-8.3c-1.97,-0.82 -4.88,-0.73 -6.87,0.1c-5.49,2.28 -10.81,5.01 -16.03,7.87c-4.63,2.54 -6.06,7.19 -3.97,11.49c2.04,4.18 6.73,5.95 11.55,4.09c2.69,-1.04 5.36,-2.29 7.8,-3.82c3.12,-1.96 5.81,-1.75 8.95,0.07C306.06,402.61 309.82,403.99 312.12,405.04zM186.6,262.09c0.05,8.66 5.72,15.1 14.55,16.52c7.21,1.16 15.06,-3.96 17.57,-11.47c2.13,-6.36 0.68,-9.3 -5.95,-10.15c-3.72,-0.47 -4.65,-1.86 -5.04,-5.35c-0.62,-5.66 -3.07,-7 -8.67,-5.72C191.46,247.66 186.55,254.03 186.6,262.09zM282.02,278.75c7.26,0.03 14.41,-5.46 16.13,-12.38c1.46,-5.89 0.26,-8.38 -5.73,-9.23c-4.07,-0.58 -5.45,-2.48 -5.77,-6.12c-0.33,-3.76 -2.15,-6.05 -6.28,-5.61c-8.6,0.91 -15.51,8.95 -14.97,17.39C265.96,271.4 273.59,278.72 282.02,278.75zM259.29,278.52c-0.09,-5.33 -3.97,-9.16 -8.9,-9.28c-5.17,-0.13 -10.35,0 -15.52,-0.04c-3.82,-0.03 -6.54,1.76 -8.08,5.12c-1.56,3.4 -0.95,6.7 1.57,9.44c2.62,2.85 5.33,5.66 8.23,8.22c3.42,3.02 7.46,3.55 11.05,0.59c3.59,-2.97 6.8,-6.44 9.9,-9.93C258.63,281.4 258.91,279.45 259.29,278.52z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="@color/m3_sys_color_dynamic_dark_primary"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1"
|
||||
tools:targetApi="s" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="circle">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:valueFrom="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
|
||||
android:valueTo="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator" />
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="200"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="totoro_group">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:propertyName="translateY"
|
||||
android:valueFrom="160"
|
||||
android:valueTo="0"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
<objectAnimator
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="0.73903004444"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="totoro">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="200"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="fg">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -1,64 +1,131 @@
|
||||
<animated-vector
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="288dp"
|
||||
android:height="288dp"
|
||||
android:viewportWidth="288"
|
||||
android:viewportHeight="288">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="@color/splash_primary"
|
||||
android:pathData="M 144 121.499 C 121.258 121.499 102.75 141.685 102.75 166.499 C 102.75 191.314 121.258 211.499 144 211.499 C 166.741 211.499 185.25 191.314 185.25 166.499 C 185.25 141.685 166.742 121.499 144 121.499 Z M 128.604 156.927 C 127.945 158.238 126.619 159 125.25 159 C 124.686 159 124.114 158.875 123.572 158.604 L 117.749 155.689 L 111.926 158.604 C 110.066 159.534 107.825 158.772 106.894 156.927 C 105.972 155.074 106.719 152.825 108.572 151.895 L 116.072 148.145 C 117.126 147.617 118.372 147.617 119.426 148.145 L 126.926 151.895 C 128.78 152.825 129.527 155.074 128.604 156.927 Z M 154.854 145.677 C 154.195 146.988 152.869 147.75 151.5 147.75 C 150.936 147.75 150.364 147.625 149.822 147.354 L 144 144.439 L 138.177 147.354 C 136.316 148.284 134.075 147.522 133.145 145.677 C 132.222 143.824 132.969 141.575 134.822 140.645 L 142.322 136.895 C 143.377 136.367 144.622 136.367 145.677 136.895 L 153.177 140.645 C 155.03 141.575 155.777 143.824 154.854 145.677 Z M 179.427 151.895 C 181.28 152.825 182.027 155.074 181.104 156.927 C 180.445 158.238 179.119 159 177.75 159 C 177.186 159 176.615 158.875 176.072 158.604 L 170.25 155.689 L 164.427 158.604 C 162.574 159.534 160.325 158.772 159.395 156.927 C 158.472 155.074 159.219 152.825 161.072 151.895 L 168.572 148.145 C 169.627 147.617 170.872 147.617 171.927 148.145 Z"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillAlpha="0"
|
||||
android:fillColor="@color/splash_primary"
|
||||
android:pathData="M 192.709 104.964 C 191.142 101.857 189.595 98.926 188.115 96.247 L 184.395 89.731 C 181.336 84.548 178.879 80.775 177.742 79.082 C 177.42 59.584 169.136 54.875 168.177 54.391 C 166.734 53.681 164.984 53.952 163.848 55.094 C 158.333 60.61 154.744 66.103 153.045 68.996 L 134.954 68.996 C 133.255 66.103 129.666 60.61 124.151 55.094 C 123.016 53.952 121.272 53.681 119.822 54.391 C 118.863 54.875 110.579 59.584 110.257 79.082 C 109.12 80.775 106.663 84.547 103.605 89.731 L 99.882 96.252 C 98.401 98.933 96.853 101.864 95.285 104.974 L 91.935 111.87 C 83.959 128.994 76.5 150.313 76.5 170.248 C 76.5 201.442 102.72 228.037 103.841 229.151 C 104.251 229.561 104.757 229.876 105.306 230.059 C 105.79 230.22 117.362 233.999 136.5 233.999 C 137.496 233.999 138.448 233.604 139.151 232.901 L 141.803 230.249 L 146.197 230.249 L 148.849 232.901 C 149.552 233.604 150.504 233.999 151.5 233.999 C 170.639 233.999 182.211 230.22 182.694 230.059 C 183.244 229.876 183.749 229.561 184.159 229.151 C 185.279 228.037 211.5 201.443 211.5 170.248 C 211.5 150.313 204.041 128.994 196.064 111.87 Z M 162.75 83.999 C 164.822 83.999 164.735 87.309 164.735 89.382 C 166.808 89.382 170.25 89.427 170.25 91.5 C 170.25 95.638 166.888 98.999 162.75 98.999 C 158.612 98.999 155.25 95.638 155.25 91.5 C 155.25 87.361 158.612 83.999 162.75 83.999 Z M 136.785 97.564 C 137.364 96.165 138.734 95.25 140.25 95.25 L 147.75 95.25 C 149.266 95.25 150.635 96.165 151.214 97.564 C 151.793 98.963 151.47 100.582 150.401 101.651 L 146.651 105.401 C 145.918 106.133 144.959 106.5 143.999 106.5 C 143.04 106.5 142.08 106.133 141.348 105.401 L 137.597 101.651 C 136.529 100.582 136.207 98.963 136.785 97.564 Z M 125.25 83.999 C 127.323 83.999 127.235 87.309 127.235 89.382 C 129.308 89.382 132.75 89.427 132.75 91.5 C 132.75 95.638 129.388 98.999 125.25 98.999 C 121.112 98.999 117.75 95.637 117.75 91.5 C 117.75 87.361 121.112 83.999 125.25 83.999 Z M 144 218.999 C 117.12 218.999 95.25 195.445 95.25 166.499 C 95.25 137.554 117.12 114 144 114 C 170.88 114 192.75 137.554 192.75 166.499 C 192.75 195.445 170.88 218.999 144 218.999 Z"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:fillAlpha="0"
|
||||
android:fillColor="#ef5350"
|
||||
android:pathData="M 160.739 158.44 C 158.668 157.198 158.323 154.077 160.084 152.508 C 160.329 152.29 162.49 151.126 164.885 149.923 C 168.283 148.216 169.46 147.734 170.24 147.734 C 171.02 147.734 172.231 148.231 175.782 150.008 C 181.059 152.65 181.457 153.016 181.457 155.234 C 181.457 156.765 181.009 157.577 179.655 158.499 C 178.268 159.443 177.207 159.257 173.531 157.425 L 170.292 155.811 L 166.974 157.429 C 163.198 159.271 162.354 159.408 160.739 158.44 Z M 134.489 147.254 C 132.75 146.194 132.17 143.625 133.288 141.937 C 134.027 140.82 142.612 136.429 144.055 136.429 C 144.759 136.429 146.131 136.998 149.552 138.71 C 153.393 140.632 154.194 141.125 154.638 141.844 C 155.428 143.122 155.516 144.373 154.903 145.613 C 154.216 147.001 153.386 147.558 151.813 147.689 C 150.606 147.789 150.298 147.688 147.211 146.177 L 143.902 144.559 L 140.733 146.146 C 137.176 147.929 135.951 148.146 134.489 147.254 Z M 108.448 158.594 C 107.987 158.349 107.357 157.781 107.048 157.331 C 106.572 156.639 106.5 156.284 106.578 155.015 C 106.709 152.893 107.213 152.446 112.383 149.872 C 115.854 148.143 116.881 147.734 117.748 147.734 C 118.625 147.734 119.651 148.152 123.361 150.023 C 127.26 151.989 127.981 152.44 128.46 153.211 C 130.051 155.77 128.211 159.04 125.18 159.04 C 124.616 159.04 123.247 158.499 121.093 157.425 L 117.853 155.811 L 114.536 157.429 C 110.902 159.201 109.938 159.386 108.448 158.594 Z"
|
||||
android:strokeWidth="1" />
|
||||
android:name="splash"
|
||||
android:width="320dp"
|
||||
android:height="320dp"
|
||||
android:viewportWidth="320"
|
||||
android:viewportHeight="320">
|
||||
<group
|
||||
android:name="scaleme"
|
||||
android:pivotX="160"
|
||||
android:pivotY="160"
|
||||
android:scaleX="1"
|
||||
android:scaleY="1">
|
||||
<group android:name="bg">
|
||||
<path
|
||||
android:name="circle"
|
||||
android:pathData="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
|
||||
android:fillColor="@color/m3_sys_color_dynamic_light_primary_container"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="fg"
|
||||
android:pivotX="160"
|
||||
android:pivotY="160"
|
||||
android:scaleX="0"
|
||||
android:scaleY="0">
|
||||
<clip-path
|
||||
android:name="mask"
|
||||
android:pathData="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z" />
|
||||
<group
|
||||
android:name="totoro_group"
|
||||
android:scaleX="0.73903004444"
|
||||
android:scaleY="0.73903004444">
|
||||
<clip-path android:pathData="M0.81,0.07h432v432h-432z" />
|
||||
<path
|
||||
android:name="totoro"
|
||||
android:pathData="M242.26,418.58c-44.77,0 -89.54,0 -134.31,0c-6.96,0 -7.12,-0.56 -6.31,-7.57c6.75,-58.64 31.14,-110.6 61.45,-160.07c6.71,-10.95 9.45,-22.06 11.05,-34.22c1.43,-10.89 5.32,-20.99 13.4,-28.93c5.67,-5.58 9.49,-5.72 14.67,0.29c6.42,7.45 12.41,15.28 18.2,23.23c2.01,2.76 3.88,4.35 7.39,4.3c9.86,-0.15 19.72,-0.13 29.58,-0.01c3.26,0.04 5.21,-1.26 7.14,-3.9c6.02,-8.18 12.19,-16.28 18.74,-24.04c4.56,-5.41 8.36,-5.29 13.65,-0.51c8.35,7.53 11.83,17.58 13.9,28.16c1.14,5.82 1.07,11.88 2.18,17.71c0.61,3.18 2.21,6.32 3.97,9.1c19.34,30.51 35.86,62.44 48.61,96.27c8.91,23.64 15.25,47.93 18.01,73.09c0.72,6.6 0.28,7.1 -6.53,7.1C332.12,418.59 287.19,418.58 242.26,418.58zM172.46,404.96c0.23,-0.04 1.58,0.09 2.4,-0.51c8.32,-6.01 16.3,-5.48 24.88,-0.06c3.28,2.07 7.64,0.92 10.26,-2.87c2.4,-3.47 2.93,-7.99 -0.37,-10.11c-6.96,-4.48 -14.69,-7.79 -22.16,-11.45c-0.71,-0.35 -1.97,0.02 -2.8,0.42c-6.1,2.95 -12.31,5.72 -18.16,9.11c-3.47,2.01 -4.86,5.62 -3.44,9.68C164.45,403.11 167.3,405.15 172.46,404.96zM218.13,370.48c-0.72,7.32 5.26,12.42 10.7,10.65c3.5,-1.14 6.64,-3.36 10.15,-4.45c2.19,-0.68 4.93,-0.73 7.1,-0.05c3.5,1.09 6.63,3.81 10.14,4.31c2.71,0.39 6.58,-0.8 8.42,-2.72c1.69,-1.77 2.38,-5.74 1.7,-8.26c-1.66,-6.13 -21.82,-15 -27.76,-12.78c-0.3,0.11 -0.6,0.25 -0.88,0.39c-4.76,2.37 -9.72,4.42 -14.2,7.23C220.99,366.38 219.28,369.23 218.13,370.48zM312.12,405.04c5.83,-0.02 8.75,-2.02 9.96,-6c1.29,-4.24 -0.29,-7.83 -4,-9.92c-5.46,-3.07 -11.09,-5.89 -16.87,-8.3c-1.97,-0.82 -4.88,-0.73 -6.87,0.1c-5.49,2.28 -10.81,5.01 -16.03,7.87c-4.63,2.54 -6.06,7.19 -3.97,11.49c2.04,4.18 6.73,5.95 11.55,4.09c2.69,-1.04 5.36,-2.29 7.8,-3.82c3.12,-1.96 5.81,-1.75 8.95,0.07C306.06,402.61 309.82,403.99 312.12,405.04zM186.6,262.09c0.05,8.66 5.72,15.1 14.55,16.52c7.21,1.16 15.06,-3.96 17.57,-11.47c2.13,-6.36 0.68,-9.3 -5.95,-10.15c-3.72,-0.47 -4.65,-1.86 -5.04,-5.35c-0.62,-5.66 -3.07,-7 -8.67,-5.72C191.46,247.66 186.55,254.03 186.6,262.09zM282.02,278.75c7.26,0.03 14.41,-5.46 16.13,-12.38c1.46,-5.89 0.26,-8.38 -5.73,-9.23c-4.07,-0.58 -5.45,-2.48 -5.77,-6.12c-0.33,-3.76 -2.15,-6.05 -6.28,-5.61c-8.6,0.91 -15.51,8.95 -14.97,17.39C265.96,271.4 273.59,278.72 282.02,278.75zM259.29,278.52c-0.09,-5.33 -3.97,-9.16 -8.9,-9.28c-5.17,-0.13 -10.35,0 -15.52,-0.04c-3.82,-0.03 -6.54,1.76 -8.08,5.12c-1.56,3.4 -0.95,6.7 1.57,9.44c2.62,2.85 5.33,5.66 8.23,8.22c3.42,3.02 7.46,3.55 11.05,0.59c3.59,-2.97 6.8,-6.44 9.9,-9.93C258.63,281.4 258.91,279.45 259.29,278.52z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="@color/m3_sys_color_dynamic_light_primary"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1"
|
||||
tools:targetApi="s" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_2">
|
||||
<target android:name="circle">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:valueFrom="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
|
||||
android:valueTo="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator" />
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="200"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_1">
|
||||
<target android:name="totoro_group">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"
|
||||
android:propertyName="fillAlpha"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:propertyName="translateY"
|
||||
android:valueFrom="160"
|
||||
android:valueTo="0"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
<objectAnimator
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="0.73903004444"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path">
|
||||
<target android:name="totoro">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="800"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 144 165.978 C 143.736 165.978 143.522 166.212 143.522 166.499 C 143.522 166.787 143.736 167.021 144 167.021 C 144.263 167.021 144.478 166.787 144.478 166.499 C 144.478 166.212 144.263 165.978 144 165.978 Z M 143.821 166.389 C 143.813 166.404 143.798 166.413 143.783 166.413 C 143.776 166.413 143.769 166.412 143.763 166.408 L 143.696 166.374 L 143.628 166.408 C 143.607 166.418 143.581 166.41 143.57 166.388 C 143.559 166.367 143.568 166.341 143.589 166.33 L 143.676 166.286 C 143.688 166.28 143.703 166.28 143.715 166.286 L 143.802 166.33 C 143.823 166.341 143.832 166.367 143.821 166.388 Z M 144.126 166.258 C 144.118 166.273 144.103 166.282 144.087 166.282 C 144.08 166.282 144.074 166.281 144.067 166.277 L 144 166.244 L 143.932 166.277 C 143.911 166.288 143.885 166.279 143.874 166.258 C 143.863 166.236 143.872 166.21 143.893 166.2 L 143.98 166.156 C 143.992 166.15 144.007 166.15 144.019 166.156 L 144.106 166.2 C 144.128 166.21 144.136 166.236 144.125 166.258 Z M 144.41 166.33 C 144.432 166.341 144.44 166.367 144.43 166.389 C 144.422 166.404 144.407 166.413 144.391 166.413 C 144.384 166.413 144.378 166.412 144.371 166.408 L 144.304 166.374 L 144.236 166.408 C 144.215 166.418 144.189 166.41 144.178 166.388 C 144.167 166.367 144.176 166.341 144.197 166.33 L 144.284 166.286 C 144.297 166.28 144.311 166.28 144.323 166.286 Z"
|
||||
android:valueTo="M 144 121.499 C 121.258 121.499 102.75 141.685 102.75 166.499 C 102.75 191.314 121.258 211.499 144 211.499 C 166.741 211.499 185.25 191.314 185.25 166.499 C 185.25 141.685 166.742 121.499 144 121.499 Z M 128.604 156.927 C 127.945 158.238 126.619 159 125.25 159 C 124.686 159 124.114 158.875 123.572 158.604 L 117.749 155.689 L 111.926 158.604 C 110.066 159.534 107.825 158.772 106.894 156.927 C 105.972 155.074 106.719 152.825 108.572 151.895 L 116.072 148.145 C 117.126 147.617 118.372 147.617 119.426 148.145 L 126.926 151.895 C 128.78 152.825 129.527 155.074 128.604 156.927 Z M 154.854 145.677 C 154.195 146.988 152.869 147.75 151.5 147.75 C 150.936 147.75 150.364 147.625 149.822 147.354 L 144 144.439 L 138.177 147.354 C 136.316 148.284 134.075 147.522 133.145 145.677 C 132.222 143.824 132.969 141.575 134.822 140.645 L 142.322 136.895 C 143.377 136.367 144.622 136.367 145.677 136.895 L 153.177 140.645 C 155.03 141.575 155.777 143.824 154.854 145.677 Z M 179.427 151.895 C 181.28 152.825 182.027 155.074 181.104 156.927 C 180.445 158.238 179.119 159 177.75 159 C 177.186 159 176.615 158.875 176.072 158.604 L 170.25 155.689 L 164.427 158.604 C 162.574 159.534 160.325 158.772 159.395 156.927 C 158.472 155.074 159.219 152.825 161.072 151.895 L 168.572 148.145 C 169.627 147.617 170.872 147.617 171.927 148.145 Z"
|
||||
android:valueType="pathType" />
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="200"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="fg">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="200"
|
||||
android:duration="300"
|
||||
android:valueFrom="0.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
|
||||
11
app/src/main/res/drawable/ic_welcome.xml
Normal file
11
app/src/main/res/drawable/ic_welcome.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12 2C13.1 2 14 2.9 14 4S13.1 6 12 6 10 5.1 10 4 10.9 2 12 2M15.9 8.1C15.5 7.7 14.8 7 13.5 7H11C8.2 7 6 4.8 6 2H4C4 5.2 6.1 7.8 9 8.7V22H11V16H13V22H15V10.1L19 14L20.4 12.6L15.9 8.1Z" />
|
||||
</vector>
|
||||
@@ -11,7 +11,8 @@
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
app:liftOnScroll="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
@@ -27,21 +28,10 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_list"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_source_config" />
|
||||
|
||||
</FrameLayout>
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
78
app/src/main/res/layout/dialog_restore.xml
Normal file
78
app/src/main/res/layout/dialog_restore.xml
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="?dialogPreferredPadding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:paddingBottom="@dimen/margin_normal"
|
||||
android:text="@string/restore_backup"
|
||||
android:textAppearance="?textAppearanceTitleLarge" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:indeterminate="true"
|
||||
android:max="100"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:scrollIndicators="top|bottom"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="6"
|
||||
tools:listitem="@layout/item_checkable_multiple"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[10]" />
|
||||
|
||||
<LinearLayout
|
||||
style="?buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
style="?buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/cancel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_restore"
|
||||
style="?buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/restore" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,165 +1,197 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollIndicators="bottom">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?colorSecondaryContainer"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,13:18"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||
tools:background="@tools:sample/backgrounds/scenic[5]"
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_open"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="16dp"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
android:paddingBottom="?actionBarSize">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_open"
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?colorSecondaryContainer"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,13:18"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||
tools:background="@tools:sample/backgrounds/scenic[5]"
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_open"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="16dp"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_open"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/details"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_expand" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/close"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="?actionModeCloseDrawable"
|
||||
app:tint="?colorControlNormal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||
tools:text="@tools:sample/lorem[12]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/list_selector"
|
||||
android:padding="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorTertiary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/rating_bar"
|
||||
style="?ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:isIndicator="true"
|
||||
android:max="1"
|
||||
android:numStars="5"
|
||||
android:stepSize="0.5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_author"
|
||||
tools:rating="4" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:barrierMargin="8dp"
|
||||
app:constraint_referenced_ids="imageView_cover,rating_bar" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
|
||||
android:id="@+id/textView_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginEnd="@dimen/margin_normal"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:paddingBottom="@dimen/margin_normal"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/chips_tags"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?colorBackgroundFloating">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_read"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/details"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_expand" />
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||
android:enabled="false"
|
||||
android:text="@string/read"
|
||||
android:textAllCaps="false"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
tools:enabled="true"
|
||||
tools:icon="@drawable/ic_read" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/close"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="?actionModeCloseDrawable"
|
||||
app:tint="?colorControlNormal" />
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||
tools:text="@tools:sample/lorem[12]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/list_selector"
|
||||
android:padding="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorTertiary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/rating_bar"
|
||||
style="?ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:isIndicator="true"
|
||||
android:max="1"
|
||||
android:numStars="5"
|
||||
android:stepSize="0.5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_author"
|
||||
tools:rating="4" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:barrierMargin="8dp"
|
||||
app:constraint_referenced_ids="imageView_cover,rating_bar" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
|
||||
android:id="@+id/textView_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginEnd="@dimen/margin_normal"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:paddingBottom="@dimen/margin_normal"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/chips_tags"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
19
app/src/main/res/layout/item_catalog_page.xml
Normal file
19
app/src/main/res/layout/item_catalog_page.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layout_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_source_config" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,34 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<HorizontalScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="8dp"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
android:scrollbars="none">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_create"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/create_category"
|
||||
app:icon="@drawable/ic_add" />
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
app:singleLine="true">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_manage"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/manage"
|
||||
app:icon="@drawable/ic_edit" />
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_create"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/create_category"
|
||||
app:chipIcon="@drawable/ic_add" />
|
||||
|
||||
</LinearLayout>
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_manage"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/manage_categories"
|
||||
app:chipIcon="@drawable/ic_edit" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
<HorizontalScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="6dp" />
|
||||
android:scrollbars="none">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
app:singleLine="true" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
android:scrollIndicators="top">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
@@ -114,10 +115,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:drawablePadding="16dp"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:paddingVertical="8dp"
|
||||
android:textAppearance="?textAppearanceBodySmall"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_error_small"
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_input_layout"
|
||||
style="?textInputFilledDenseStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:startIconDrawable="?android:actionModeWebSearchDrawable"
|
||||
app:endIconMode="clear_text"
|
||||
app:endIconDrawable="@drawable/abc_ic_clear_material"
|
||||
android:hint="@string/genres_search_hint"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:hint="@string/genres_search_hint"
|
||||
app:endIconDrawable="@drawable/abc_ic_clear_material"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@id/recyclerView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerBar"
|
||||
app:layout_constraintBottom_toTopOf="@id/recyclerView">
|
||||
app:startIconDrawable="?android:actionModeWebSearchDrawable">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_search"
|
||||
@@ -36,7 +37,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionSearch|flagNoFullscreen"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"/>
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
117
app/src/main/res/layout/sheet_welcome.xml
Normal file
117
app/src/main/res/layout/sheet_welcome.xml
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/welcome" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollIndicators="top">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_welcome_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/welcome"
|
||||
android:textAppearance="?textAppearanceTitleLarge"
|
||||
android:drawablePadding="16dp"
|
||||
app:drawableStartCompat="@drawable/ic_welcome" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
app:singleLine="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_backup"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/restore_backup"
|
||||
app:chipIcon="@drawable/ic_backup_restore" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_sync"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sync_auth"
|
||||
app:chipIcon="@drawable/ic_sync" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:text="@string/welcome_text"
|
||||
android:textAppearance="?textAppearanceBodyMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_locales_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/languages"
|
||||
android:textAppearance="?textAppearanceTitleSmall" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_locales"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_type_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/type"
|
||||
android:textAppearance="?textAppearanceTitleSmall" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
@@ -178,4 +178,7 @@
|
||||
<string name="system_default">الافتراضي</string>
|
||||
<string name="sign_in">تسجبل الدخول</string>
|
||||
<string name="domain">المجال</string>
|
||||
<string name="text_history_holder_primary">كل ما تقرأه سيعرض هنا</string>
|
||||
<string name="text_search_holder_secondary">حاول إعادة صياغة الكلمات.</string>
|
||||
<string name="text_empty_holder_primary">يبدو أنه فارغ…</string>
|
||||
</resources>
|
||||
@@ -538,4 +538,9 @@
|
||||
<string name="skip">Прапусціць</string>
|
||||
<string name="color_correction_apply_text">Гэтыя налады могуць прымяняцца глабальна або толькі да бягучай мангі. Пры глабальным прымяненні індывідуальныя налады не будуць перавызначаны.</string>
|
||||
<string name="grayscale">Адценні шэрага</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Можа дапамагчы з пачаткам загрузкі, калі ў вас узнікаюць з ёй праблемы</string>
|
||||
<string name="welcome_text">Выберыце, якія крыніцы змесціва вы хочаце ўключыць. Гэта таксама можна наладзіць пазней у наладах</string>
|
||||
<string name="restore">Аднавіць</string>
|
||||
<string name="backup_date_">Дата стварэння рэзервовай копіі: %s</string>
|
||||
<string name="sync_auth">Увайдзіце, каб сінхранізаваць уліковы запіс</string>
|
||||
</resources>
|
||||
@@ -538,4 +538,11 @@
|
||||
<string name="error_filter_states_genre_not_supported">Esta fuente no permite filtrar por géneros ni por estados</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Esta fuente no permite filtrar por géneros ni por configuración regional</string>
|
||||
<string name="genres_search_hint">Empieza a escribir el nombre del género</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Puede que te ayude a iniciar la descarga si tienes algún problema</string>
|
||||
<string name="welcome_text">Por favor, seleccione las fuentes de contenido que desea habilitar. Esto también se puede configurar más tarde en la configuración</string>
|
||||
<string name="restore">Restaurar</string>
|
||||
<string name="backup_date_">Fecha de la copia de seguridad: %s</string>
|
||||
<string name="sync_auth">Iniciar sesión en la cuenta de sincronización</string>
|
||||
<string name="by_name_reverse">Nombre invertido</string>
|
||||
<string name="state_upcoming">Próximamente</string>
|
||||
</resources>
|
||||
2
app/src/main/res/values-et/plurals.xml
Normal file
2
app/src/main/res/values-et/plurals.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
413
app/src/main/res/values-et/strings.xml
Normal file
413
app/src/main/res/values-et/strings.xml
Normal file
@@ -0,0 +1,413 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="updates">Uuendused</string>
|
||||
<string name="languages">Keel</string>
|
||||
<string name="status_re_reading">Uuesti loen</string>
|
||||
<string name="add_to_favourites">Lisa lemmikuks</string>
|
||||
<string name="detect_reader_mode">Automaatne lugeja mood</string>
|
||||
<string name="restore_backup_description">Impordi varem tehtud tagavara oma kasutaja andmetest</string>
|
||||
<string name="manga_shelf">Riiul</string>
|
||||
<string name="download_started">Allalaadimine alustatud</string>
|
||||
<string name="tracking">Jälgimine</string>
|
||||
<string name="close">Sulge</string>
|
||||
<string name="progress">Progress</string>
|
||||
<string name="text_history_holder_secondary">Leia mida lugeda «Seiklemis» osakonnas</string>
|
||||
<string name="cancel_all">Tühista kõik</string>
|
||||
<string name="sync_host_description">Saate kasutada ise korraldatud sünkroonimisserverit või tava sünkroonimisserverit. Ärge muutke seda, kui te ei ole kindel, mida teete.</string>
|
||||
<string name="all_favourites">Kõik lemmikud</string>
|
||||
<string name="email_enter_hint">Sisesta oma emaili aadress et jätkata</string>
|
||||
<string name="save">Salvesta</string>
|
||||
<string name="light_indicator">LED indikaator</string>
|
||||
<string name="no_chapters">Pole peatükke</string>
|
||||
<string name="related_manga_summary">Näita seotud mangade nimekirja. Mõnel juhul võib see olla ebatäpne või puududa</string>
|
||||
<string name="theme_name_dynamic">Dünaamiline</string>
|
||||
<string name="text_clear_cookies_prompt">Teid logitakse välja kõikidest allikatest</string>
|
||||
<string name="clear_cookies">Tühjenda küpsisesd</string>
|
||||
<string name="favourites_categories">Lemmikud kategooriad</string>
|
||||
<string name="pages_cache">Lehekülgede vahemälu</string>
|
||||
<string name="local_storage">Kohalik salvestamine</string>
|
||||
<string name="filter">Filterid</string>
|
||||
<string name="enabled_sources">Kasutatud allikad</string>
|
||||
<string name="reset">Nulli</string>
|
||||
<string name="disable_all">Lülita kõik välja</string>
|
||||
<string name="error_occurred">Tekkis viga</string>
|
||||
<string name="gestures_only">Ainult žestid</string>
|
||||
<string name="order_added">Lisatud</string>
|
||||
<string name="enable_logging">Lülita sisse loggig</string>
|
||||
<string name="clear_thumbs_cache">Tühjenda pisipildi vahemälu</string>
|
||||
<string name="taps_on_edges">Külje puudutused</string>
|
||||
<string name="switch_pages">Vaheta lehekülge</string>
|
||||
<string name="settings_apply_restart_required">Muudatuste rakendamiseks käivitage rakendus uuesti</string>
|
||||
<string name="rotate_screen">Keera ekraani</string>
|
||||
<string name="text_clear_updates_feed_prompt">Tühjenda kõik uuenduste ajalugu?</string>
|
||||
<string name="source_disabled">Allikas välja lülitatud</string>
|
||||
<string name="clear_cookies_summary">Võib aidata mõndade probleemidega. Kõik autoriseerimine saab invalideeritud</string>
|
||||
<string name="enable_logging_summary">Salvesta mõned tegevused silumiseks. Ärge lülitage seda sisse, kui te ei ole kindel, mida teete</string>
|
||||
<string name="suggestions_enable">Lülita sisse soovitused</string>
|
||||
<string name="clear_feed">Tühjenda voog</string>
|
||||
<string name="welcome">Tere tulemast</string>
|
||||
<string name="no_description">Kirjeldus puudub</string>
|
||||
<string name="about_app_translation_summary">Tõlgi see äppi</string>
|
||||
<string name="text_shelf_holder_secondary">Leia mida lugeda «Seiklemis» osakonnas</string>
|
||||
<string name="chapters_empty">Selles mangas ei ole peatükke</string>
|
||||
<string name="remove">Eemalda</string>
|
||||
<string name="vibration">Värisemine</string>
|
||||
<string name="no_update_available">Uuendusi pole saadaval</string>
|
||||
<string name="remove_category">Eemalda</string>
|
||||
<string name="clear_all_history">Tühjenda kõik ajalugu</string>
|
||||
<string name="preload_pages">Lehekülgede eellaadimine</string>
|
||||
<string name="data_deletion">Andmete kustutamine</string>
|
||||
<string name="favourites">Lemmikud</string>
|
||||
<string name="history_shortcuts">Näita hiljutise manga lühitee</string>
|
||||
<string name="downloads_wifi_only_summary">Peata allalaadimised kui vahetad mobiilsele ühendusele</string>
|
||||
<string name="read_mode">Lugemis mood</string>
|
||||
<string name="internal_storage">Sisemine mälu</string>
|
||||
<string name="import_completed">Importitud</string>
|
||||
<string name="different_languages">Erinevad keeled</string>
|
||||
<string name="show_reading_indicators">Näita lugemis progressi näitaja</string>
|
||||
<string name="read_later">Loe hiljem</string>
|
||||
<string name="backup_saved">Tagavara salvestatud</string>
|
||||
<string name="local_manga_processing">Töötlen salvestatud manga</string>
|
||||
<string name="user_agent">UserAgent pealkiri</string>
|
||||
<string name="cannot_find_available_storage">Puudub olemasolevat salvestamisruumi</string>
|
||||
<string name="create_backup">Tee andmete tagavara</string>
|
||||
<string name="detailed_list">Detailitud loetelu</string>
|
||||
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">sisse lülitatud %1$d / %2$d</string>
|
||||
<string name="tap_to_try_again">Vajuta, et uuesti proovida</string>
|
||||
<string name="ignore_ssl_errors">Ignoreeri SSL vigasi</string>
|
||||
<string name="auth_required">Selle sisu vaatamiseks logi sisse</string>
|
||||
<string name="search_manga">Otsi mangat</string>
|
||||
<string name="next">Järgmine</string>
|
||||
<string name="restore_backup">Taasta andmete tagavarast</string>
|
||||
<string name="reader_info_bar">Näita inforiba lugejas</string>
|
||||
<string name="password_length_hint">Parool peab olema 4 tähemärki või veel</string>
|
||||
<string name="server_address">Sereri aadress</string>
|
||||
<string name="text_feed_holder">Uued peatükid mangast mis sa loed näidatakse siin</string>
|
||||
<string name="text_suggestion_holder">Hakka lugema mangat ja sa saad isikupärastatud soovitusi</string>
|
||||
<string name="explore">Seikle</string>
|
||||
<string name="find_similar">Leija samasuguseid</string>
|
||||
<string name="show_notification_new_chapters_off">Sa ei saa teateid uuenduste kohta, aga uued peatükkid on esiletoodud nimekirjas</string>
|
||||
<string name="storage_usage">Ruumi kasutatud</string>
|
||||
<string name="data_restored">Taastatud</string>
|
||||
<string name="favourites_category_empty">Tühi kategooria</string>
|
||||
<string name="protect_application_subtitle">Sisesta parool millega startida äppi</string>
|
||||
<string name="remote_sources">Manga allikad</string>
|
||||
<string name="theme_name_sakura">Sakura</string>
|
||||
<string name="clear">Tühjenda</string>
|
||||
<string name="view_list">Vaata nimekirja</string>
|
||||
<string name="by_rating">Hinnang</string>
|
||||
<string name="unknown">Mitte teatud</string>
|
||||
<string name="newest">Uusimad</string>
|
||||
<string name="suggestions">Soovitused</string>
|
||||
<string name="sort_order">Sorteerimisjärjekord</string>
|
||||
<string name="clear_history">Kustuta ajalugu</string>
|
||||
<string name="show_notification_new_chapters_on">Sa saad manga kohta mis sa loed teateid uuenduste kohta</string>
|
||||
<string name="importing_manga">Impordi manga</string>
|
||||
<string name="enabled">Sisse lülitatud</string>
|
||||
<string name="pause">Paus</string>
|
||||
<string name="clear_new_chapters_counters">Ja tühjenda informatsioon uute peatükkide kohta</string>
|
||||
<string name="text_clear_search_history_prompt">Eemaldada kõik hiljutised otsingupäringud?</string>
|
||||
<string name="nothing_here">Midagi pole siin</string>
|
||||
<string name="remove_completed">Eemalda lõpetatud</string>
|
||||
<string name="manga_save_location">Kaust allalaadimiseks</string>
|
||||
<string name="status_reading">Loen</string>
|
||||
<string name="auth_complete">Lubatud</string>
|
||||
<string name="loading_">Laadimine…</string>
|
||||
<string name="suggestions_notifications_summary">Vahepeal näita teateid koos soovitatud mangaga</string>
|
||||
<string name="updates_feed_cleared">Tühjendatud</string>
|
||||
<string name="webtoon_zoom">Webtooni zoom</string>
|
||||
<string name="various_languages">Erinevad keeled</string>
|
||||
<string name="list_mode">Loetelu mood</string>
|
||||
<string name="removal_completed">Eemalda lõppetatud</string>
|
||||
<string name="download_complete">Alla laetud</string>
|
||||
<string name="theme_name_miku">Miku</string>
|
||||
<string name="edit">Muuda</string>
|
||||
<string name="captcha_required">CAPTCHA vajatud</string>
|
||||
<string name="filter_load_error">Ei saa laadida žanrite nimekirja</string>
|
||||
<string name="popular">Populaarsed</string>
|
||||
<string name="manage_categories">Halda kategoorjaid</string>
|
||||
<string name="update">Uuenda</string>
|
||||
<string name="scrobbling_empty_hint">Lugemise edenemise jälgimiseks valige manga üksikasjade ekraanil menüü → Jälgimine.</string>
|
||||
<string name="removed_from_history">Eemaldatud ajaloost</string>
|
||||
<string name="crash_text">Midagi läks valesti. Palun esitage arendajatele veateade, et aidata meil seda parandada.</string>
|
||||
<string name="theme">Teema</string>
|
||||
<string name="detect_reader_mode_summary">Automaatselt tuvastada, kas manga on webtoon</string>
|
||||
<string name="color_correction_hint">Valitud värvi seaded jäetakse meelde selle manga jaoks</string>
|
||||
<string name="not_found_404">Sisu mitte saadaval või eemaldatud</string>
|
||||
<string name="feed_will_update_soon">Voogu uuendused algavad varsti</string>
|
||||
<string name="appwidget_recent_description">Sinu hiljuti loetud manga</string>
|
||||
<string name="got_it">Saadud</string>
|
||||
<string name="chapters">Peatükid</string>
|
||||
<string name="appearance">Välimus</string>
|
||||
<string name="bookmark_remove">Eemalda järjehoidja</string>
|
||||
<string name="search_hint">Sisestage manga pealkiri, žanr või allika nimi</string>
|
||||
<string name="disable_battery_optimization_summary">Aitab tausta uuenduste kontrollimisega</string>
|
||||
<string name="downloads">Alla laadimised</string>
|
||||
<string name="auth_not_supported_by">Sisselogimine %s ei ole toetatud</string>
|
||||
<string name="app_update_available">Uus äppi versioon on olemas</string>
|
||||
<string name="status_on_hold">Ootel</string>
|
||||
<string name="last_2_hours">Viimased 2 tundi</string>
|
||||
<string name="name">Nimi</string>
|
||||
<string name="sources_reorder_tip">Vajuta ja hoija asjal, et uuesti reastada neid</string>
|
||||
<string name="edit_category">Muuda kategooriat</string>
|
||||
<string name="resume">Jätka</string>
|
||||
<string name="server_error">Serveri poolene viga (%1$d). Palun proovi uuessti hiljem</string>
|
||||
<string name="bookmark_removed">Järjehoidja eemaldatud</string>
|
||||
<string name="network_unavailable_hint">Lülita sisse Wi-Fi või mobile data, et lugeda mangat online</string>
|
||||
<string name="you_have_not_favourites_yet">Pole lemmikuid veel</string>
|
||||
<string name="select_range">Valige vahemik</string>
|
||||
<string name="tracker_warning">Mõnedel seadmetel on erinev süsteemikäitumine, mis võib katkestada taustatööd.</string>
|
||||
<string name="network_error">Võrguviga</string>
|
||||
<string name="new_version_s">Uus versioon: %s</string>
|
||||
<string name="no_manga_sources">Pole manga allikaid</string>
|
||||
<string name="light">Valge</string>
|
||||
<string name="suggestions_excluded_genres_summary">Täpsusta žanrid, mida te ei soovi ettepanekutes näha</string>
|
||||
<string name="text_delete_local_manga">Kustuta \"%s\" seadelt?</string>
|
||||
<string name="prefetch_content">Sisu eellaeb</string>
|
||||
<string name="confirm_exit">Vajuta Tagasi, et lahkuda</string>
|
||||
<string name="scale_mode">skaala mood</string>
|
||||
<string name="advanced">Täiustatud</string>
|
||||
<string name="only_using_wifi">Ainult Wi-Fi-l</string>
|
||||
<string name="automatic">Jälgne süsteemile</string>
|
||||
<string name="sync_settings">Sünkroniseeri seadeid</string>
|
||||
<string name="black_dark_theme">Must</string>
|
||||
<string name="text_history_holder_primary">Mis sa loed näidatakse siin</string>
|
||||
<string name="back">Must</string>
|
||||
<string name="delete_manga">Kustuta manga</string>
|
||||
<string name="create_category">Uus kategooria</string>
|
||||
<string name="delete">Kustuta</string>
|
||||
<string name="notification_sound">Teate hääl</string>
|
||||
<string name="screenshots_allow">Luba</string>
|
||||
<string name="backup_restore">Varundamine ja taastamine</string>
|
||||
<string name="other_cache">Teised vahemälud</string>
|
||||
<string name="dns_over_https">DNS üle HTTPS</string>
|
||||
<string name="show_suspicious_content">Näita kahtlast sisu</string>
|
||||
<string name="show_pages_numbers">nummerda leheküljed</string>
|
||||
<string name="allow_unstable_updates_summary">Saa teateid ebastabiilsete uuenduste kohta</string>
|
||||
<string name="sync_title">Sünkroniseeri oma andmed</string>
|
||||
<string name="appwidget_shelf_description">Manga sinu lemmikutest</string>
|
||||
<string name="comics_archive_import_description">Sa võid valida üks või veel .cbz või .zip faile, iga fail võetakse kui erinev manga.</string>
|
||||
<string name="search_history_cleared">Tühjendatud</string>
|
||||
<string name="_s_deleted_from_local_storage">\"%s\" eemaldatud lokaasest salvestusruumist</string>
|
||||
<string name="too_many_requests_message">Liiga palju taotlusi. Proovige hiljem uuesti</string>
|
||||
<string name="downloads_wifi_only">Allalaadi ainult üle Wi-Fi</string>
|
||||
<string name="send">Saada</string>
|
||||
<string name="bookmark_add">Lisa järjehoidja</string>
|
||||
<string name="discard">Unusta</string>
|
||||
<string name="saved_manga">Salvestatud manga</string>
|
||||
<string name="manga_downloading_">Laadin alla…</string>
|
||||
<string name="screenshots_block_all">Alati blokeeri</string>
|
||||
<string name="new_sources_text">Uued manga allikad on saadaval</string>
|
||||
<string name="manga_error_description_pattern">Vea detailid:<br><tt>%1$s</tt><br><br>1. Proovi <a href=%2$s>avada mangat oma browseris</a> et olla kindel et see manga on saadaval allikast <br>2. tee kindlaks et sa kasutad <a href=kotatsu://about>kõige uuemat versiooni Kotatsut</a><br>3. Kui see on saadaval, saada vea raport arendajatele.</string>
|
||||
<string name="open_in_browser">Ava veebibrauser</string>
|
||||
<string name="about_app_translation">Tõlge</string>
|
||||
<string name="zoom_mode_fit_height">Sobita kõrgusele</string>
|
||||
<string name="notifications">Teated</string>
|
||||
<string name="not_available">Mitte saadaval</string>
|
||||
<string name="check_new_chapters_title">Otsi uusi peatükke ja tevita sellest</string>
|
||||
<string name="history_shortcuts_summary">Tee hiljutine manga saadaval hoides pikkalt applikatsiooni ikoonil</string>
|
||||
<string name="automatic_scroll">Automaatiline scroll</string>
|
||||
<string name="reverse">Tagurpidi</string>
|
||||
<string name="track_sources">Otsi uusi uuendusi</string>
|
||||
<string name="paused">Pausis</string>
|
||||
<string name="clear_search_history">Tühjenda otsingute ajalugu</string>
|
||||
<string name="wrong_password">Vale parool</string>
|
||||
<string name="group">Grupp</string>
|
||||
<string name="_import">Impordi</string>
|
||||
<string name="color_correction">Värvi korektsioon</string>
|
||||
<string name="just_now">Just nüüd</string>
|
||||
<string name="text_file_not_supported">Vali kas ZIP või CBZ fail.</string>
|
||||
<string name="download">Lae alla</string>
|
||||
<string name="chapter_is_missing">See peatükk puudub</string>
|
||||
<string name="categories_delete_confirm">Kas sa oled kindel, et tahad kustutada valitud lemmikud kategooriad?
|
||||
\nKõik manga nende sees kaob ja seda tagasi ei saa.</string>
|
||||
<string name="logged_in_as">Logitud sisse kui %s</string>
|
||||
<string name="suggestions_info">Kõik andmed analüüsitakse ainult lokaalselt selles seadmes ja neid ei saadeta kunagi kuhugi.</string>
|
||||
<string name="reader_info_pattern">Pt. %1$d/%2$d Lk. %3$d/%4$d</string>
|
||||
<string name="contrast">Kontrast</string>
|
||||
<string name="history_cleared">Ajalugu tühjendatud</string>
|
||||
<string name="size_s">Suurus: %s</string>
|
||||
<string name="reader_slider">Näita lehekülje vahetamise liugurit</string>
|
||||
<string name="no_manga_sources_text">Lülita sisse manga allikaid, et lugeda mangat online</string>
|
||||
<string name="about">Kohta</string>
|
||||
<string name="undo">Tühista</string>
|
||||
<string name="zoom_mode_fit_center">Sobita keskele</string>
|
||||
<string name="options">Valikud</string>
|
||||
<string name="services">Teenused</string>
|
||||
<string name="exclude_nsfw_from_history">Jäta NSFW manga ajaloost välja</string>
|
||||
<string name="clear_pages_cache">Tühjenda lehekülje vahemälu</string>
|
||||
<string name="download_slowdown_summary">Aitab vältida teie IP-aadressi blokeerimist</string>
|
||||
<string name="check_for_new_chapters">Otsi uusi peatükke</string>
|
||||
<string name="read">Loe</string>
|
||||
<string name="text_delete_local_manga_batch">Kustuta valitud asjad seadmelt?</string>
|
||||
<string name="queued">Järjekorras</string>
|
||||
<string name="text_clear_history_prompt">Tühjenda kõik lugemis ajalugu?</string>
|
||||
<string name="text_shelf_holder_primary">Sinu mangat näidatakse siin</string>
|
||||
<string name="report">Raporteeri</string>
|
||||
<string name="captcha_solve">Lahenda</string>
|
||||
<string name="download_slowdown">Aeglusta alla laadimised</string>
|
||||
<string name="bookmark_added">Järjehoidja lisatud</string>
|
||||
<string name="data_restored_with_errors">Andmed on taastatud, aga on vigasi</string>
|
||||
<string name="details">Detailid</string>
|
||||
<string name="sync">Sünkronisatsioon</string>
|
||||
<string name="new_chapters">Uued peatükid</string>
|
||||
<string name="search_chapters">Leia peatükk</string>
|
||||
<string name="exit_confirmation">Lahkumise kinnitus</string>
|
||||
<string name="comics_archive">Koomikute arhiiv</string>
|
||||
<string name="more">Rohkem</string>
|
||||
<string name="theme_name_asuka">Asuka</string>
|
||||
<string name="history_is_empty">Pole mingit ajalugu veel</string>
|
||||
<string name="always">Alati</string>
|
||||
<string name="import_will_start_soon">Import algab varsti</string>
|
||||
<string name="try_again">Proovi uuesti</string>
|
||||
<string name="compact">Kompaktne</string>
|
||||
<string name="protect_application">Kaitse seda äppi</string>
|
||||
<string name="folder_with_images_import_description">Sa võid valida kausta arhiivi või piltidega. Iga arhiiv (või alamkaust) võetakse kui erinev peatükk.</string>
|
||||
<string name="reorder">ümberjärjestamine</string>
|
||||
<string name="share_s">Jaga %s</string>
|
||||
<string name="suggestions_excluded_genres">Välista žanrid</string>
|
||||
<string name="chapter_d_of_d">Peatükid %1$d %2$d-st</string>
|
||||
<string name="canceled">Peatatud</string>
|
||||
<string name="feed">Voog</string>
|
||||
<string name="account_already_exists">Kasutaja juba eksisteerib</string>
|
||||
<string name="dark">Must</string>
|
||||
<string name="hide">Peida</string>
|
||||
<string name="passwords_mismatch">Ebavõrdsed paroolid</string>
|
||||
<string name="by_name">Nimi</string>
|
||||
<string name="yesterday">Eile</string>
|
||||
<string name="check_for_updates">Otsi uuendusi</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Manga märkitud kui NSFW kungai ei lisata sinu ajalukku ja progressi ei salvestata</string>
|
||||
<string name="mark_as_current">Margi kui präegune</string>
|
||||
<string name="protect_application_summary">Kusi parooli avades Kotatsut</string>
|
||||
<string name="right_to_left">Paremalt-vasakule</string>
|
||||
<string name="show_reading_indicators_summary">Näita mitu protsenti on loetud ajaloos ja lemmikutes</string>
|
||||
<string name="random">Suvakas</string>
|
||||
<string name="mirror_switching">Vali peegeldus automaatselt</string>
|
||||
<string name="use_fingerprint">Kasuta näppujälge, kui see on olemas</string>
|
||||
<string name="standard">Standard</string>
|
||||
<string name="history">Ajalugu</string>
|
||||
<string name="nothing_found">Mitte midagi leitud</string>
|
||||
<string name="onboard_text">Väli keeled milles sa tahad mangat lugeda. Seda võib hiljem seadetes muuta.</string>
|
||||
<string name="show_in_grid_view">Näita ruudustiku vaates</string>
|
||||
<string name="reader_mode_hint">Valitud konfiguratsioon jäetakse meelde selle manga jaoks</string>
|
||||
<string name="default_s">Default: %s</string>
|
||||
<string name="volume_buttons">Helitugevusnupud</string>
|
||||
<string name="confirm">Kinnita</string>
|
||||
<string name="suggestions_updating">Soovitused uuendavad</string>
|
||||
<string name="add_new_category">Uus kategooria</string>
|
||||
<string name="clear_updates_feed">Tühjenda uuenduste voog</string>
|
||||
<string name="enable">Lülita sisse</string>
|
||||
<string name="percent_string_pattern">%1$s%%</string>
|
||||
<string name="chapters_will_removed_background">Peatükid eemaldatakse taustal</string>
|
||||
<string name="import_completed_hint">Ruumi säästmiseks saad originaalfaili salvestusruumist kustutada</string>
|
||||
<string name="theme_name_rikka">Rikka</string>
|
||||
<string name="share_image">Jaga pilt</string>
|
||||
<string name="disabled">Välja lülitatud</string>
|
||||
<string name="long_ago">Kaua aega tagasi</string>
|
||||
<string name="reader_control_ltr_summary">Vajutades paremal äärele või paremale nuppule alati vahetub järgmisele leheküljele</string>
|
||||
<string name="list">Loetelu</string>
|
||||
<string name="notifications_settings">Teate seaded</string>
|
||||
<string name="language">Keel</string>
|
||||
<string name="domain">Domeen</string>
|
||||
<string name="save_manga">Salvesta</string>
|
||||
<string name="incognito_mode">Inkognito mood</string>
|
||||
<string name="no_bookmarks_summary">Sa võid teha järjehoidjaid mangat lugedes</string>
|
||||
<string name="large_manga_save_confirm">Sellel mangal on %s. Salvesta kõik?</string>
|
||||
<string name="search">Otsi</string>
|
||||
<string name="default_mode">Tava mood</string>
|
||||
<string name="reader_settings">Lugeja seaded</string>
|
||||
<string name="theme_name_mamimi">Mamimi</string>
|
||||
<string name="text_search_holder_secondary">Proovi otsingut teiste sõnadega.</string>
|
||||
<string name="error">Viga</string>
|
||||
<string name="grid_size">Ruudustiku suurus</string>
|
||||
<string name="manage">Halda</string>
|
||||
<string name="logout">Logi välja</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="status_completed">Lõpetatud</string>
|
||||
<string name="_continue">Jätka</string>
|
||||
<string name="grid">Ruudustik</string>
|
||||
<string name="recent_manga">Hiljutised</string>
|
||||
<string name="operation_not_supported">See operatsioon ei ole toetatud</string>
|
||||
<string name="reader_control_ltr">Ergonoomiline lügeja kontroll</string>
|
||||
<string name="dont_check">Ära kontrolli</string>
|
||||
<string name="save_page">Salvesta lehekülg</string>
|
||||
<string name="zoom_mode_fit_width">Sobita laijusele</string>
|
||||
<string name="read_more">Loe veel</string>
|
||||
<string name="reset_filter">Nulli filter</string>
|
||||
<string name="search_on_s">Otsi %s-st</string>
|
||||
<string name="status_dropped">Kukkutatud</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="disable_nsfw">Lülita välja NSFW</string>
|
||||
<string name="processing_">Töötlen…</string>
|
||||
<string name="black_dark_theme_summary">Kasuta vähem elektrit AMOLED ekraanidel</string>
|
||||
<string name="available">Saadaval</string>
|
||||
<string name="notifications_enable">Lülita sisse notifikatsioonid</string>
|
||||
<string name="network_unavailable">Võrk ei ole saadaval</string>
|
||||
<string name="search_results">Otsingutulemused</string>
|
||||
<string name="empty">Tühi</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Ära soovita NSFW mangat</string>
|
||||
<string name="file_not_found">Fail mitte leitud</string>
|
||||
<string name="never">Mitte kungai</string>
|
||||
<string name="text_unsaved_changes_prompt">Salvesta või unusta mitte salvestatud muutused?</string>
|
||||
<string name="settings">Seaded</string>
|
||||
<string name="folder_with_images">Kaustad piltitega</string>
|
||||
<string name="pages">Leheküljed</string>
|
||||
<string name="app_version">Versioon %s</string>
|
||||
<string name="cookies_cleared">Kõik küpsised on eemaldatud</string>
|
||||
<string name="disable_battery_optimization">Lülita välja aku optimeerimine</string>
|
||||
<string name="status_planned">Planeeritud</string>
|
||||
<string name="pages_animation">Lehekülje animatsioon</string>
|
||||
<string name="state_finished">Lõppetatud</string>
|
||||
<string name="state_ongoing">Käimasolev</string>
|
||||
<string name="suggestions_summary">Soovita manga vastavalt oma eelistustele</string>
|
||||
<string name="show">Näita</string>
|
||||
<string name="genres">Žanrid</string>
|
||||
<string name="show_on_shelf">Näita riiulil</string>
|
||||
<string name="allow_unstable_updates">Luba ebastabiilseid uuendusi</string>
|
||||
<string name="sync_auth_hint">Sa võid sisse logida olemasolevasse kasutajasse või teha uue</string>
|
||||
<string name="create_shortcut">Tee otsetee…</string>
|
||||
<string name="theme_name_kanade">Kanade</string>
|
||||
<string name="other_storage">Teine salvestamisruum</string>
|
||||
<string name="updated">Uuendatud</string>
|
||||
<string name="color_theme">Värvi skeema</string>
|
||||
<string name="brightness">Helendus</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
<string name="preparing_">Valmistan…</string>
|
||||
<string name="share">Jaga</string>
|
||||
<string name="computing_">Arvutan…</string>
|
||||
<string name="add">Lisa</string>
|
||||
<string name="exit_confirmation_summary">Vajuta Tagasi kaks korda, et lahkuda äppist</string>
|
||||
<string name="bookmarks_removed">Järjehoidjad eemaldatud</string>
|
||||
<string name="screenshots_block_nsfw">Blokeeri kui NSFW</string>
|
||||
<string name="enter_password">Sisesta parool</string>
|
||||
<string name="repeat_password">Korda parooli</string>
|
||||
<string name="data_restored_success">Kõik andmed on taastatud</string>
|
||||
<string name="theme_name_mion">Mion</string>
|
||||
<string name="mirror_switching_summary">Automaatselt vaheta domeene manga allika jaoks veaga kui peegeldused on saadaval</string>
|
||||
<string name="no_bookmarks_yet">Pole järjehoidjaid veel</string>
|
||||
<string name="no_thanks">Tänan ei</string>
|
||||
<string name="backup_information">Sa saad teha tagavara enda ajaloost, lemmikutest ja siis taastada need</string>
|
||||
<string name="available_sources">Kättesaadavad allikad</string>
|
||||
<string name="share_logs">Jaga looge</string>
|
||||
<string name="zoom_mode_keep_start">Hoija stardis</string>
|
||||
<string name="external_storage">Väline mälu</string>
|
||||
<string name="text_local_holder_secondary">Salvesta see interneti allikast või impordidtud failist.</string>
|
||||
<string name="speed">Kiirus</string>
|
||||
<string name="silent">Vaikne</string>
|
||||
<string name="text_local_holder_primary">Salvesta midagi esimesena</string>
|
||||
<string name="suggestion_manga">Soovitused: %s</string>
|
||||
<string name="removed_from_favourites">Eemaldatud lemmikutest</string>
|
||||
<string name="bookmarks">Järjehoidja</string>
|
||||
<string name="show_all">Näita kõik</string>
|
||||
<string name="page_saved">Salvestatud</string>
|
||||
<string name="today">Täna</string>
|
||||
<string name="empty_favourite_categories">Pole lemmikkategooriaid</string>
|
||||
<string name="invalid_domain_message">Kehtetu domeen</string>
|
||||
<string name="system_default">Tava</string>
|
||||
<string name="text_empty_holder_primary">On üsna tühi siin…</string>
|
||||
<string name="screenshots_policy">Ekraanipiltide poliitika</string>
|
||||
<string name="done">Tehtud</string>
|
||||
<string name="error_no_space_left">Ruumi pole ülejäänud seadmel</string>
|
||||
<string name="sign_in">Logi sisse</string>
|
||||
</resources>
|
||||
@@ -150,7 +150,7 @@
|
||||
<string name="favourites_categories">Mga paboritong kategorya</string>
|
||||
<string name="remove_category">Tanggalin</string>
|
||||
<string name="text_empty_holder_primary">Parang walang laman dito…</string>
|
||||
<string name="text_history_holder_secondary">Hanapin kung ano ang babasahin sa side menu.</string>
|
||||
<string name="text_history_holder_secondary">Hanapin kung anong pwedeng basahin sa «Explore» section</string>
|
||||
<string name="text_shelf_holder_secondary">Hanapin kung ano ang babasahin sa seksyong «Mag-explore»</string>
|
||||
<string name="recent_manga">Kamakailan</string>
|
||||
<string name="manga_save_location">Folder para sa mga download</string>
|
||||
@@ -267,7 +267,7 @@
|
||||
<string name="incognito_mode">Incognito mode</string>
|
||||
<string name="no_chapters">Walang mga kabanata</string>
|
||||
<string name="automatic_scroll">Awtomatikong pag-scroll</string>
|
||||
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
|
||||
<string name="reader_info_pattern">Ka. %1$d/%2$d Pp. %3$d/%4$d</string>
|
||||
<string name="reader_info_bar">Ipakita ang information bar sa pagbasa</string>
|
||||
<string name="comics_archive">Archive ng mga comics</string>
|
||||
<string name="folder_with_images">Folder na may mga larawan</string>
|
||||
@@ -303,8 +303,8 @@
|
||||
<string name="nothing_here">Wala naman dito</string>
|
||||
<string name="services">Mga serbisyo</string>
|
||||
<string name="allow_unstable_updates">Payagan ang mga hindi stable na update</string>
|
||||
<string name="show_reading_indicators">Ipakita ang mga tagapagpahiwatig ng progress ng pagbabasa</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Manga na minarkahan bilang NSFW ay hindi kailanman idadagdag sa kasaysayan at ang iyong progress ay hindi mase-save</string>
|
||||
<string name="show_reading_indicators">Ipakita ang pang-hiwatig ng progreso</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Ang mga Manga na naka-mark na NSFW ay hindi madadagdag sa history at yung progreso mo ay hindi ma-sasave</string>
|
||||
<string name="clear_cookies_summary">Maaaring makatulong sa kaso ng ilang mga isyu. Ang lahat ng pahintulot ay mawawalan ng bisa</string>
|
||||
<string name="invalid_domain_message">Imbalidong domain</string>
|
||||
<string name="last_2_hours">Huling 2 oras</string>
|
||||
@@ -313,7 +313,7 @@
|
||||
<string name="no_bookmarks_yet">Wala pang bookmark</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="not_found_404">Hindi natagpuan o inalis ang nilalaman</string>
|
||||
<string name="enable_logging_summary">Magtala ng ilang pagkilos para sa mga layunin ng pag-debug</string>
|
||||
<string name="enable_logging_summary">Magtala ng ilang pagkilos para sa mga layunin ng pag-debug. Huwag buksan kung hindi mo alam kung ano ang ginagawa mo</string>
|
||||
<string name="text_delete_local_manga_batch">Permanenteng tanggalin ang mga napiling item sa device\?</string>
|
||||
<string name="chapters_empty">Walang mga kabanata sa manga na ito</string>
|
||||
<string name="suggestions_updating">Nag-a-update ang mga mungkahi</string>
|
||||
@@ -343,7 +343,7 @@
|
||||
<string name="use_fingerprint">Gumamit ng fingerprint kung magagamit</string>
|
||||
<string name="report">Ulat</string>
|
||||
<string name="reset">I-reset</string>
|
||||
<string name="allow_unstable_updates_summary">Magmungkahi ng mga update sa mga beta na bersyon ng app</string>
|
||||
<string name="allow_unstable_updates_summary">Makakuha ng paunawa tungkol sa mga unstable build</string>
|
||||
<string name="network_unavailable">Hindi magagamit ang network</string>
|
||||
<string name="network_unavailable_hint">I-on ang Wi-Fi o mobile network para magbasa ng manga online</string>
|
||||
<string name="reader_control_ltr_summary">Mag-tap sa kanang gilid o ang pagpindot sa kanang key ay palaging lilipat sa susunod na pahina</string>
|
||||
@@ -361,16 +361,16 @@
|
||||
<string name="theme_name_miku">Miku</string>
|
||||
<string name="user_agent">Header ng UserAgent</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="standard">Standard</string>
|
||||
<string name="standard">Pamantayan</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="read_mode">Read mode</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="internal_storage">Internal storage</string>
|
||||
<string name="external_storage">External storage</string>
|
||||
<string name="read_mode">Mode na pang-basa</string>
|
||||
<string name="error">May Mali</string>
|
||||
<string name="internal_storage">Pangkaloobang storage</string>
|
||||
<string name="external_storage">Panlabasang storage</string>
|
||||
<string name="vibration">Vibration</string>
|
||||
<string name="domain">Domain</string>
|
||||
<string name="light_indicator">LED indicator</string>
|
||||
<string name="settings_apply_restart_required">Mangyaring i-restart ang application upang ilapat ang mga pagbabagong ito</string>
|
||||
<string name="settings_apply_restart_required">I-restart ang application para ma-ilapat ang mga pagbabagong ito</string>
|
||||
<string name="got_it">Nakuha ko</string>
|
||||
<string name="sources_reorder_tip">I-tap at hawakan ang isang aytem upang muling ayusin ang mga ito</string>
|
||||
<string name="restore_backup_description">Mag-import ng dating ginawa na backup ng data ng user</string>
|
||||
@@ -389,7 +389,7 @@
|
||||
<string name="sync_host_description">Maaari kang gumamit ng self-hosted synchronization server o isang default. Huwag baguhin ito kung hindi ka sigurado sa iyong ginagawa.</string>
|
||||
<string name="ignore_ssl_errors">Huwag pansinin ang mga error sa SSL</string>
|
||||
<string name="mirror_switching">Awtomatikong pumili ng mirror</string>
|
||||
<string name="mirror_switching_summary">Awtomatikong lumipat ng mga domain para sa mga remote source sa mga error kung available ang mga mirror</string>
|
||||
<string name="mirror_switching_summary">Kusang lumipat ng domain para sa mga manga source na may mali kung mayroong mga mirror</string>
|
||||
<string name="cancel_all">Kanselahin lahat</string>
|
||||
<string name="downloads_wifi_only">Mag-download lamang sa pamamagitan ng Wi-Fi</string>
|
||||
<string name="downloads_wifi_only_summary">Itigil ang pag-download kapag lumipat sa isang mobile network</string>
|
||||
@@ -423,7 +423,7 @@
|
||||
<string name="data_and_privacy">Data at privacy</string>
|
||||
<string name="restore_summary">Ibalik ang dating ginawang backup</string>
|
||||
<string name="webtoon_zoom_summary">Payagan ang pag-zoom in na gesture sa webtoon mode</string>
|
||||
<string name="reader_info_bar_summary">Ipakita ang kasalukuyang oras at pagbabasa ng progreso sa tuktok ng screen</string>
|
||||
<string name="reader_info_bar_summary">Ipakita ang kasalukuyang oras at progreso ng pagbabasa sa tuktok ng screen</string>
|
||||
<string name="show_pages_numbers_summary">Ipakita ang mga numero ng pahina sa ibabang gilid</string>
|
||||
<string name="pages_animation_summary">I-animate ang paglipat ng pahina</string>
|
||||
<string name="invalid_port_number">Imbalidong numero ng port</string>
|
||||
@@ -437,8 +437,8 @@
|
||||
<string name="download_option_all_unread_b">Lahat ng di-nababasang kabanata (%s)</string>
|
||||
<string name="download_option_manual_selection">Manu-manong pumili ng mga kabanata</string>
|
||||
<string name="invert_colors">Baliktarin ang mga kulay</string>
|
||||
<string name="custom_directory">Pasadyang direktoryo</string>
|
||||
<string name="pick_custom_directory">Pumili ng pasadyang direktoryo</string>
|
||||
<string name="custom_directory">Custom na direktoryo</string>
|
||||
<string name="pick_custom_directory">Pumili ng Custom na direktoryo</string>
|
||||
<string name="no_access_to_file">Wala kang access sa file o direktoryo na ito</string>
|
||||
<string name="local_manga_directories">Mga lokal na direktoryo ng manga</string>
|
||||
<string name="password">Password</string>
|
||||
@@ -464,17 +464,17 @@
|
||||
<string name="manage_categories">Ayusin ang mga kategorya</string>
|
||||
<string name="suggestions_wifi_only_summary">Huwag i-update ang mga mungkahi gamit ang mga naka-metrong network na koneksyon</string>
|
||||
<string name="show">Ipakita</string>
|
||||
<string name="unknown">Di-alam</string>
|
||||
<string name="unknown">Di-kilala</string>
|
||||
<string name="in_progress">Isinasagawa</string>
|
||||
<string name="disable_nsfw">I-disable ang NSFW</string>
|
||||
<string name="error_corrupted_file">Ang hindi wastong data ay ibinalik nito o ang file ay nasira</string>
|
||||
<string name="error_corrupted_file">Nabalik ang di-wastong data o na-corrupt ang file</string>
|
||||
<string name="related_manga_summary">Magpakita ng listahan ng mga kaugnay na manga. Sa ilang mga kaso, ito ay maaaring may mali o nawawala</string>
|
||||
<string name="advanced">Advanced</string>
|
||||
<string name="too_many_requests_message">Masyadong maraming request. Subukang ulit mamaya</string>
|
||||
<string name="default_section">Default na seksyon</string>
|
||||
<string name="manga_list">Listahan ng Manga</string>
|
||||
<string name="zoom_in">Mag-zoom paloob</string>
|
||||
<string name="reader_zoom_buttons_summary">Kung magpapakita ng mga button ng kontrol ng zoom sa kanang sulok sa ibaba</string>
|
||||
<string name="reader_zoom_buttons_summary">Kung magpapakita ba ng mga button ng pang-zoom sa kanang sulok sa ibaba</string>
|
||||
<string name="on_device">Sa device</string>
|
||||
<string name="moved_to_top">Nailipat sa itaas</string>
|
||||
<string name="items_limit_exceeded">Wala nang mga aytem na pwedeng idagdag</string>
|
||||
@@ -487,13 +487,13 @@
|
||||
<string name="enhanced_colors_summary">Binabawasan ang banding, ngunit maaaring makaapekto ito sa performance</string>
|
||||
<string name="state_abandoned">Na-drop</string>
|
||||
<string name="keep_screen_on">Panatilihing naka-on ang screen</string>
|
||||
<string name="enhanced_colors">32-bit color mode</string>
|
||||
<string name="enhanced_colors">32-bit na color mode</string>
|
||||
<string name="keep_screen_on_summary">Huwag I-off ang screen habang nagbabasa ng manga</string>
|
||||
<string name="suggest_new_sources_summary">I-prompt na paganahin ang mga bagong idinagdag na source pagkatapos i-update ang aplikasyon</string>
|
||||
<string name="categories">Mga Kategorya</string>
|
||||
<string name="list_options">Opsyon sa Listahan</string>
|
||||
<string name="by_relevance">Kaugnayan</string>
|
||||
<string name="online_variant">Online na baryante</string>
|
||||
<string name="online_variant">Online na variant</string>
|
||||
<string name="frequency_every_day">Araw araw</string>
|
||||
<string name="backup_frequency">Dalas ng paglikha ng backup</string>
|
||||
<string name="periodic_backups_enable">Paganahin ang periodic na pag-backup</string>
|
||||
@@ -517,7 +517,8 @@
|
||||
<string name="manual">Manu-mano</string>
|
||||
<string name="source_enabled">Napaganang source</string>
|
||||
<string name="disable_nsfw_summary">Huwag paganahin ang mga source na NSFW at itago ang manga na pang-adulto mula sa listahan kung maaari</string>
|
||||
<string name="no_manga_sources_catalog_text">Walang available na source sa seksyong ito. Antabayanan</string>
|
||||
<string name="no_manga_sources_catalog_text">Walang mga sources na maaring gamitin sa seksyong ito, o maaring na-add na lahat.
|
||||
\nManatiling nakatutok para sa iba pang mga source</string>
|
||||
<string name="available_d">Available: %1$d</string>
|
||||
<string name="content_type_other">Iba pa</string>
|
||||
<string name="error_multiple_states_not_supported">Ang pag-filter ayon sa maraming estado ay hindi sinusuportahan ng manga source na ito</string>
|
||||
@@ -532,9 +533,14 @@
|
||||
<string name="apply">Gamitin</string>
|
||||
<string name="genres_search_hint">Simulan ang pag-type ng pangalan ng genre</string>
|
||||
<string name="globally">Pangkahalatan</string>
|
||||
<string name="downloads_settings_info">Maaari mong paganahin ang pabagal na pag-download para sa bawat source ng manga nang paisa-isa sa mga setting ng source kung nagkakaroon ka ng mga problema sa pagharang sa server-side</string>
|
||||
<string name="downloads_settings_info">Maaari mong paganahin ang pag-bagal ng pag-download para sa bawat source ng manga nang paisa-isa sa mga setting ng source kung nagkakaroon ka ng mga problema sa pagharang na server-side</string>
|
||||
<string name="this_manga">Itong manga na ito</string>
|
||||
<string name="skip">Laktawan</string>
|
||||
<string name="color_correction_apply_text">Ang mga setting na ito ay maaaring ilapat sa pangkahalatan o sa kasalukuyang manga lamang. Kung inilapat sa pangkahalatan, hindi ma-override ang mga indibidwal na setting.</string>
|
||||
<string name="grayscale">Grayscale</string>
|
||||
<string name="grayscale">Walang kulay</string>
|
||||
<string name="welcome_text">Paki-select kung anong mga content source ang gusto mo i-enable. Pwede rin ito i-configure mamaya sa settings</string>
|
||||
<string name="restore">I-balik</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Baka makatulong sa pag-simula ng download kung mayroong isyu</string>
|
||||
<string name="backup_date_">Petsa ng pag-backup: %s</string>
|
||||
<string name="sync_auth">Mag-login para i-sync ang account</string>
|
||||
</resources>
|
||||
@@ -268,7 +268,7 @@
|
||||
<string name="show_reading_indicators">Mostrare gli indicatori di progresso della lettura</string>
|
||||
<string name="data_deletion">Eliminazione dei dati</string>
|
||||
<string name="show_reading_indicators_summary">Mostra la percentuale di lettura nella cronologia e nei preferiti</string>
|
||||
<string name="exclude_nsfw_from_history_summary">I manga contrassegnati come per adulti non verranno mai aggiunti alla cronologia e i vostri progressi non verranno salvati</string>
|
||||
<string name="exclude_nsfw_from_history_summary">I manga contrassegnati come per adulti non verranno mai aggiunti alla cronologia e i progressi non saranno salvati</string>
|
||||
<string name="clear_cookies_summary">Può aiutare in caso di problemi. Tutte le autorizzazioni saranno invalidate</string>
|
||||
<string name="show_all">Mostra tutto</string>
|
||||
<string name="logout">Esci</string>
|
||||
@@ -345,12 +345,12 @@
|
||||
\n Tutti i manga in esso contenuti andranno persi e questo non può essere annullato.</string>
|
||||
<string name="history_shortcuts_summary">Rendere disponibili i manga recenti premendo a lungo sull\'icona dell\'applicazione</string>
|
||||
<string name="reader_info_bar">Mostra la barra delle informazioni nel lettore</string>
|
||||
<string name="manga_error_description_pattern">Dettagli dell\'errore:<br><tt>%1$s</tt><br>1. Prova ad <a href=%2$s>aprire il manga in un browser web</a> per assicurarsi che sia disponibile sulla sua fonte<br>2. Se è disponibile, inviare una segnalazione di errore agli sviluppatori.</string>
|
||||
<string name="manga_error_description_pattern">Dettagli dell\'errore:<br><tt>%1$s</tt><br><br>1. Prova ad <a href=%2$s>aprire il manga in un browser web</a> per assicurarsi che sia disponibile sulla sua fonte<br>2. Controllare di stare usando la <a href=kotatsu://about>versione più recente di Kotatsu</a><br>3. Se è disponibile, inviare una segnalazione di errore agli sviluppatori.</string>
|
||||
<string name="text_unsaved_changes_prompt">Salvare o eliminare le modifiche non salvate\?</string>
|
||||
<string name="discard">Abbandona</string>
|
||||
<string name="language">Lingua</string>
|
||||
<string name="share_logs">Condividi i registri</string>
|
||||
<string name="enable_logging_summary">Registra alcune azioni a scopo di debug</string>
|
||||
<string name="enable_logging_summary">Registra alcune azioni a scopo di debug. Non attivare se non si è sicuri di cosa si stia facendo</string>
|
||||
<string name="enable_logging">Abilita la registrazione</string>
|
||||
<string name="show_suspicious_content">Mostra il contenuto sospetto</string>
|
||||
<string name="theme_name_dynamic">Dinamico</string>
|
||||
@@ -366,7 +366,181 @@
|
||||
<string name="theme_name_rikka">Rikka</string>
|
||||
<string name="theme_name_miku">Miku</string>
|
||||
<string name="allow_unstable_updates">Permetti aggiornamenti instabili</string>
|
||||
<string name="allow_unstable_updates_summary">Proponi aggiornamenti alle versioni beta dell\'app</string>
|
||||
<string name="allow_unstable_updates_summary">Ricevi notifiche riguardo versioni instabili</string>
|
||||
<string name="download_started">Scaricamento iniziato</string>
|
||||
<string name="theme_name_sakura">Sakura</string>
|
||||
<string name="languages">Lingue</string>
|
||||
<string name="zoom_in">Ingrandire</string>
|
||||
<string name="captcha_required_summary">%s richiede il completamento di un captcha per funzionare correttamente</string>
|
||||
<string name="sources_catalog">Catalogo fonti</string>
|
||||
<string name="download_option_all_unread">Tutti i capitoli non letti</string>
|
||||
<string name="frequency_every_day">Ogni giorno</string>
|
||||
<string name="categories">Categorie</string>
|
||||
<string name="progress">Progresso</string>
|
||||
<string name="cancel_all">Annulla tutti</string>
|
||||
<string name="sync_host_description">Puoi usare un server di sincronizzazione self-hosted oppure uno di default. Non cambiare se non si è sicuri di cosa si stia facendo.</string>
|
||||
<string name="error_corrupted_file">Dati non validi in ritorno o file corrotto</string>
|
||||
<string name="pick_custom_directory">Seleziona cartella personalizzata</string>
|
||||
<string name="list_options">Mostra lista opzioni</string>
|
||||
<string name="related_manga_summary">Mostra una lista di manga correlati. In alcuni casi potrebbe essere imprecisa o mancante</string>
|
||||
<string name="remove_completed_downloads_confirm">La tua cronologia dei download sarà permanentemente cancellata</string>
|
||||
<string name="reader_zoom_buttons_summary">Se mostrare o no i tasti di controllo dello zoom in basso a destra</string>
|
||||
<string name="content_type_manga">Manga</string>
|
||||
<string name="tracker_wifi_only_summary">Non controllare nuovi capitoli mentre è in uso una connessione a consumo</string>
|
||||
<string name="error_multiple_states_not_supported">Questa fonte di manga non supporta il filtraggio per multipli stati</string>
|
||||
<string name="order_added">Aggiunto</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="on_device">Su dispositivo</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="download_option_whole_manga">L\'intero manga</string>
|
||||
<string name="settings_apply_restart_required">Riavviare l\'applicazione per applicare queste modifiche</string>
|
||||
<string name="backup_frequency">Frequenza creazione backup</string>
|
||||
<string name="data_and_privacy">Dati e privacy</string>
|
||||
<string name="content_type_hentai">Hentai</string>
|
||||
<string name="clear_source_cookies_summary">Pulisci cookies solo per dominio specifico. Nella maggior parte dei casi invaliderà l\'autorizzazione</string>
|
||||
<string name="downloads_wifi_only_summary">Smetti di scaricare quando si passa a rete mobile</string>
|
||||
<string name="suggest_new_sources">Suggerisci nuove fonti dopo aggiornamento dell\'app</string>
|
||||
<string name="user_agent">Intestazione UserAgent</string>
|
||||
<string name="error_filter_states_genre_not_supported">Questa fonte non supporta il filtraggio per sia genere che stato</string>
|
||||
<string name="ignore_ssl_errors">Ignora errori SSL</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Questa fonte non supporta il filtraggio per sia genere che localizzazione</string>
|
||||
<string name="periodic_backups_enable">Abilita backup periodici</string>
|
||||
<string name="server_address">Indirizzo server</string>
|
||||
<string name="content_type_comics">Fumetti</string>
|
||||
<string name="moved_to_top">Spostato in cima</string>
|
||||
<string name="find_similar">Trova simili</string>
|
||||
<string name="data_not_restored_text">Assicurati di aver selezionato il file di backup corretto</string>
|
||||
<string name="catalog">Catalogo</string>
|
||||
<string name="view_list">Vedi lista</string>
|
||||
<string name="welcome_text">Selezionare le fonti di contenuti da abilitare. Si può configurare anche successivamente nelle impostazioni</string>
|
||||
<string name="unknown">Sconosciuto</string>
|
||||
<string name="in_progress">In progresso</string>
|
||||
<string name="download_option_manual_selection">Selezione manuale capitoli</string>
|
||||
<string name="enhanced_colors_summary">Riduce il banding, ma può impattare la performance</string>
|
||||
<string name="pause">Pausa</string>
|
||||
<string name="remove_completed">Rimuovi completati</string>
|
||||
<string name="items_limit_exceeded">Impossibile aggiungere altri oggetti</string>
|
||||
<string name="frequency_every_2_days">Ogni 2 giorni</string>
|
||||
<string name="suggestions_notifications_summary">A volte mostra le notifiche sui manga suggeriti</string>
|
||||
<string name="invalid_value_message">Valore non valido</string>
|
||||
<string name="downloads_cancelled">I download sono stati annullati</string>
|
||||
<string name="reader_optimize">Riduci consumo di memoria (beta)</string>
|
||||
<string name="apply">Applica</string>
|
||||
<string name="restore">Ripristina</string>
|
||||
<string name="data_not_restored">Dati non ripristinati</string>
|
||||
<string name="manage_sources">Gestisci fonti</string>
|
||||
<string name="directories">Cartelle</string>
|
||||
<string name="local_manga_directories">Cartelle manga locali</string>
|
||||
<string name="manage_categories">Gestisci categorie</string>
|
||||
<string name="no_manga_sources_found">Nessuna fonte manga trovata in base alla tua query</string>
|
||||
<string name="color_light">Chiaro</string>
|
||||
<string name="web_view_unavailable">WebView non disponibile: controlla se è installato un provider di WebView</string>
|
||||
<string name="genres_search_hint">Inizia a scrivere il nome del genere</string>
|
||||
<string name="port">Porta</string>
|
||||
<string name="type">Tipo</string>
|
||||
<string name="search_hint">Inserisci titolo manga, genere o nome fonte</string>
|
||||
<string name="frequency_once_per_week">Una volta a settimana</string>
|
||||
<string name="description">Descrizione</string>
|
||||
<string name="periodic_backups">Backup periodici</string>
|
||||
<string name="reader_zoom_buttons">Mostra tasti zoom</string>
|
||||
<string name="sources_reorder_tip">Tocca e tieni premuto su un elemento per riordinarlo</string>
|
||||
<string name="globally">Globalmente</string>
|
||||
<string name="resume">Riprendi</string>
|
||||
<string name="images_proxy_title">Proxy ottimizzazione immagini</string>
|
||||
<string name="username">Nome utente</string>
|
||||
<string name="frequency_twice_per_month">Due volte al mese</string>
|
||||
<string name="main_screen_sections">Sezioni principali schermo</string>
|
||||
<string name="advanced">Avanzate</string>
|
||||
<string name="downloads_settings_info">Se stai avendo problemi di blocco dal server, puoi abilitare il rallentamento dei download per ogni fonte di manga nelle impostazioni delle fonti</string>
|
||||
<string name="sync_settings">Impostazioni di sincronizzazione</string>
|
||||
<string name="online_variant">Variante online</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Potrebbe aiutare a far iniziare il download in caso di problemi</string>
|
||||
<string name="error_multiple_genres_not_supported">Questa fonte di manga non supporta il filtraggio per multipli generi</string>
|
||||
<string name="download_option_all_unread_b">Tutti i capitoli non letti (%s)</string>
|
||||
<string name="authorization_optional">Autorizzazione (opzionale)</string>
|
||||
<string name="color_dark">Scuro</string>
|
||||
<string name="this_manga">Questo manga</string>
|
||||
<string name="reader_info_bar_summary">Mostra l\'orario attuale e il progresso di lettura in cima allo schermo</string>
|
||||
<string name="translations">Traduzioni</string>
|
||||
<string name="downloads_paused">I download sono stati messi in pausa</string>
|
||||
<string name="too_many_requests_message">Troppe richieste. Riprova più tardi</string>
|
||||
<string name="downloads_wifi_only">Scarica solo via Wi-Fi</string>
|
||||
<string name="lock_screen_rotation">Blocca rotazione schermo</string>
|
||||
<string name="cancel_all_downloads_confirm">Tutti i download attivi saranno annullati, i dati scaricati parzialmente saranno persi</string>
|
||||
<string name="by_relevance">Rilevanza</string>
|
||||
<string name="related_manga">Manga correlati</string>
|
||||
<string name="state_abandoned">Abbandonato</string>
|
||||
<string name="download_option_first_n_chapters">Primo %s</string>
|
||||
<string name="keep_screen_on">Tieni schermo acceso</string>
|
||||
<string name="paused">In pausa</string>
|
||||
<string name="text_downloads_list_holder">Non hai alcun download</string>
|
||||
<string name="skip">Salta</string>
|
||||
<string name="error_search_not_supported">Questa fonte di manga non supporta la ricerca</string>
|
||||
<string name="invalid_port_number">Numero di porta non valido</string>
|
||||
<string name="suggestions_wifi_only_summary">Non aggiornare suggerimenti mentre è in uso una connessione a consumo</string>
|
||||
<string name="webtoon_zoom_summary">Consenti gesto di zoom in modalità webtoon</string>
|
||||
<string name="frequency_once_per_month">Una volta al mese</string>
|
||||
<string name="network">Rete</string>
|
||||
<string name="downloaded">Scaricato</string>
|
||||
<string name="suggestions_enable_prompt">Vuoi ricevere suggerimenti personalizzati di manga?</string>
|
||||
<string name="manual">Manuale</string>
|
||||
<string name="custom_directory">Cartella personalizzata</string>
|
||||
<string name="more">Di più</string>
|
||||
<string name="reader_optimize_summary">Riduci la qualità delle pagine fuori schermo per usare meno memoria</string>
|
||||
<string name="address">Indirizzo</string>
|
||||
<string name="color_correction_apply_text">Queste impostazioni possono essere applicate globalmente o per solo questo manga. Le applicazioni globali non sovrascrivono quelle individuali.</string>
|
||||
<string name="source_enabled">Fonte abilitata</string>
|
||||
<string name="enhanced_colors">Modalità colori 32-bit</string>
|
||||
<string name="folder_with_images_import_description">Puoi selezionare una cartella contenente archivi o immagini. Ogni archivio (o sottocartella) sarà riconosciuto come singolo capitolo.</string>
|
||||
<string name="default_section">Sezione default</string>
|
||||
<string name="background">Sfondo</string>
|
||||
<string name="disable_nsfw_summary">Disabilita fonti per adulti e nascondi manga per adulti dalla lista se possibile</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="downloads_removed">I download sono stati rimossi</string>
|
||||
<string name="pages_animation_summary">Animazioni cambio pagina</string>
|
||||
<string name="no_access_to_file">Non hai accesso a questo file o cartella</string>
|
||||
<string name="mirror_switching">Scegli mirror automaticamente</string>
|
||||
<string name="restore_summary">Ripristina backup creato in precedenza</string>
|
||||
<string name="show_pages_numbers_summary">Mostra numeri di pagina nell\'angolo inferiore</string>
|
||||
<string name="zoom_out">Rimpicciolire</string>
|
||||
<string name="keep_screen_on_summary">Non spegnere lo schermo durante la lettura di manga</string>
|
||||
<string name="download_option_next_unread_n_chapters">Prossimo %s non letto</string>
|
||||
<string name="clear_network_cache">Pulisci cache di rete</string>
|
||||
<string name="voice_search">Ricerca vocale</string>
|
||||
<string name="enable">Abilita</string>
|
||||
<string name="backup_date_">Data del backup: %s</string>
|
||||
<string name="images_procy_description">Usa il servizio wsrv.nl per ridurre i consumi di rete e velocizzare il caricamento delle immagini, se possibile</string>
|
||||
<string name="no_manga_sources_catalog_text">Non sono disponibili fonti in questa sezione, o potrebbero essere state aggiunte tutte.
|
||||
\nRimani sintonizzato</string>
|
||||
<string name="available_d">Disponibile: %1$d</string>
|
||||
<string name="manga_branch_title_template">%1$s (%2$s)</string>
|
||||
<string name="state">Stato</string>
|
||||
<string name="manga_list">Lista manga</string>
|
||||
<string name="grayscale">Bianco e nero</string>
|
||||
<string name="disable_nsfw">Disabilita contenuti per adulti</string>
|
||||
<string name="last_successful_backup">Ultimo backup completato: %s</string>
|
||||
<string name="color_white">Bianco</string>
|
||||
<string name="downloads_resumed">I download sono stati ripresi</string>
|
||||
<string name="details_button_tip">Tocca e tieni premuto il tasto Leggi per vedere più opzioni</string>
|
||||
<string name="state_paused">In pausa</string>
|
||||
<string name="to_top">Va in cima</string>
|
||||
<string name="show">Mostra</string>
|
||||
<string name="sync_auth_hint">Puoi accedere ad un account esistente o crearne uno nuovo</string>
|
||||
<string name="backups_output_directory">Cartella salvataggio backup</string>
|
||||
<string name="invert_colors">Inverti colori</string>
|
||||
<string name="mirror_switching_summary">Cambiare dominio di fonte del manga automaticamente in caso di errore se sono disponibili mirror</string>
|
||||
<string name="no_thanks">No grazie</string>
|
||||
<string name="suggest_new_sources_summary">Chiedi se aggiungere nuove fonti dopo aggiornamento dell\'applicazione</string>
|
||||
<string name="speed">Velocità</string>
|
||||
<string name="download_option_all_chapters">Tutti i capitoli con traduzioni %s</string>
|
||||
<string name="content_type_other">Altro</string>
|
||||
<string name="suggestion_manga">Consigliato: %s</string>
|
||||
<string name="color_black">Nero</string>
|
||||
<string name="this_month">Questo mese</string>
|
||||
<string name="sync_auth">Accedi per sincronizzare l\'account</string>
|
||||
<string name="proxy">Proxy</string>
|
||||
<string name="restore_backup_description">Importa un backup di dati utente creato precedentemente</string>
|
||||
<string name="got_it">Ho capito</string>
|
||||
<string name="comics_archive_import_description">Puoi selezionare uno o più file .cbz o .zip, ogni file sarà riconosciuto come un manga a parte.</string>
|
||||
<string name="show_on_shelf">Mostra sullo Scaffale</string>
|
||||
</resources>
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
|
||||
<item name="android:windowSplashScreenBackground">@android:color/system_neutral2_900</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration">800</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration">@integer/splash_screen_duration</item>
|
||||
<item name="android:windowSplashScreenBackground">@color/m3_sys_color_dynamic_dark_surface</item>
|
||||
</style>
|
||||
|
||||
<!-- From ThemeOverlay.Material3.DynamicColors.Dark -->
|
||||
<style name="Theme.Kotatsu.Monet">
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="isMaterial3DynamicColorApplied">true</item>
|
||||
<!-- Color palettes -->
|
||||
<item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
|
||||
@@ -61,11 +62,11 @@
|
||||
<item name="colorOnErrorContainer">@color/m3_sys_color_dark_on_error_container</item>
|
||||
<item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item>
|
||||
<item name="colorOutlineVariant">@color/m3_sys_color_dynamic_dark_outline_variant</item>
|
||||
<item name="android:colorBackground">@android:color/system_neutral2_900</item>
|
||||
<item name="m3ColorBackground">@android:color/system_neutral2_900</item>
|
||||
<item name="m3ColorExploreButton">@android:color/system_neutral2_800</item>
|
||||
<item name="m3ColorCardBackground">@android:color/system_neutral2_700</item>
|
||||
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_800</item>
|
||||
<item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_surface</item>
|
||||
<item name="m3ColorBackground">@color/m3_sys_color_dynamic_dark_surface</item>
|
||||
<item name="m3ColorExploreButton">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
|
||||
<item name="m3ColorCardBackground">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
|
||||
<item name="m3ColorBottomMenuBackground">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
|
||||
<!-- Default Framework Text Colors. -->
|
||||
<item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text
|
||||
</item>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<color name="status_bar_incognito">#66311B92</color>
|
||||
<color name="warning">#FB8C00</color>
|
||||
<color name="launcher_background">#222222</color>
|
||||
<color name="splash_primary">#0C4091</color>
|
||||
|
||||
<!-- Color schemes colors -->
|
||||
<color name="background_miku">#191C1C</color>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="ThemeOverlay.Kotatsu" parent="ThemeOverlay.Material3.Dark" />
|
||||
<style name="ThemeOverlay.Kotatsu" parent="ThemeOverlay.Material3.Dark">
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.Kotatsu.Amoled" parent="">
|
||||
<item name="colorSurface">@color/surface_amoled</item>
|
||||
|
||||
@@ -2,31 +2,37 @@
|
||||
<resources>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$d item</item>
|
||||
<item quantity="many">%1$d itens</item>
|
||||
<item quantity="other">%1$d itens</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$d capítulo</item>
|
||||
<item quantity="many">%1$d capítulos</item>
|
||||
<item quantity="other">%1$d capítulos</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%1$d novo capítulo</item>
|
||||
<item quantity="other">%1$d novos capítulos</item>
|
||||
<item quantity="one">%1$d capítulo novo</item>
|
||||
<item quantity="many">%1$d capítulos novos</item>
|
||||
<item quantity="other">%1$d capítulos novos</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">%1$d minuto atrás</item>
|
||||
<item quantity="many">%1$d minutos atrás</item>
|
||||
<item quantity="other">%1$d minutos atrás</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">%1$d hora atrás</item>
|
||||
<item quantity="many">%1$d horas atrás</item>
|
||||
<item quantity="other">%1$d horas atrás</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">%1$d dia atrás</item>
|
||||
<item quantity="many">%1$d dias atrás</item>
|
||||
<item quantity="other">%1$d dias atrás</item>
|
||||
</plurals>
|
||||
<plurals name="months_ago">
|
||||
<item quantity="one">%1$d mês atrás</item>
|
||||
<item quantity="many">%1$d meses atrás</item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="other">%1$d meses atrás</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user