Compare commits

..

17 Commits
v5.2.3 ... v5.3

Author SHA1 Message Date
Koitharu
9d31e76cc7 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Cookies
20910ffb5d Translated using Weblate (Vietnamese)
Currently translated at 81.2% (360 of 443 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Clxff H3r4ld0
7497ee6364 Translated using Weblate (Indonesian)
Currently translated at 100.0% (443 of 443 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Abay Emes
0f2ed50e18 Translated using Weblate (Kazakh)
Currently translated at 48.8% (213 of 436 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
BlackSpectrum
ba066b577b Translated using Weblate (Hindi)
Currently translated at 15.5% (68 of 436 strings)

Co-authored-by: BlackSpectrum <tittan5000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
CakesTwix
4496fe876f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (436 of 436 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
gallegonovato
a9f5abebf0 Translated using Weblate (Spanish)
Currently translated at 100.0% (443 of 443 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (436 of 436 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
qrynill
bebee2ef27 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 91.7% (399 of 435 strings)

Co-authored-by: qrynill <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Макар Разин
4ec2b0c8fe Translated using Weblate (Vietnamese)
Currently translated at 79.3% (345 of 435 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Korean)

Currently translated at 75.1% (327 of 435 strings)

Translated using Weblate (Greek)

Currently translated at 19.3% (84 of 435 strings)

Translated using Weblate (Serbian)

Currently translated at 28.2% (123 of 435 strings)

Translated using Weblate (Arabic)

Currently translated at 18.1% (79 of 435 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Italian)

Currently translated at 85.2% (371 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Koitharu
4a7be70898 Update queries in manifest 2023-06-23 16:14:41 +03:00
Koitharu
2bcba1eb21 Configure manga directories 2023-06-22 13:45:29 +03:00
Koitharu
feca7ba3fc Support for custom directories for manga 2023-06-22 10:11:11 +03:00
Koitharu
745b349e5e Ability to remove item from updates 2023-06-21 15:27:20 +03:00
Koitharu
13946783a5 Fix crashes 2023-06-21 15:06:01 +03:00
Koitharu
84e5400522 Download options dialog 2023-06-21 14:54:11 +03:00
Koitharu
02c9a933d2 Fix offline manga details 2023-06-20 17:06:18 +03:00
Koitharu
92af851d3b Option to clear single source cookies 2023-06-20 13:43:09 +03:00
82 changed files with 3432 additions and 1680 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 555 versionCode 556
versionName '5.2.3' versionName '5.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -79,7 +79,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') { implementation('com.github.KotatsuApp:kotatsu-parsers:c2b79b55f8') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -113,9 +113,10 @@ dependencies {
exclude group: 'com.google.j2objc', module: 'j2objc-annotations' exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
} }
implementation 'androidx.room:room-runtime:2.5.1' implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.room:room-ktx:2.5.1' implementation 'androidx.room:room-ktx:2.5.2'
kapt 'androidx.room:room-compiler:2.5.1' //noinspection KaptUsageInsteadOfKsp
kapt 'androidx.room:room-compiler:2.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
@@ -141,7 +142,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227' testImplementation 'org.json:json:20230618'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'

View File

@@ -18,6 +18,22 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
</intent>
</queries>
<application <application
android:name="org.koitharu.kotatsu.KotatsuApp" android:name="org.koitharu.kotatsu.KotatsuApp"
@@ -32,6 +48,7 @@
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales" android:localeConfig="@xml/locales"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Kotatsu" android:theme="@style/Theme.Kotatsu"
@@ -98,6 +115,9 @@
<data android:host="sync-settings" /> <data android:host="sync-settings" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
android:label="@string/local_manga_directories" />
<activity <activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity" android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"

View File

@@ -8,6 +8,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.webkit.CookieManager
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@@ -36,6 +37,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
javaScriptEnabled = true javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome userAgentString = CommonHeadersInterceptor.userAgentChrome
} }
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this) viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)

View File

@@ -33,7 +33,16 @@ abstract class ErrorObserver(
return resolver != null && ExceptionResolver.canResolve(error) return resolver != null && ExceptionResolver.canResolve(error)
} }
private fun isAlive(): Boolean {
return when {
fragment != null -> fragment.view != null
activity != null -> !activity.isDestroyed
else -> true
}
}
protected fun resolve(error: Throwable) { protected fun resolve(error: Throwable) {
if (isAlive()) {
lifecycleScope.launch { lifecycleScope.launch {
val isResolved = resolver?.resolve(error) ?: false val isResolved = resolver?.resolve(error) ?: false
if (isActive) { if (isActive) {
@@ -41,4 +50,5 @@ abstract class ErrorObserver(
} }
} }
} }
}
} }

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
class ToastErrorObserver(
host: View,
fragment: Fragment?,
) : ErrorObserver(host, fragment, null, null) {
override suspend fun emit(value: Throwable) {
val toast = Toast.makeText(host.context, value.getDisplayMessage(host.context.resources), Toast.LENGTH_SHORT)
toast.show()
}
}

View File

@@ -8,10 +8,14 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
@JvmName("mangaIds")
fun Collection<Manga>.ids() = mapToSet { it.id } fun Collection<Manga>.ids() = mapToSet { it.id }
fun Collection<Manga>.distinctById() = distinctBy { it.id } fun Collection<Manga>.distinctById() = distinctBy { it.id }
@JvmName("chaptersIds")
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
fun Collection<ChapterListItem>.countChaptersByBranch(): Int { fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) { if (size <= 1) {
return size return size

View File

@@ -4,6 +4,7 @@ import android.webkit.CookieManager
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.util.ext.newBuilder
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -30,6 +31,21 @@ class AndroidCookieJar : MutableCookieJar {
} }
} }
override fun removeCookies(url: HttpUrl) {
val cookies = loadForRequest(url)
if (cookies.isEmpty()) {
return
}
val urlString = url.toString()
for (c in cookies) {
val nc = c.newBuilder()
.expiresAt(System.currentTimeMillis() - 100000)
.build()
cookieManager.setCookie(urlString, nc.toString())
}
check(loadForRequest(url).isEmpty())
}
override suspend fun clear() = suspendCoroutine<Boolean> { continuation -> override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume) cookieManager.removeAllCookies(continuation::resume)
} }

View File

@@ -13,5 +13,8 @@ interface MutableCookieJar : CookieJar {
@WorkerThread @WorkerThread
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
@WorkerThread
fun removeCookies(url: HttpUrl)
suspend fun clear(): Boolean suspend fun clear(): Boolean
} }

View File

@@ -21,6 +21,7 @@ class PreferencesCookieJar(
private var isLoaded = false private var isLoaded = false
@WorkerThread @WorkerThread
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> { override fun loadForRequest(url: HttpUrl): List<Cookie> {
loadPersistent() loadPersistent()
val expired = HashSet<String>() val expired = HashSet<String>()
@@ -40,6 +41,7 @@ class PreferencesCookieJar(
} }
@WorkerThread @WorkerThread
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val wrapped = cookies.map { CookieWrapper(it) } val wrapped = cookies.map { CookieWrapper(it) }
prefs.edit(commit = true) { prefs.edit(commit = true) {
@@ -53,6 +55,22 @@ class PreferencesCookieJar(
} }
} }
@Synchronized
@WorkerThread
override fun removeCookies(url: HttpUrl) {
loadPersistent()
val toRemove = HashSet<String>()
for ((key, cookie) in cache) {
if (cookie.isExpired() || cookie.cookie.matches(url)) {
toRemove += key
}
}
if (toRemove.isNotEmpty()) {
cache.removeAll(toRemove)
removePersistent(toRemove)
}
}
override suspend fun clear(): Boolean { override suspend fun clear(): Boolean {
cache.clear() cache.clear()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@@ -21,9 +21,11 @@ import org.koitharu.kotatsu.core.util.ext.filterToSet
import org.koitharu.kotatsu.core.util.ext.getEnumValue import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
import java.io.File import java.io.File
@@ -234,14 +236,28 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key) if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
}.getOrDefault(ScreenshotsPolicy.ALLOW) }.getOrDefault(ScreenshotsPolicy.ALLOW)
var userSpecifiedMangaDirectories: Set<File>
get() {
val set = prefs.getStringSet(KEY_LOCAL_MANGA_DIRS, emptySet()).orEmpty()
return set.mapNotNullToSet { File(it).takeIfReadable() }
}
set(value) {
val set = value.mapToSet { it.absolutePath }
prefs.edit { putStringSet(KEY_LOCAL_MANGA_DIRS, set) }
}
var mangaStorageDir: File? var mangaStorageDir: File?
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let { get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
File(it) File(it)
}?.takeIf { it.exists() } }?.takeIf { it.exists() && it in userSpecifiedMangaDirectories }
set(value) = prefs.edit { set(value) = prefs.edit {
if (value == null) { if (value == null) {
remove(KEY_LOCAL_STORAGE) remove(KEY_LOCAL_STORAGE)
} else { } else {
val userDirs = userSpecifiedMangaDirectories
if (value !in userDirs) {
userSpecifiedMangaDirectories = userDirs + value
}
putString(KEY_LOCAL_STORAGE, value.path) putString(KEY_LOCAL_STORAGE, value.path)
} }
} }
@@ -461,6 +477,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_LOGIN = "proxy_login" const val KEY_PROXY_LOGIN = "proxy_login"
const val KEY_PROXY_PASSWORD = "proxy_password" const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy" const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -1,101 +0,0 @@
package org.koitharu.kotatsu.core.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.data.LocalStorageManager
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(storageManager)
private val delegate = MaterialAlertDialogBuilder(context)
init {
if (adapter.isEmpty) {
delegate.setMessage(R.string.cannot_find_available_storage)
} else {
val defaultValue = runBlocking {
storageManager.getDefaultWriteableDir()
}
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
it.first.canonicalPath == defaultValue?.canonicalPath
}
delegate.setAdapter(adapter) { d, i ->
listener.onStorageSelected(adapter.getItem(i).first)
d.dismiss()
}
}
}
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setNegativeButton(@StringRes textId: Int): Builder {
delegate.setNegativeButton(textId, null)
return this
}
fun create() = StorageSelectDialog(delegate.create())
}
private class VolumesAdapter(storageManager: LocalStorageManager) : BaseAdapter() {
var selectedItemPosition: Int = -1
val volumes = getAvailableVolumes(storageManager)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
view.tag = it
}
val item = volumes[position]
binding.imageViewIndicator.isChecked = selectedItemPosition == position
binding.textViewTitle.text = item.second
binding.textViewSubtitle.text = item.first.path
return view
}
override fun getItem(position: Int): Pair<File, String> = volumes[position]
override fun getItemId(position: Int) = position.toLong()
override fun getCount() = volumes.size
override fun hasStableIds() = true
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
return runBlocking {
storageManager.getWriteableDirs().map {
it to storageManager.getStorageDisplayName(it)
}
}
}
}
fun interface OnStorageSelectListener {
fun onStorageSelected(file: File)
}
}

View File

@@ -24,6 +24,7 @@ import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.shape.ShapeAppearanceModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ViewTwoLinesItemBinding import org.koitharu.kotatsu.databinding.ViewTwoLinesItemBinding
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@@ -35,6 +36,18 @@ class TwoLinesItemView @JvmOverloads constructor(
private val binding = ViewTwoLinesItemBinding.inflate(LayoutInflater.from(context), this) private val binding = ViewTwoLinesItemBinding.inflate(LayoutInflater.from(context), this)
var title: CharSequence?
get() = binding.title.text
set(value) {
binding.title.text = value
}
var subtitle: CharSequence?
get() = binding.subtitle.textAndVisible
set(value) {
binding.subtitle.textAndVisible = value
}
init { init {
var textColors: ColorStateList? = null var textColors: ColorStateList? = null
context.withStyledAttributes( context.withStyledAttributes(
@@ -76,8 +89,7 @@ class TwoLinesItemView @JvmOverloads constructor(
} }
fun setIconResource(@DrawableRes resId: Int) { fun setIconResource(@DrawableRes resId: Int) {
val icon = if (resId != 0) ContextCompat.getDrawable(context, resId) else null binding.icon.setImageResource(resId)
binding.icon.setImageDrawable(icon)
} }
private fun createShapeDrawable(ta: TypedArray): InsetDrawable { private fun createShapeDrawable(ta: TypedArray): InsetDrawable {

View File

@@ -0,0 +1,117 @@
package org.koitharu.kotatsu.core.util;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.DocumentsContract;
import androidx.annotation.Nullable;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.List;
public final class FileUtil {
private static final String PRIMARY_VOLUME_NAME = "primary";
@Nullable
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
if (treeUri == null) return null;
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con);
if (volumePath == null) return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
} else return volumePath;
}
private static String getVolumePath(final String volumeId, Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return getVolumePathForAndroid11AndAbove(volumeId, context);
} else
return getVolumePathBeforeAndroid11(volumeId, context);
}
private static String getVolumePathBeforeAndroid11(final String volumeId, Context context) {
try {
StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
Method getUuid = storageVolumeClazz.getMethod("getUuid");
Method getPath = storageVolumeClazz.getMethod("getPath");
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
Object result = getVolumeList.invoke(mStorageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
Object storageVolumeElement = Array.get(result, i);
String uuid = (String) getUuid.invoke(storageVolumeElement);
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) // primary volume?
return (String) getPath.invoke(storageVolumeElement);
if (uuid != null && uuid.equals(volumeId)) // other volumes?
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
@TargetApi(Build.VERSION_CODES.R)
private static String getVolumePathForAndroid11AndAbove(final String volumeId, Context context) {
try {
StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
for (StorageVolume storageVolume : storageVolumes) {
// primary volume?
if (storageVolume.isPrimary() && PRIMARY_VOLUME_NAME.equals(volumeId))
return storageVolume.getDirectory().getPath();
// other volumes?
String uuid = storageVolume.getUuid();
if (uuid != null && uuid.equals(volumeId))
return storageVolume.getDirectory().getPath();
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if (split.length > 0) return split[0];
else return null;
}
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null)) return split[1];
else return File.separator;
}
}

View File

@@ -0,0 +1,90 @@
package org.koitharu.kotatsu.core.util.ext
import android.annotation.TargetApi
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import org.koitharu.kotatsu.parsers.util.removeSuffix
import java.io.File
import java.lang.reflect.Array as ArrayReflect
private const val PRIMARY_VOLUME_NAME = "primary"
fun Uri.resolveFile(context: Context): File? {
val volumeId = getVolumeIdFromTreeUri(this) ?: return null
val volumePath = getVolumePath(volumeId, context)?.removeSuffix(File.separatorChar) ?: return null
val documentPath = getDocumentPathFromTreeUri(this)?.removeSuffix(File.separatorChar) ?: return null
return File(
if (documentPath.isNotEmpty()) {
if (documentPath.startsWith(File.separator)) {
volumePath + documentPath
} else {
volumePath + File.separator + documentPath
}
} else {
volumePath
},
)
}
private fun getVolumePath(volumeId: String, context: Context): String? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getVolumePathForAndroid11AndAbove(volumeId, context)
} else {
getVolumePathBeforeAndroid11(volumeId, context)
}
}
private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): String? = runCatching {
val mStorageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList")
val getUuid = storageVolumeClazz.getMethod("getUuid")
val getPath = storageVolumeClazz.getMethod("getPath")
val isPrimary = storageVolumeClazz.getMethod("isPrimary")
val result = getVolumeList.invoke(mStorageManager)
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 primary = isPrimary.invoke(storageVolumeElement) as Boolean
when {
primary && volumeId == PRIMARY_VOLUME_NAME -> getPath.invoke(storageVolumeElement) as String
uuid == volumeId -> getPath.invoke(storageVolumeElement) as String
else -> null
}
}
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
@TargetApi(Build.VERSION_CODES.R)
private fun getVolumePathForAndroid11AndAbove(volumeId: String, context: Context): String? = runCatching {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
storageManager.storageVolumes.firstNotNullOfOrNull { volume ->
if (volume.isPrimary && volumeId == PRIMARY_VOLUME_NAME) {
volume.directory?.path
} else {
val uuid = volume.uuid
if (uuid != null && uuid == volumeId) volume.directory?.path else null
}
}
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
private fun getVolumeIdFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split = docId.split(":".toRegex())
return split.firstOrNull()?.takeUnless { it.isEmpty() }
}
private fun getDocumentPathFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split: Array<String?> = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
return if (split.size >= 2 && split[1] != null) split[1] else File.separator
}

View File

@@ -4,6 +4,7 @@ import android.os.SystemClock
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
@@ -62,3 +63,23 @@ fun <T> Flow<T>.zipWithPrevious(): Flow<Pair<T?, T>> = flow {
emit(result) emit(result)
} }
} }
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3,
args[3] as T4,
args[4] as T5,
args[5] as T6,
)
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@@ -38,3 +39,23 @@ fun Response.ensureSuccess() = apply {
throw IllegalStateException(message) throw IllegalStateException(message)
} }
} }
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
c.name(name)
c.value(value)
if (persistent) {
c.expiresAt(expiresAt)
}
if (hostOnly) {
c.hostOnlyDomain(domain)
} else {
c.domain(domain)
}
c.path(path)
if (secure) {
c.secure()
}
if (httpOnly) {
c.httpOnly()
}
}

View File

@@ -34,6 +34,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is FileNotFoundException -> resources.getString(R.string.file_not_found) is FileNotFoundException -> resources.getString(R.string.file_not_found)
is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is SyncApiException, is SyncApiException,
is ContentUnavailableException, is ContentUnavailableException,

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.util.ext
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.Toolbar
fun Toolbar.setNavigationIconSafe(@DrawableRes iconRes: Int, retry: Boolean = true) {
try {
setNavigationIcon(iconRes)
} catch (e: IllegalStateException) {
if (retry) {
post { setNavigationIconSafe(iconRes, retry = false) }
}
}
}

View File

@@ -30,6 +30,7 @@ class ChaptersBottomSheetMediator(
} }
override fun onActionModeStarted(mode: ActionMode) { override fun onActionModeStarted(mode: ActionMode) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
lock() lock()
} }

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
@@ -66,6 +67,9 @@ class ChaptersFragment :
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it binding.textViewHolder.isVisible = it
} }
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) {
selectionController?.onItemLongClick(it)
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@@ -43,6 +43,7 @@ import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
@@ -211,7 +212,7 @@ class DetailsActivity :
} }
if (isExpanded) { if (isExpanded) {
toolbar.addMenuProvider(chaptersMenuProvider) toolbar.addMenuProvider(chaptersMenuProvider)
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material) toolbar.setNavigationIconSafe(materialR.drawable.abc_ic_clear_material)
} else { } else {
toolbar.removeMenuProvider(chaptersMenuProvider) toolbar.removeMenuProvider(chaptersMenuProvider)
toolbar.navigationIcon = null toolbar.navigationIcon = null

View File

@@ -16,12 +16,11 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.details.ui.model.MangaBranch import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
@@ -30,7 +29,7 @@ class DetailsMenuProvider(
private val viewModel: DetailsViewModel, private val viewModel: DetailsViewModel,
private val snackbarHost: View, private val snackbarHost: View,
private val appShortcutManager: AppShortcutManager, private val appShortcutManager: AppShortcutManager,
) : MenuProvider { ) : MenuProvider, OnListItemClickListener<DownloadOption> {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_details, menu) menuInflater.inflate(R.menu.opt_details, menu)
@@ -44,7 +43,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity) menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_favourite).setIcon( menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline, if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
) )
} }
@@ -80,15 +79,7 @@ class DetailsMenuProvider(
} }
R.id.action_save -> { R.id.action_save -> {
viewModel.manga.value?.let { DownloadDialogHelper(snackbarHost, viewModel).show(this)
val chaptersCount = it.chapters?.size ?: 0
val branches = viewModel.branches.value.orEmpty()
if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches)
} else {
viewModel.download(null)
}
}
} }
R.id.action_browser -> { R.id.action_browser -> {
@@ -125,35 +116,16 @@ class DetailsMenuProvider(
return true return true
} }
private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<MangaBranch>) { override fun onItemClick(item: DownloadOption, view: View) {
val dialogBuilder = MaterialAlertDialogBuilder(activity) val chaptersIds: Set<Long>? = when (item) {
.setTitle(R.string.save_manga) is DownloadOption.WholeManga -> null
.setNegativeButton(android.R.string.cancel, null) is DownloadOption.SelectionHint -> {
if (branches.size > 1) { viewModel.startChaptersSelection()
val items = Array(branches.size) { i -> branches[i].name.orEmpty() } return
val currentBranch = branches.indexOfFirst { it.isSelected }
val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch }
dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked ->
checkedIndices[i] = checked
}.setPositiveButton(R.string.save) { _, _ ->
val selectedBranches = branches.mapIndexedNotNullTo(HashSet()) { i, b ->
if (checkedIndices[i]) b.name else null
} }
val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
if (c.branch in selectedBranches) c.id else null else -> item.chaptersIds
} }
viewModel.download(chaptersIds) viewModel.download(chaptersIds)
} }
} else {
dialogBuilder.setMessage(
activity.getString(
R.string.large_manga_save_confirm,
activity.resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount),
),
).setPositiveButton(R.string.save) { _, _ ->
viewModel.download(null)
}
}
dialogBuilder.show()
}
} }

View File

@@ -31,12 +31,14 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.core.util.ext.sanitize
@@ -72,6 +74,7 @@ class DetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
networkState: NetworkState,
) : BaseViewModel() { ) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle) private val intent = MangaIntent(savedStateHandle)
@@ -81,6 +84,7 @@ class DetailsViewModel @Inject constructor(
val onShowToast = MutableEventFlow<Int>() val onShowToast = MutableEventFlow<Int>()
val onShowTip = MutableEventFlow<Unit>() val onShowTip = MutableEventFlow<Unit>()
val onSelectChapter = MutableEventFlow<Long>()
val onDownloadStarted = MutableEventFlow<Unit>() val onDownloadStarted = MutableEventFlow<Unit>()
val manga = doubleManga.map { it?.any } val manga = doubleManga.map { it?.any }
@@ -176,8 +180,9 @@ class DetailsViewModel @Inject constructor(
selectedBranch, selectedBranch,
newChaptersCount, newChaptersCount,
bookmarks, bookmarks,
) { manga, history, branch, news, bookmarks -> networkState,
mapChapters(manga?.remote, manga?.local, history, news, branch, bookmarks) ) { manga, history, branch, news, bookmarks, isOnline ->
mapChapters(manga?.remote?.takeIf { isOnline }, manga?.local, history, news, branch, bookmarks)
}, },
isChaptersReversed, isChaptersReversed,
chaptersQuery, chaptersQuery,
@@ -286,6 +291,14 @@ class DetailsViewModel @Inject constructor(
} }
} }
fun startChaptersSelection() {
val chapters = chapters.value
val chapter = chapters.find {
it.isUnread && !it.isDownloaded
} ?: chapters.firstOrNull() ?: return
onSelectChapter.call(chapter.chapter.id)
}
fun onButtonTipClosed() { fun onButtonTipClosed() {
settings.closeTip(DetailsActivity.TIP_BUTTON) settings.closeTip(DetailsActivity.TIP_BUTTON)
} }

View File

@@ -0,0 +1,64 @@
package org.koitharu.kotatsu.details.ui
import android.content.DialogInterface
import android.view.View
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
import org.koitharu.kotatsu.download.ui.dialog.downloadOptionAD
class DownloadDialogHelper(
private val host: View,
private val viewModel: DetailsViewModel,
) {
fun show(callback: OnListItemClickListener<DownloadOption>) {
val branch = viewModel.selectedBranchValue
val allChapters = viewModel.manga.value?.chapters ?: return
val branchChapters = viewModel.manga.value?.getChapters(branch).orEmpty()
val history = viewModel.history.value
val options = buildList {
add(DownloadOption.WholeManga(allChapters.ids()))
if (branch != null && branchChapters.isNotEmpty()) {
add(DownloadOption.AllChapters(branch, branchChapters.ids()))
}
if (history != null) {
val unreadChapters = branchChapters.takeLastWhile { it.id != history.chapterId }
if (unreadChapters.isNotEmpty() && unreadChapters.size < branchChapters.size) {
add(DownloadOption.AllUnreadChapters(unreadChapters.ids(), branch))
if (unreadChapters.size > 5) {
add(DownloadOption.NextUnreadChapters(unreadChapters.take(5).ids()))
if (unreadChapters.size > 10) {
add(DownloadOption.NextUnreadChapters(unreadChapters.take(10).ids()))
}
}
}
} else {
if (branchChapters.size > 5) {
add(DownloadOption.FirstChapters(branchChapters.take(5).ids()))
if (branchChapters.size > 10) {
add(DownloadOption.FirstChapters(branchChapters.take(10).ids()))
}
}
}
add(DownloadOption.SelectionHint())
}
var dialog: DialogInterface? = null
val listener = OnListItemClickListener<DownloadOption> { item, _ ->
callback.onItemClick(item, host)
dialog?.dismiss()
}
dialog = RecyclerViewAlertDialog.Builder<DownloadOption>(host.context)
.addAdapterDelegate(downloadOptionAD(listener))
.setCancelable(true)
.setTitle(R.string.download)
.setNegativeButton(android.R.string.cancel)
.setItems(options)
.create()
.also { it.show() }
}
}

View File

@@ -0,0 +1,99 @@
package org.koitharu.kotatsu.download.ui.dialog
import android.content.res.Resources
import androidx.annotation.DrawableRes
import org.koitharu.kotatsu.R
import java.util.Locale
import com.google.android.material.R as materialR
sealed interface DownloadOption {
val chaptersIds: Set<Long>
@get:DrawableRes
val iconResId: Int
val chaptersCount: Int
get() = chaptersIds.size
fun getLabel(resources: Resources): CharSequence
class AllChapters(
val branch: String,
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = R.drawable.ic_select_group
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(R.string.download_option_all_chapters, branch)
}
}
class WholeManga(
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = materialR.drawable.abc_ic_menu_selectall_mtrl_alpha
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(R.string.download_option_whole_manga)
}
}
class FirstChapters(
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = R.drawable.ic_list_start
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(
R.string.download_option_first_n_chapters,
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount)
.lowercase(Locale.getDefault()),
)
}
}
class AllUnreadChapters(
override val chaptersIds: Set<Long>,
val branch: String?,
) : DownloadOption {
override val iconResId = R.drawable.ic_list_end
override fun getLabel(resources: Resources): CharSequence {
return if (branch == null) {
resources.getString(R.string.download_option_all_unread)
} else {
resources.getString(R.string.download_option_all_unread_b, branch)
}
}
}
class NextUnreadChapters(
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = R.drawable.ic_list_next
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(
R.string.download_option_next_unread_n_chapters,
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount)
.lowercase(Locale.getDefault()),
)
}
}
class SelectionHint : DownloadOption {
override val chaptersIds: Set<Long> = emptySet()
override val iconResId = R.drawable.ic_tap
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(R.string.download_option_manual_selection)
}
}
}

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.download.ui.dialog
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemDownloadOptionBinding
fun downloadOptionAD(
onClickListener: OnListItemClickListener<DownloadOption>,
) = adapterDelegateViewBinding<DownloadOption, DownloadOption, ItemDownloadOptionBinding>(
{ layoutInflater, parent -> ItemDownloadOptionBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v -> onClickListener.onItemClick(item, v) }
bind {
with(binding.root) {
title = item.getLabel(resources)
subtitle = if (item.chaptersCount == 0) null else resources.getQuantityString(
R.plurals.chapters,
item.chaptersCount,
item.chaptersCount,
)
setIconResource(item.iconResId)
}
}
}

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.local.data
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.StatFs import android.os.StatFs
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import dagger.Reusable import dagger.Reusable
@@ -13,11 +15,13 @@ import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.getStorageName import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
private const val DIR_NAME = "manga" private const val DIR_NAME = "manga"
private const val NOMEDIA = ".nomedia"
private const val CACHE_DISK_PERCENTAGE = 0.02 private const val CACHE_DISK_PERCENTAGE = 0.02
private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB
private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
@@ -74,14 +78,38 @@ class LocalStorageManager @Inject constructor(
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() } preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
} }
fun getStorageDisplayName(file: File) = file.getStorageName(context) suspend fun getApplicationStorageDirs(): Set<File> = runInterruptible(Dispatchers.IO) {
getAvailableStorageDirs()
}
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
uri.resolveFile(context)
}
suspend fun setDirIsNoMedia(dir: File) = runInterruptible(Dispatchers.IO) {
File(dir, NOMEDIA).createNewFile()
}
fun takePermissions(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, flags)
}
suspend fun getDirectoryDisplayName(dir: File, isFullPath: Boolean): String = runInterruptible(Dispatchers.IO) {
val packageName = context.packageName
if (dir.absolutePath.contains(packageName)) {
dir.getStorageName(context)
} else if (isFullPath) {
dir.path
} else {
dir.name
}
}
@WorkerThread @WorkerThread
private fun getConfiguredStorageDirs(): MutableSet<File> { private fun getConfiguredStorageDirs(): MutableSet<File> {
val set = getAvailableStorageDirs() val set = getAvailableStorageDirs()
settings.mangaStorageDir?.let { set.addAll(settings.userSpecifiedMangaDirectories)
set.add(it)
}
return set return set
} }

View File

@@ -33,7 +33,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick)) addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick))
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() } viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
} }
@@ -45,7 +45,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
FilterSheetFragment.show(childFragmentManager) FilterSheetFragment.show(childFragmentManager)
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = viewModel.loadNextPage()
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_local, menu) mode.menuInflater.inflate(R.menu.mode_local, menu)

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.local.ui package org.koitharu.kotatsu.local.ui
import android.content.Context
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
class LocalListMenuProvider( class LocalListMenuProvider(
private val context: Context,
private val onImportClick: Function0<Unit>, private val onImportClick: Function0<Unit>,
) : MenuProvider { ) : MenuProvider {
@@ -20,6 +23,12 @@ class LocalListMenuProvider(
onImportClick() onImportClick()
true true
} }
R.id.action_settings -> {
context.startActivity(MangaDirectoriesActivity.newIntent(context))
true
}
else -> false else -> false
} }
} }

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.local.ui package org.koitharu.kotatsu.local.ui
import android.content.SharedPreferences
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -24,7 +25,7 @@ class LocalListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
filter: FilterCoordinator, filter: FilterCoordinator,
settings: AppSettings, private val settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
listExtraProvider: ListExtraProvider, listExtraProvider: ListExtraProvider,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
@@ -36,7 +37,7 @@ class LocalListViewModel @Inject constructor(
settings, settings,
listExtraProvider, listExtraProvider,
downloadScheduler, downloadScheduler,
) { ), SharedPreferences.OnSharedPreferenceChangeListener {
val onMangaRemoved = MutableEventFlow<Unit>() val onMangaRemoved = MutableEventFlow<Unit>()
@@ -47,6 +48,18 @@ class LocalListViewModel @Inject constructor(
loadList(filter.snapshot(), append = false).join() loadList(filter.snapshot(), append = false).join()
} }
} }
settings.subscribe(this)
}
override fun onCleared() {
settings.unsubscribe(this)
super.onCleared()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == AppSettings.KEY_LOCAL_MANGA_DIRS) {
onRefresh()
}
} }
fun delete(ids: Set<Long>) { fun delete(ids: Set<Long>) {

View File

@@ -11,20 +11,18 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.dialog.StorageSelectDialog import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
import java.io.File import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class DownloadsSettingsFragment : class DownloadsSettingsFragment :
BasePreferenceFragment(R.string.downloads), BasePreferenceFragment(R.string.downloads),
SharedPreferences.OnSharedPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
StorageSelectDialog.OnStorageSelectListener {
@Inject @Inject
lateinit var storageManager: LocalStorageManager lateinit var storageManager: LocalStorageManager
@@ -39,6 +37,7 @@ class DownloadsSettingsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName() findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
findPreference<Preference>(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount()
settings.subscribe(this) settings.subscribe(this)
} }
@@ -53,6 +52,10 @@ class DownloadsSettingsFragment :
findPreference<Preference>(key)?.bindStorageName() findPreference<Preference>(key)?.bindStorageName()
} }
AppSettings.KEY_LOCAL_MANGA_DIRS -> {
findPreference<Preference>(key)?.bindDirectoriesCount()
}
AppSettings.KEY_DOWNLOADS_WIFI -> { AppSettings.KEY_DOWNLOADS_WIFI -> {
updateDownloadsConstraints() updateDownloadsConstraints()
} }
@@ -62,12 +65,12 @@ class DownloadsSettingsFragment :
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) { return when (preference.key) {
AppSettings.KEY_LOCAL_STORAGE -> { AppSettings.KEY_LOCAL_STORAGE -> {
val ctx = context ?: return false MangaDirectorySelectDialog.show(childFragmentManager)
StorageSelectDialog.Builder(ctx, storageManager, this) true
.setTitle(preference.title ?: "") }
.setNegativeButton(android.R.string.cancel)
.create() AppSettings.KEY_LOCAL_MANGA_DIRS -> {
.show() startActivity(MangaDirectoriesActivity.newIntent(preference.context))
true true
} }
@@ -75,14 +78,21 @@ class DownloadsSettingsFragment :
} }
} }
override fun onStorageSelected(file: File) {
settings.mangaStorageDir = file
}
private fun Preference.bindStorageName() { private fun Preference.bindStorageName() {
viewLifecycleScope.launch { viewLifecycleScope.launch {
val storage = storageManager.getDefaultWriteableDir() val storage = storageManager.getDefaultWriteableDir()
summary = storage?.getStorageName(context) ?: getString(R.string.not_available) summary = if (storage != null) {
storageManager.getDirectoryDisplayName(storage, isFullPath = true)
} else {
getString(R.string.not_available)
}
}
}
private fun Preference.bindDirectoriesCount() {
viewLifecycleScope.launch {
val dirs = storageManager.getReadableDirs().size
summary = resources.getQuantityString(R.plurals.items, dirs, dirs)
} }
} }

View File

@@ -4,16 +4,15 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
@@ -49,10 +48,18 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
getString(R.string.logged_in_as, it) getString(R.string.logged_in_as, it)
} }
} }
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) viewModel.onError.observeEvent(
viewLifecycleOwner,
SnackbarErrorObserver(
listView,
this,
exceptionResolver,
) { viewModel.onResume() },
)
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
findPreference<Preference>(KEY_AUTH)?.isEnabled = !isLoading findPreference<Preference>(KEY_AUTH)?.isEnabled = !isLoading
} }
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
} }
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
@@ -61,32 +68,15 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source)) startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source))
true true
} }
AppSettings.KEY_COOKIES_CLEAR -> {
viewModel.clearCookies()
true
}
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }
private fun onError(error: Throwable) {
val snackbar = Snackbar.make(
listView ?: return,
error.getDisplayMessage(resources),
Snackbar.LENGTH_INDEFINITE,
)
if (ExceptionResolver.canResolve(error)) {
snackbar.setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
}
snackbar.show()
}
private fun resolveError(error: Throwable) {
view ?: return
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
viewModel.onResume()
}
}
}
companion object { companion object {
private const val KEY_AUTH = "auth" private const val KEY_AUTH = "auth"

View File

@@ -5,9 +5,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.HttpUrl
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -17,11 +23,13 @@ import javax.inject.Inject
class SourceSettingsViewModel @Inject constructor( class SourceSettingsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
private val cookieJar: MutableCookieJar,
) : BaseViewModel() { ) : BaseViewModel() {
val source = savedStateHandle.require<MangaSource>(SourceSettingsFragment.EXTRA_SOURCE) val source = savedStateHandle.require<MangaSource>(SourceSettingsFragment.EXTRA_SOURCE)
val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
val onActionDone = MutableEventFlow<ReversibleAction>()
val username = MutableStateFlow<String?>(null) val username = MutableStateFlow<String?>(null)
private var usernameLoadJob: Job? = null private var usernameLoadJob: Job? = null
@@ -35,6 +43,18 @@ class SourceSettingsViewModel @Inject constructor(
} }
} }
fun clearCookies() {
launchLoadingJob(Dispatchers.Default) {
val url = HttpUrl.Builder()
.scheme("https")
.host(repository.domain)
.build()
cookieJar.removeCookies(url)
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
loadUsername()
}
}
private fun loadUsername() { private fun loadUsername() {
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
try { try {

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.webkit.CookieManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
@@ -68,6 +69,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
javaScriptEnabled = true javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome userAgentString = CommonHeadersInterceptor.userAgentChrome
} }
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this) viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.settings.storage
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageBinding
fun directoryAD(
clickListener: OnListItemClickListener<DirectoryModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageBinding>(
{ layoutInflater, parent -> ItemStorageBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v -> clickListener.onItemClick(item, v) }
bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes)
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath
binding.imageViewIndicator.isChecked = item.isChecked
}
}

View File

@@ -0,0 +1,22 @@
package org.koitharu.kotatsu.settings.storage
import androidx.recyclerview.widget.DiffUtil.ItemCallback
class DirectoryDiffCallback : ItemCallback<DirectoryModel>() {
override fun areItemsTheSame(oldItem: DirectoryModel, newItem: DirectoryModel): Boolean {
return oldItem.file == newItem.file
}
override fun areContentsTheSame(oldItem: DirectoryModel, newItem: DirectoryModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: DirectoryModel, newItem: DirectoryModel): Any? {
return if (oldItem.isChecked != newItem.isChecked) {
Unit
} else {
super.getChangePayload(oldItem, newItem)
}
}
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.settings.storage
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File
class DirectoryModel(
val title: String?,
@StringRes val titleRes: Int,
val file: File?,
val isChecked: Boolean,
val isAvailable: Boolean,
) : ListModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DirectoryModel
if (title != other.title) return false
if (titleRes != other.titleRes) return false
if (file != other.file) return false
if (isChecked != other.isChecked) return false
return isAvailable == other.isAvailable
}
override fun hashCode(): Int {
var result = title?.hashCode() ?: 0
result = 31 * result + titleRes
result = 31 * result + (file?.hashCode() ?: 0)
result = 31 * result + isChecked.hashCode()
result = 31 * result + isAvailable.hashCode()
return result
}
}

View File

@@ -0,0 +1,81 @@
package org.koitharu.kotatsu.settings.storage
import android.Manifest
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ToastErrorObserver
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.databinding.DialogDirectorySelectBinding
@AndroidEntryPoint
class MangaDirectorySelectDialog : AlertDialogFragment<DialogDirectorySelectBinding>(),
OnListItemClickListener<DirectoryModel> {
private val viewModel: MangaDirectorySelectViewModel by viewModels()
private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) viewModel.onCustomDirectoryPicked(it)
}
private val permissionRequestLauncher = registerForActivityResult(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
RequestStorageManagerPermissionContract()
} else {
ActivityResultContracts.RequestPermission()
},
) {
if (it) {
viewModel.refresh()
pickFileTreeLauncher.launch(null)
}
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogDirectorySelectBinding {
return DialogDirectorySelectBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: DialogDirectorySelectBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryAD(this))
binding.root.adapter = adapter
viewModel.items.observe(viewLifecycleOwner) { adapter.items = it }
viewModel.onDismissDialog.observeEvent(viewLifecycleOwner) { dismiss() }
viewModel.onPickDirectory.observeEvent(viewLifecycleOwner) { pickCustomDirectory() }
viewModel.onError.observeEvent(viewLifecycleOwner, ToastErrorObserver(binding.root, this))
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder)
.setCancelable(true)
.setTitle(R.string.manga_save_location)
.setNegativeButton(android.R.string.cancel, null)
}
override fun onItemClick(item: DirectoryModel, view: View) {
viewModel.onItemClick(item)
}
private fun pickCustomDirectory() {
permissionRequestLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
companion object {
private const val TAG = "MangaDirectorySelectDialog"
fun show(fm: FragmentManager) = MangaDirectorySelectDialog()
.showDistinct(fm, TAG)
}
}

View File

@@ -0,0 +1,78 @@
package org.koitharu.kotatsu.settings.storage
import android.net.Uri
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import okio.FileNotFoundException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
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.local.data.LocalStorageManager
import javax.inject.Inject
@HiltViewModel
class MangaDirectorySelectViewModel @Inject constructor(
private val storageManager: LocalStorageManager,
private val settings: AppSettings,
) : BaseViewModel() {
val items = MutableStateFlow(emptyList<DirectoryModel>())
val onDismissDialog = MutableEventFlow<Unit>()
val onPickDirectory = MutableEventFlow<Unit>()
init {
refresh()
}
fun onItemClick(item: DirectoryModel) {
if (item.file != null) {
settings.mangaStorageDir = item.file
onDismissDialog.call(Unit)
} else {
onPickDirectory.call(Unit)
}
}
fun onCustomDirectoryPicked(uri: Uri) {
launchJob(Dispatchers.Default) {
storageManager.takePermissions(uri)
val dir = storageManager.resolveUri(uri) ?: throw FileNotFoundException()
if (!dir.canWrite()) {
throw AccessDeniedException(dir)
}
if (dir !in storageManager.getApplicationStorageDirs()) {
settings.mangaStorageDir = dir
storageManager.setDirIsNoMedia(dir)
}
onDismissDialog.call(Unit)
}
}
fun refresh() {
launchJob(Dispatchers.Default) {
val defaultValue = storageManager.getDefaultWriteableDir()
val available = storageManager.getWriteableDirs()
items.value = buildList(available.size + 1) {
available.mapTo(this) { dir ->
DirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
titleRes = 0,
file = dir,
isChecked = dir == defaultValue,
isAvailable = true,
)
}
this += DirectoryModel(
title = null,
titleRes = R.string.pick_custom_directory,
file = null,
isChecked = false,
isAvailable = true,
)
}
}
}
}

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.settings.storage
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
@RequiresApi(Build.VERSION_CODES.R)
class RequestStorageManagerPermissionContract : ActivityResultContract<String, Boolean>() {
override fun createIntent(context: Context, input: String): Intent {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = "package:${context.packageName}".toUri()
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
return Environment.isExternalStorageManager()
}
override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? {
return if (Environment.isExternalStorageManager()) {
SynchronousResult(true)
} else {
null
}
}
}

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.settings.storage.directories
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding
import org.koitharu.kotatsu.settings.storage.DirectoryModel
fun directoryConfigAD(
clickListener: OnListItemClickListener<DirectoryModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageConfigBinding>(
{ layoutInflater, parent -> ItemStorageConfigBinding.inflate(layoutInflater, parent, false) },
) {
binding.imageViewRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes)
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath
binding.imageViewRemove.isVisible = item.isChecked
binding.textViewTitle.drawableStart = if (item.isAvailable) {
null
} else {
ContextCompat.getDrawable(context, R.drawable.ic_alert_outline)
}
}
}

View File

@@ -0,0 +1,93 @@
package org.koitharu.kotatsu.settings.storage.directories
import android.Manifest
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityMangaDirectoriesBinding
import org.koitharu.kotatsu.settings.storage.DirectoryDiffCallback
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
@AndroidEntryPoint
class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>(),
OnListItemClickListener<DirectoryModel>, View.OnClickListener {
private val viewModel: MangaDirectoriesViewModel by viewModels()
private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) viewModel.onCustomDirectoryPicked(it)
}
private val permissionRequestLauncher = registerForActivityResult(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
RequestStorageManagerPermissionContract()
} else {
ActivityResultContracts.RequestPermission()
},
) {
if (it) {
viewModel.updateList()
pickFileTreeLauncher.launch(null)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryConfigAD(this))
viewBinding.recyclerView.adapter = adapter
viewBinding.fabAdd.setOnClickListener(this)
viewModel.items.observe(this) { adapter.items = it }
viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it }
viewModel.onError.observeEvent(
this,
SnackbarErrorObserver(viewBinding.root, null, exceptionResolver) {
if (it) viewModel.updateList()
},
)
}
override fun onItemClick(item: DirectoryModel, view: View) {
viewModel.onRemoveClick(item.file ?: return)
}
override fun onClick(v: View?) {
permissionRequestLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.fabAdd.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = topMargin + insets.right
leftMargin = topMargin + insets.left
bottomMargin = topMargin + insets.bottom
}
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
viewBinding.recyclerView.updatePadding(
bottom = insets.bottom,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, MangaDirectoriesActivity::class.java)
}
}

View File

@@ -0,0 +1,85 @@
package org.koitharu.kotatsu.settings.storage.directories
import android.net.Uri
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import okio.FileNotFoundException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import java.io.File
import javax.inject.Inject
@HiltViewModel
class MangaDirectoriesViewModel @Inject constructor(
private val storageManager: LocalStorageManager,
private val settings: AppSettings,
) : BaseViewModel() {
val items = MutableStateFlow(emptyList<DirectoryModel>())
private var loadingJob: Job? = null
init {
loadList()
}
fun updateList() {
loadList()
}
fun onCustomDirectoryPicked(uri: Uri) {
launchLoadingJob(Dispatchers.Default) {
loadingJob?.cancelAndJoin()
storageManager.takePermissions(uri)
val dir = storageManager.resolveUri(uri) ?: throw FileNotFoundException()
if (!dir.canWrite()) {
throw AccessDeniedException(dir)
}
if (dir !in storageManager.getApplicationStorageDirs()) {
settings.userSpecifiedMangaDirectories += dir
loadList()
}
}
}
fun onRemoveClick(directory: File) {
settings.userSpecifiedMangaDirectories -= directory
if (settings.mangaStorageDir == directory) {
settings.mangaStorageDir = null
}
loadList()
}
private fun loadList() {
val prevJob = loadingJob
loadingJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
val applicationDirs = storageManager.getApplicationStorageDirs()
val customDirs = settings.userSpecifiedMangaDirectories
items.value = buildList(applicationDirs.size + customDirs.size) {
applicationDirs.mapTo(this) { dir ->
DirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
titleRes = 0,
file = dir,
isChecked = false,
isAvailable = dir.canRead() && dir.canWrite(),
)
}
customDirs.mapTo(this) { dir ->
DirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
titleRes = 0,
file = dir,
isChecked = true,
isAvailable = dir.canRead() && dir.canWrite(),
)
}
}
}
}
}

View File

@@ -13,9 +13,9 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
entity = MangaEntity::class, entity = MangaEntity::class,
parentColumns = ["manga_id"], parentColumns = ["manga_id"],
childColumns = ["manga_id"], childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE,
) ),
] ],
) )
class TrackEntity( class TrackEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)

View File

@@ -43,6 +43,9 @@ abstract class TracksDao {
@Query("UPDATE tracks SET chapters_new = 0") @Query("UPDATE tracks SET chapters_new = 0")
abstract suspend fun clearCounters() abstract suspend fun clearCounters()
@Query("UPDATE tracks SET chapters_new = 0 WHERE manga_id = :mangaId")
abstract suspend fun clearCounter(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id = :mangaId") @Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long) abstract suspend fun delete(mangaId: Long)

View File

@@ -126,6 +126,18 @@ class TrackingRepository @Inject constructor(
} }
} }
suspend fun clearUpdates(ids: Collection<Long>) {
when {
ids.isEmpty() -> return
ids.size == 1 -> db.tracksDao.clearCounter(ids.single())
else -> db.withTransaction {
for (id in ids) {
db.tracksDao.clearCounter(id)
}
}
}
}
suspend fun syncWithHistory(manga: Manga, chapterId: Long) { suspend fun syncWithHistory(manga: Manga, chapterId: Long) {
val chapters = manga.chapters ?: return val chapters = manga.chapters ?: return
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }

View File

@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.tracker.ui.updates package org.koitharu.kotatsu.tracker.ui.updates
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
@AndroidEntryPoint @AndroidEntryPoint
@@ -12,6 +17,22 @@ class UpdatesFragment : MangaListFragment() {
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_updates, menu)
return super.onCreateActionMode(controller, mode, menu)
}
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_remove -> {
viewModel.remove(controller.snapshot())
true
}
else -> super.onActionItemClicked(controller, mode, item)
}
}
companion object { companion object {
fun newInstance() = UpdatesFragment() fun newInstance() = UpdatesFragment()

View File

@@ -59,4 +59,10 @@ class UpdatesViewModel @Inject constructor(
override fun onRefresh() = Unit override fun onRefresh() = Unit
override fun onRetry() = Unit override fun onRetry() = Unit
fun remove(ids: Set<Long>) {
launchJob(Dispatchers.Default) {
repository.clearUpdates(ids)
}
}
} }

View File

@@ -48,7 +48,7 @@ class RecentListFactory(
override fun getViewAt(position: Int): RemoteViews { override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.item_recent) val views = RemoteViews(context.packageName, R.layout.item_recent)
val item = dataSet[position] val item = dataSet.getOrNull(position) ?: return views
runCatching { runCatching {
coil.executeBlocking( coil.executeBlocking(
ImageRequest.Builder(context) ImageRequest.Builder(context)

View File

@@ -58,7 +58,7 @@ class ShelfListFactory(
override fun getViewAt(position: Int): RemoteViews { override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.item_shelf) val views = RemoteViews(context.packageName, R.layout.item_shelf)
val item = dataSet[position] val item = dataSet.getOrNull(position) ?: return views
views.setTextViewText(R.id.textView_title, item.title) views.setTextViewText(R.id.textView_title, item.title)
runCatching { runCatching {
coil.executeBlocking( coil.executeBlocking(

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12C7,9.58 8.72,7.56 11,7.1V3H13V7.1C15.28,7.56 17,9.58 17,12M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12M12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15M13,3V5H11V3H13Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,7A5,5 0 0,1 17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" />
</vector>

View 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"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5 3A2 2 0 0 0 3 5H5M7 3V5H9V3M11 3V5H13V3M15 3V5H17V3M19 3V5H21A2 2 0 0 0 19 3M3 7V9H5V7M7 7V11H11V7M13 7V11H17V7M19 7V9H21V7M3 11V13H5V11M19 11V13H21V11M7 13V17H11V13M13 13V17H17V13M3 15V17H5V15M19 15V17H21V15M3 19A2 2 0 0 0 5 21V19M7 19V21H9V19M11 19V21H13V19M15 19V21H17V19M19 19V21A2 2 0 0 0 21 19Z" />
</vector>

View File

@@ -58,7 +58,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:contentDescription="@string/add_new_category" android:contentDescription="@string/add_new_category"
android:src="@drawable/ic_add"
android:text="@string/create_category" android:text="@string/create_category"
app:fabSize="normal" app:fabSize="normal"
app:icon="@drawable/ic_add" app:icon="@drawable/ic_add"

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="noScroll">
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/pick_custom_directory"
android:text="@string/add"
app:fabSize="normal"
app:icon="@drawable/ic_add"
app:layout_anchor="@id/recyclerView"
app:layout_anchorGravity="bottom|end"
app:layout_behavior="org.koitharu.kotatsu.core.ui.util.ShrinkOnScrollBehavior"
app:layout_dodgeInsetEdges="bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
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/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top|bottom"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:ignore="UnusedAttribute" />

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
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/button_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:minHeight="?android:listPreferredItemHeightSmall"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
tools:subtitle="@string/chapters"
tools:title="@string/download_option_whole_manga" />

View File

@@ -1,48 +1,46 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:minHeight="?listPreferredItemHeightLarge" android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeight"
android:orientation="horizontal"
android:paddingVertical="12dp"
android:paddingStart="?listPreferredItemPaddingStart" android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="16dp" android:paddingEnd="?listPreferredItemPaddingEnd">
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="16dp">
<org.koitharu.kotatsu.core.ui.widgets.CheckableImageView <org.koitharu.kotatsu.core.ui.widgets.CheckableImageView
android:id="@+id/imageView_indicator" android:id="@+id/imageView_indicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="?android:listChoiceIndicatorSingle" android:src="?android:listChoiceIndicatorSingle"
tools:ignore="TouchTargetSizeCheck" /> tools:ignore="TouchTargetSizeCheck" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:layout_toEndOf="@id/imageView_indicator"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[3]" /> tools:text="@tools:sample/lorem[3]" />
<TextView <TextView
android:id="@+id/textView_subtitle" android:id="@+id/textView_subtitle"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/textView_title"
android:layout_alignParentEnd="true"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:layout_marginTop="6dp" android:layout_marginTop="6dp"
android:layout_toEndOf="@id/imageView_indicator"
android:ellipsize="end"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[20]" /> tools:text="@tools:sample/lorem[20]" />
</RelativeLayout> </LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,53 @@
<?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:background="?selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeight"
android:orientation="horizontal"
android:paddingVertical="12dp"
android:paddingStart="?listPreferredItemPaddingStart">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="6dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:drawableTint="@color/warning"
tools:drawableStart="@drawable/ic_alert_outline"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[20]" />
</LinearLayout>
<ImageView
android:id="@+id/imageView_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/remove"
android:padding="?listPreferredItemPaddingEnd"
app:srcCompat="@drawable/ic_delete" />
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="?actionModeShareDrawable"
android:title="@string/share"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_remove"
android:icon="@drawable/ic_delete"
android:title="@string/delete"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save"
android:title="@string/save"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_select_all"
android:icon="?actionModeSelectAllDrawable"
android:title="@android:string/selectAll"
app:showAsAction="ifRoom|withText" />
</menu>

View File

@@ -20,7 +20,7 @@
<item <item
android:id="@+id/action_save" android:id="@+id/action_save"
android:orderInCategory="40" android:orderInCategory="40"
android:title="@string/save" android:title="@string/download"
android:visible="false" android:visible="false"
app:showAsAction="never" /> app:showAsAction="never" />

View File

@@ -9,4 +9,10 @@
android:title="@string/_import" android:title="@string/_import"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/settings"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -6,7 +6,7 @@
<string name="grid">شبكة</string> <string name="grid">شبكة</string>
<string name="list_mode">وضع القائمة</string> <string name="list_mode">وضع القائمة</string>
<string name="settings">إعدادات</string> <string name="settings">إعدادات</string>
<string name="remote_sources">المصادر البعيدة</string> <string name="remote_sources">مصادر المانجا</string>
<string name="chapters">فصول</string> <string name="chapters">فصول</string>
<string name="favourites">المفضلة</string> <string name="favourites">المفضلة</string>
<string name="network_error">‌خطاء في الشبكة</string> <string name="network_error">‌خطاء في الشبكة</string>

View File

@@ -179,7 +179,7 @@
<string name="text_history_holder_secondary">Вы можаце знайсці, што пачытаць, у бакавым меню.</string> <string name="text_history_holder_secondary">Вы можаце знайсці, што пачытаць, у бакавым меню.</string>
<string name="text_history_holder_primary">Тут будзе паказана манга, якую вы чытаеце</string> <string name="text_history_holder_primary">Тут будзе паказана манга, якую вы чытаеце</string>
<string name="text_search_holder_secondary">Паспрабуйце перафармуляваць запыт.</string> <string name="text_search_holder_secondary">Паспрабуйце перафармуляваць запыт.</string>
<string name="text_empty_holder_primary">Тут неяк пуста…</string> <string name="text_empty_holder_primary">Неяк тут пуста…</string>
<string name="chapter_is_missing">Глава адсутнічае</string> <string name="chapter_is_missing">Глава адсутнічае</string>
<string name="queued">У чарзе</string> <string name="queued">У чарзе</string>
<string name="about_app_translation_summary">Дапамагчы з перакладам праграмы</string> <string name="about_app_translation_summary">Дапамагчы з перакладам праграмы</string>
@@ -191,7 +191,7 @@
<string name="state_finished">Завершана</string> <string name="state_finished">Завершана</string>
<string name="state_ongoing">Ангоінг</string> <string name="state_ongoing">Ангоінг</string>
<string name="system_default">Па змаўчанні</string> <string name="system_default">Па змаўчанні</string>
<string name="exclude_nsfw_from_history">Не паказваць NSFW мангу з гісторыі</string> <string name="exclude_nsfw_from_history">Выключыць NSFW мангу з гісторыі</string>
<string name="show_pages_numbers">Паказваць нумары старонак</string> <string name="show_pages_numbers">Паказваць нумары старонак</string>
<string name="enabled_sources">Уключаныя крыніцы</string> <string name="enabled_sources">Уключаныя крыніцы</string>
<string name="available_sources">Даступныя крыніцы</string> <string name="available_sources">Даступныя крыніцы</string>
@@ -199,11 +199,11 @@
<string name="screenshots_allow">Дазваляць</string> <string name="screenshots_allow">Дазваляць</string>
<string name="screenshots_policy">Палітыка скрыншотаў</string> <string name="screenshots_policy">Палітыка скрыншотаў</string>
<string name="screenshots_block_all">Заўсёды блакуйце</string> <string name="screenshots_block_all">Заўсёды блакуйце</string>
<string name="screenshots_block_nsfw">Блок на NSFW</string> <string name="screenshots_block_nsfw">Забараніць для NSFW</string>
<string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string> <string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string>
<string name="disabled">Адключаны</string> <string name="disabled">Адключаны</string>
<string name="enabled">Уключаны</string> <string name="enabled">Уключаны</string>
<string name="exclude_nsfw_from_suggestions">Не прапануйце мангу NSFW</string> <string name="exclude_nsfw_from_suggestions">Ня прапаноўваць NSFW мангу</string>
<string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string> <string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string>
<string name="suggestions_info">Усе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца.</string> <string name="suggestions_info">Усе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца.</string>
<string name="suggestions_summary">Прапануеце мангу, заснаваную на вашых перавагах</string> <string name="suggestions_summary">Прапануеце мангу, заснаваную на вашых перавагах</string>
@@ -240,7 +240,7 @@
<string name="removed_from_history">Выдалена з гісторыі</string> <string name="removed_from_history">Выдалена з гісторыі</string>
<string name="dns_over_https">DNS праз HTTPS</string> <string name="dns_over_https">DNS праз HTTPS</string>
<string name="detect_reader_mode">Аўтавызначэнне рэжыму чытання</string> <string name="detect_reader_mode">Аўтавызначэнне рэжыму чытання</string>
<string name="detect_reader_mode_summary">Аўтаматычна вызначае, ці зяўляецца манга вэбтунам</string> <string name="detect_reader_mode_summary">Аўтаматычна вызначае, ці з\'яўляецца манга вэб-коміксам</string>
<string name="new_sources_text">Даступныя новыя крыніцы мангі</string> <string name="new_sources_text">Даступныя новыя крыніцы мангі</string>
<string name="download_slowdown">Запавольванне спампоўкі</string> <string name="download_slowdown">Запавольванне спампоўкі</string>
<string name="suggestions_excluded_genres">Выключыць жанры</string> <string name="suggestions_excluded_genres">Выключыць жанры</string>
@@ -299,7 +299,7 @@
<string name="different_languages">Розныя мовы</string> <string name="different_languages">Розныя мовы</string>
<string name="network_unavailable">Сетка недаступная</string> <string name="network_unavailable">Сетка недаступная</string>
<string name="network_unavailable_hint">Каб чытаць мангу онлайн, уключыце Wi-Fi або мабільную сетку</string> <string name="network_unavailable_hint">Каб чытаць мангу онлайн, уключыце Wi-Fi або мабільную сетку</string>
<string name="webtoon_zoom">Webtoon зум</string> <string name="webtoon_zoom">Маштабаванне ў рэжыме манхвы</string>
<string name="theme_name_dynamic">Дынамічны</string> <string name="theme_name_dynamic">Дынамічны</string>
<string name="color_theme">Каляровая гама</string> <string name="color_theme">Каляровая гама</string>
<string name="language">Мова</string> <string name="language">Мова</string>
@@ -426,7 +426,7 @@
<string name="show_pages_numbers_summary">Паказаць нумары старонак у ніжнім куце</string> <string name="show_pages_numbers_summary">Паказаць нумары старонак у ніжнім куце</string>
<string name="network">Сетка</string> <string name="network">Сетка</string>
<string name="data_and_privacy">Дадзеныя і канфідэнцыяльнасць</string> <string name="data_and_privacy">Дадзеныя і канфідэнцыяльнасць</string>
<string name="webtoon_zoom_summary">Дазволіць жэст для павелічэння ў рэжыме webtoon</string> <string name="webtoon_zoom_summary">Уключыць жэст павелічэння маштабу ў рэжыме манхвы</string>
<string name="details_button_tip">Націсніце і ўтрымлівайце кнопку \"Чытаць\", каб убачыць дадатковыя параметры</string> <string name="details_button_tip">Націсніце і ўтрымлівайце кнопку \"Чытаць\", каб убачыць дадатковыя параметры</string>
<string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string> <string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string>
<string name="reader_info_bar_summary">Паказаць бягучы час і ход чытання ў верхняй частцы экрана</string> <string name="reader_info_bar_summary">Паказаць бягучы час і ход чытання ў верхняй частцы экрана</string>

View File

@@ -8,7 +8,7 @@
<string name="grid">Πλέγμα</string> <string name="grid">Πλέγμα</string>
<string name="list_mode">Εμφάνιση ως λίστα</string> <string name="list_mode">Εμφάνιση ως λίστα</string>
<string name="settings">Ρυθμίσεις</string> <string name="settings">Ρυθμίσεις</string>
<string name="remote_sources">Απομακρυσμένες πηγές</string> <string name="remote_sources">Πηγές μάνγκα</string>
<string name="computing_">Επεξεργασία…</string> <string name="computing_">Επεξεργασία…</string>
<string name="close">Κλείσιμο</string> <string name="close">Κλείσιμο</string>
<string name="clear_history">Εκκαθάριση ιστορικού</string> <string name="clear_history">Εκκαθάριση ιστορικού</string>
@@ -51,7 +51,7 @@
<string name="text_delete_local_manga">Μόνιμη διαγραφή του \"%s\" από τη συσκευή;</string> <string name="text_delete_local_manga">Μόνιμη διαγραφή του \"%s\" από τη συσκευή;</string>
<string name="reader_settings">Ρυθμίσεις λειτουργίας ανάγνωσης</string> <string name="reader_settings">Ρυθμίσεις λειτουργίας ανάγνωσης</string>
<string name="switch_pages">Αλλαγή σελίδων</string> <string name="switch_pages">Αλλαγή σελίδων</string>
<string name="network_error">Αδυναμία σύνδεσης στο ίντερνετ</string> <string name="network_error">Σφάλμα δικτύου</string>
<string name="chapters">Κεφάλαια</string> <string name="chapters">Κεφάλαια</string>
<string name="details">Πληροφορίες</string> <string name="details">Πληροφορίες</string>
<string name="list">Λίστα</string> <string name="list">Λίστα</string>

View File

@@ -431,4 +431,12 @@
<string name="details_button_tip">Manten pulsado el botón Leer para ver más opciones</string> <string name="details_button_tip">Manten pulsado el botón Leer para ver más opciones</string>
<string name="restore_summary">Restaurar una copia de seguridad creada anteriormente</string> <string name="restore_summary">Restaurar una copia de seguridad creada anteriormente</string>
<string name="reader_info_bar_summary">Muestra la hora actual y el progreso de la lectura en la parte superior de la pantalla</string> <string name="reader_info_bar_summary">Muestra la hora actual y el progreso de la lectura en la parte superior de la pantalla</string>
<string name="clear_source_cookies_summary">Borrar las cookies solo para el dominio especificado. En la mayoría de los casos invalidará la autorización</string>
<string name="download_option_whole_manga">El manga completo</string>
<string name="download_option_first_n_chapters">Primero %s</string>
<string name="download_option_all_unread">Todos los capítulos sin leer</string>
<string name="download_option_all_unread_b">Todos los capítulos sin leer (%s)</string>
<string name="download_option_manual_selection">Selección manual de los capítulos</string>
<string name="download_option_all_chapters">Todos los capítulos con traducción %s</string>
<string name="download_option_next_unread_n_chapters">Siguiente %s sin leer</string>
</resources> </resources>

View File

@@ -2,19 +2,66 @@
<resources> <resources>
<string name="details">विवरण</string> <string name="details">विवरण</string>
<string name="chapters">अध्याय</string> <string name="chapters">अध्याय</string>
<string name="nothing_found">कुछ भी नहीं मिला</string> <string name="nothing_found">कुछ नहीं मिला</string>
<string name="history_is_empty">अभी तक कोई इतिहास नहीं है</string> <string name="history_is_empty">अभी तक कोई इतिहास नहीं है</string>
<string name="read">पढ़ना</string> <string name="read">पढ़ें</string>
<string name="add_to_favourites">इसे पसंद करें</string> <string name="add_to_favourites">इसे पसंद करें</string>
<string name="add">जोड़ना</string> <string name="add">जोड़</string>
<string name="save">बचाना</string> <string name="save">संचय करो</string>
<string name="newest">नवीनतम</string> <string name="newest">नवीनतम</string>
<string name="light">रोशनी</string> <string name="light">उजाला</string>
<string name="dark">अँधेरा</string> <string name="dark">अँधेरा</string>
<string name="close">बंद करना</string> <string name="close">बंद कर</string>
<string name="try_again">पुनः प्रयास करें</string> <string name="try_again">पुनः प्रयास करें</string>
<string name="you_have_not_favourites_yet">अभी तक कोई पसंदीदा नहीं है</string> <string name="you_have_not_favourites_yet">अभी तक कोई पसंदीदा नहीं है</string>
<string name="remove">निकालना</string> <string name="remove">निकाल</string>
<string name="by_name">नाम</string> <string name="by_name">नाम</string>
<string name="popular">लोकप्रिय</string> <string name="popular">लोकप्रिय</string>
<string name="local_storage">स्थानीय स्टॉरेज</string>
<string name="error_occurred">कोई त्रुटि हुई</string>
<string name="network_error">नेटवर्क समस्या</string>
<string name="favourites">पसंदीदा</string>
<string name="detailed_list">विस्तृत सूची</string>
<string name="settings">सेटिंग्स्</string>
<string name="list_mode">सूची रुपी</string>
<string name="chapter_d_of_d">अध्याय %1$d, %2$d में से</string>
<string name="computing_">गणना हो रही है…</string>
<string name="add_new_category">नई श्रेणी</string>
<string name="clear_history">इतिहास मिटाए</string>
<string name="share">भेजो</string>
<string name="create_shortcut">शॉर्टकट बनाएं…</string>
<string name="share_s">%s भेजो</string>
<string name="search">खोजो</string>
<string name="search_manga">मांगा खोजो</string>
<string name="manga_downloading_">डाउनलोड हो रहा है…</string>
<string name="downloads">डाउनलोड किए गए मांगा</string>
<string name="by_rating">रेटिंग</string>
<string name="clear">साफ करें</string>
<string name="page_saved">पन्ना संचय हो गया</string>
<string name="share_image">चित्र को भेजें</string>
<string name="delete">मिटाएं</string>
<string name="clear_pages_cache">पन्ने के कैछ को मिटाएं</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">सामान्य</string>
<string name="webtoon">वैबटून</string>
<string name="remote_sources">मांगा स्रोत</string>
<string name="download_complete">डाउनलोड हो गया</string>
<string name="processing_">प्रक्रिया चल रही है…</string>
<string name="history">इतिहास</string>
<string name="grid">ग्रिड</string>
<string name="loading_">लोड हो रहा है…</string>
<string name="text_file_not_supported">या तो झीप नहीं तो सीबीझेड फाईल को चुनें।</string>
<string name="updated">अपडेट हो गया</string>
<string name="_s_deleted_from_local_storage">\"%s\", स्थानीय स्टॉरेज में से मिट गईं</string>
<string name="text_clear_history_prompt">पढ़ने का इतिहास सदा के लिए मिटाए\?</string>
<string name="save_page">पन्ना संचय करो</string>
<string name="_import">आयात करें</string>
<string name="operation_not_supported">यह कार्य समर्थित नहीं है</string>
<string name="sort_order">छंटाई क्रम</string>
<string name="list">सूची</string>
<string name="filter">फिल्टर</string>
<string name="theme">थीम</string>
<string name="automatic">फोन जैसा</string>
<string name="pages">पन्ने</string>
<string name="no_description">कोई विवरण नहीं है</string>
</resources> </resources>

View File

@@ -431,4 +431,12 @@
<string name="webtoon_zoom_summary">Izinkan gerakan zoom in dalam mode webtoon</string> <string name="webtoon_zoom_summary">Izinkan gerakan zoom in dalam mode webtoon</string>
<string name="reader_info_bar_summary">Tampilkan waktu saat ini dan kemajuan pembacaan di bagian atas layar</string> <string name="reader_info_bar_summary">Tampilkan waktu saat ini dan kemajuan pembacaan di bagian atas layar</string>
<string name="pages_animation_summary">Animasikan peralihan halaman</string> <string name="pages_animation_summary">Animasikan peralihan halaman</string>
<string name="clear_source_cookies_summary">Hapus cookie hanya untuk domain tertentu. Dalam kebanyakan kasus akan membatalkan otorisasi</string>
<string name="download_option_whole_manga">Seluruh manga</string>
<string name="download_option_first_n_chapters">Pertama %s</string>
<string name="download_option_all_unread">Semua bab yang belum dibaca</string>
<string name="download_option_all_unread_b">Semua bab yang belum dibaca (%s)</string>
<string name="download_option_all_chapters">Semua bab dengan terjemahan %s</string>
<string name="download_option_next_unread_n_chapters">Belum dibaca %s</string>
<string name="download_option_manual_selection">Pilih bab secara manual</string>
</resources> </resources>

View File

@@ -43,7 +43,7 @@
<string name="close">Chiudi</string> <string name="close">Chiudi</string>
<string name="chapter_d_of_d">Capitolo %1$d di %2$d</string> <string name="chapter_d_of_d">Capitolo %1$d di %2$d</string>
<string name="loading_">Caricamento…</string> <string name="loading_">Caricamento…</string>
<string name="remote_sources">Fonti remote</string> <string name="remote_sources">Fonti manga</string>
<string name="settings">Impostazioni</string> <string name="settings">Impostazioni</string>
<string name="list_mode">Modalità elenco</string> <string name="list_mode">Modalità elenco</string>
<string name="grid">Griglia</string> <string name="grid">Griglia</string>
@@ -203,7 +203,7 @@
<string name="filter_load_error">Impossibile caricare la lista dei generi</string> <string name="filter_load_error">Impossibile caricare la lista dei generi</string>
<string name="suggestions_enable">Abilita i suggerimenti</string> <string name="suggestions_enable">Abilita i suggerimenti</string>
<string name="suggestions_summary">Suggerisci manga in base alle tue preferenze</string> <string name="suggestions_summary">Suggerisci manga in base alle tue preferenze</string>
<string name="suggestions_info">Tutti i dati sono analizzati localmente su questo dispositivo. Non c\'è trasferimento dei suoi dati personali a nessun servizio</string> <string name="suggestions_info">Tutti i dati vengono analizzati solo localmente su questo dispositivo e mai inviati da nessuna parte.</string>
<string name="text_suggestion_holder">Inizia a leggere manga e riceverai suggerimenti personalizzati</string> <string name="text_suggestion_holder">Inizia a leggere manga e riceverai suggerimenti personalizzati</string>
<string name="suggestions">Suggerimenti</string> <string name="suggestions">Suggerimenti</string>
<string name="enabled">Abilitato</string> <string name="enabled">Abilitato</string>
@@ -229,7 +229,7 @@
<string name="text_delete_local_manga_batch">Eliminare gli elementi selezionati dal dispositivo in modo permanente\?</string> <string name="text_delete_local_manga_batch">Eliminare gli elementi selezionati dal dispositivo in modo permanente\?</string>
<string name="download_slowdown">Rallentamento dello scaricamento</string> <string name="download_slowdown">Rallentamento dello scaricamento</string>
<string name="local_manga_processing">Elaborazione dei manga salvati</string> <string name="local_manga_processing">Elaborazione dei manga salvati</string>
<string name="chapters_will_removed_background">I capitoli saranno rimossi in sfondo. Può richiedere un po\' di tempo</string> <string name="chapters_will_removed_background">I capitoli verranno rimossi in background</string>
<string name="download_slowdown_summary">Aiuta ad evitare il blocco del tuo indirizzo IP</string> <string name="download_slowdown_summary">Aiuta ad evitare il blocco del tuo indirizzo IP</string>
<string name="hide">Nascondi</string> <string name="hide">Nascondi</string>
<string name="new_sources_text">Sono disponibili nuove fonti di manga</string> <string name="new_sources_text">Sono disponibili nuove fonti di manga</string>

View File

@@ -125,4 +125,87 @@
<string name="volume_buttons">Дыбыс батырмалары</string> <string name="volume_buttons">Дыбыс батырмалары</string>
<string name="error">Қате</string> <string name="error">Қате</string>
<string name="clear_search_history">Іздеу тарихын тазалау</string> <string name="clear_search_history">Іздеу тарихын тазалау</string>
<string name="track_sources">Жаңартуларды қарау</string>
<string name="wrong_password">Қате құпиясөз</string>
<string name="protect_application">Қолданбаны қорғау</string>
<string name="passwords_mismatch">Құпиясөз бірдей емес</string>
<string name="app_version">%s нұсқа</string>
<string name="check_for_updates">Жаңартуды тексеру</string>
<string name="right_to_left">Оңнан солға</string>
<string name="zoom_mode_fit_height">Биіктігіне қарай қою</string>
<string name="zoom_mode_fit_width">Еніне қарай қою</string>
<string name="zoom_mode_keep_start">Өзгертпеу</string>
<string name="black_dark_theme">Қара</string>
<string name="black_dark_theme_summary">AMOLED экранда азырақ қуат жейді</string>
<string name="create_backup">Сақтық көшірме жасау</string>
<string name="restore_backup">Сақтық көшірмеден қалыпқа келтіру</string>
<string name="file_not_found">Файл табылмады</string>
<string name="data_restored_success">Түгел дерек қалыпқа келді</string>
<string name="backup_information">Таңдаулы мен тарихтың сақтық көшірмесін жасап, оны қалпына келтіре аласыз</string>
<string name="just_now">Жаңа ғана</string>
<string name="yesterday">Кеше</string>
<string name="long_ago">Бұрын</string>
<string name="group">Топтау</string>
<string name="today">Бүгін</string>
<string name="tap_to_try_again">Қайталап көру</string>
<string name="reader_mode_hint">Таңдалған пішімдеу осы маңга үшін сақталады</string>
<string name="silent">Дыбыссыз</string>
<string name="captcha_required">CAPTCHA өтіңіз</string>
<string name="captcha_solve">Өту</string>
<string name="clear_cookies">Кукиді тазалау</string>
<string name="cookies_cleared">Куки файлдары жойылды</string>
<string name="reverse">Керісінше</string>
<string name="sign_in">Кіру</string>
<string name="auth_required">Бұны көру үшін тіркелгіге кіріңіз</string>
<string name="default_s">Әдепкі: %s</string>
<string name="chapter_is_missing">Тарау жоқ</string>
<string name="about_app_translation_summary">Қолданбаны аудару</string>
<string name="about_app_translation">Аудару</string>
<string name="auth_not_supported_by">%s кіру қолжетімсіз</string>
<string name="state_finished">Аяқталған</string>
<string name="text_clear_updates_feed_prompt">Жаңарту тарихын толықтау тазартайық па\?</string>
<string name="no_update_available">Жаңарту жоқ</string>
<string name="backup_restore">Сақтық көшірме мен қалпына келтіру</string>
<string name="dont_check">Тексермеу</string>
<string name="enter_password">Құпиясөзді енгізіңіз</string>
<string name="about">Қолданба туралы</string>
<string name="create_category">Жаңа санат</string>
<string name="data_restored">Қалыпқа келді</string>
<string name="preparing_">Дайындау…</string>
<string name="data_restored_with_errors">Дерек қалыпқа келсе де, сәл қате шығып қалды</string>
<string name="exclude_nsfw_from_history">ҰЯТСЫЗ маңганы тарихта көрсетпеу</string>
<string name="state_ongoing">Шығып жатыр</string>
<string name="show_pages_numbers">Беттерді нөмірлеу</string>
<string name="system_default">Әдепкі</string>
<string name="available_sources">Қолжетімді дереккөздер</string>
<string name="enabled_sources">Қосылып тұрған дереккөздер</string>
<string name="suggestions_summary">Ұнайды ма деген маңганы ұсыну</string>
<string name="screenshots_block_nsfw">ҰЯТСЫЗ үшін бөгеу</string>
<string name="suggestions_enable">Ұсынымды қосу</string>
<string name="check_for_new_chapters">Жаңа тарау іздеу</string>
<string name="auth_complete">Сәтті тіркелдіңіз</string>
<string name="text_clear_cookies_prompt">Түгел дереккөзден шығып кетесіз</string>
<string name="genres">Түрлер</string>
<string name="screenshots_policy">Скриншот саясаты</string>
<string name="suggestions">Ұсыным</string>
<string name="screenshots_allow">Рұқсат беру</string>
<string name="screenshots_block_all">Әрқашан бөгеу</string>
<string name="text_suggestion_holder">Жеке ұсыныс алу үшін маңга оқып бастаңыз</string>
<string name="disabled">Өшірулі</string>
<string name="exclude_nsfw_from_suggestions">ҰЯТСЫЗ маңга ұсынбау</string>
<string name="enabled">Қосулы</string>
<string name="protect_application_summary">Қолданбаны қосқанда құпиясөз сұрау</string>
<string name="repeat_password">Құпиясөзді қайталаңыз</string>
<string name="zoom_mode_fit_center">Ортаға қою</string>
<string name="scale_mode">Өлшеу режімі</string>
<string name="text_clear_search_history_prompt">Соңғы іздеу тарихын толықтай жоямыз ба\?</string>
<string name="welcome">Қош келдіңіз</string>
<string name="backup_saved">Сақтау көшірмесі дайын</string>
<string name="tracker_warning">Кейбір құрылғылардың жүйесі бөлек, одан аялық тапсырмалар бұзылуы мүмкін.</string>
<string name="read_more">Толығырақ оқу</string>
<string name="queued">Кезекте</string>
<string name="next">Келесі</string>
<string name="protect_application_subtitle">Қолданбаға кіру үшін құпиясөз енгізіңіз</string>
<string name="confirm">Растау</string>
<string name="password_length_hint">Құпиясөзде 4, не одан көп таңба болу керек</string>
</resources> </resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_order">정렬 기준</string> <string name="sort_order">정렬 기준</string>
<string name="_import">불러오기</string> <string name="_import">불러오기</string>
<string name="network_error">네트워크 오류</string> <string name="network_error">네트워크 오류</string>
@@ -116,12 +116,12 @@
<string name="filter_load_error">장르 목록을 불러올 수 없음</string> <string name="filter_load_error">장르 목록을 불러올 수 없음</string>
<string name="chapters_empty">이 만화는 챕터로 나눠져 있지 않습니다</string> <string name="chapters_empty">이 만화는 챕터로 나눠져 있지 않습니다</string>
<string name="search_chapters">챕터 찾아보기</string> <string name="search_chapters">챕터 찾아보기</string>
<string name="chapters_will_removed_background">챕터들이 백그라운드에서 제거됩니다. 이 작업은 많은 시간이 소요될 수 있습니다</string> <string name="chapters_will_removed_background">챕터들이 백그라운드에서 제거됩니다</string>
<string name="download_slowdown_summary">IP 차단을 회피할 수 있게 합니다</string> <string name="download_slowdown_summary">IP 차단을 회피할 수 있게 합니다</string>
<string name="check_new_chapters_title">새로운 챕터가 나오면 알려주기</string> <string name="check_new_chapters_title">새로운 챕터가 나오면 알려주기</string>
<string name="standard">스탠다드</string> <string name="standard">스탠다드</string>
<string name="text_local_holder_secondary">온라인 소스 혹은 직접 파일을 불러와 저장하기.</string> <string name="text_local_holder_secondary">온라인 소스 혹은 직접 파일을 불러와 저장하기.</string>
<string name="suggestions_info">모든 데이터는 기기 안에서만 분석 및 사용되며 어떠한 서드파티 서비스들과도 공유되지 않습니다</string> <string name="suggestions_info">모든 데이터는 이 장치에서 로컬로 분석되며 아무데도 전송되지 않습니다.</string>
<string name="suggestions_summary">당신의 선호도를 바탕으로 만화를 추천합니다</string> <string name="suggestions_summary">당신의 선호도를 바탕으로 만화를 추천합니다</string>
<string name="bookmark_add">북마크에 추가</string> <string name="bookmark_add">북마크에 추가</string>
<string name="bookmark_remove">북마크 제거</string> <string name="bookmark_remove">북마크 제거</string>
@@ -135,7 +135,7 @@
<string name="detailed_list">자세한 목록</string> <string name="detailed_list">자세한 목록</string>
<string name="list_mode">설정</string> <string name="list_mode">설정</string>
<string name="grid">그리드</string> <string name="grid">그리드</string>
<string name="remote_sources">소스 사이트 관리</string> <string name="remote_sources">만화 소스</string>
<string name="clear_history">기록 삭제</string> <string name="clear_history">기록 삭제</string>
<string name="add">추가</string> <string name="add">추가</string>
<string name="history_is_empty">아직 기록이 없습니다</string> <string name="history_is_empty">아직 기록이 없습니다</string>
@@ -305,4 +305,22 @@
<string name="sync_title">데이터 동기화 하기</string> <string name="sync_title">데이터 동기화 하기</string>
<string name="email_enter_hint">이메일을 입력하여 계속</string> <string name="email_enter_hint">이메일을 입력하여 계속</string>
<string name="download_slowdown">다운로드 속도 늦추기</string> <string name="download_slowdown">다운로드 속도 늦추기</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d 의%2$d 에</string>
<string name="invalid_port_number">잘못된 포트 번호</string>
<string name="authorization_optional">권한 부여(선택 사항)</string>
<string name="password">비밀번호</string>
<string name="show_pages_numbers_summary">하단 모서리에 페이지 번호 표시</string>
<string name="reader_info_bar_summary">화면 상단에 현재 시간 및 읽기 진행률 표시</string>
<string name="data_and_privacy">데이터 및 개인정보 보호</string>
<string name="network">네트워크</string>
<string name="images_proxy_title">이미지 최적화 프록시</string>
<string name="images_procy_description">사용 wsrv.nl 가능한 경우 트래픽 사용량을 줄이고 이미지 로딩 속도를 높이는 서비스</string>
<string name="username">사용자 이름</string>
<string name="translations">번역</string>
<string name="web_view_unavailable">WebView를 사용할 수 없음: WebView 공급자가 설치되어 있는지 확인하십시오</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="show_pages_numbers">페이지 번호 매기기</string>
<string name="never">절대</string>
<string name="clear_network_cache">네트워크 캐시 지우기</string>
<string name="invalid_value_message">잘못된 값</string>
</resources> </resources>

View File

@@ -25,7 +25,7 @@
<string name="pages">Sider</string> <string name="pages">Sider</string>
<string name="clear">Tøm</string> <string name="clear">Tøm</string>
<string name="text_clear_history_prompt">Tøm lesehistorikken for godt\?</string> <string name="text_clear_history_prompt">Tøm lesehistorikken for godt\?</string>
<string name="remove">Tak bort</string> <string name="remove">Ta bort</string>
<string name="save_page">Hent sida</string> <string name="save_page">Hent sida</string>
<string name="page_saved">Henta</string> <string name="page_saved">Henta</string>
<string name="share_image">Del biletet</string> <string name="share_image">Del biletet</string>
@@ -33,7 +33,7 @@
<string name="delete">Slett</string> <string name="delete">Slett</string>
<string name="text_file_not_supported">Vel ei ZIP- eller CBZ-fil.</string> <string name="text_file_not_supported">Vel ei ZIP- eller CBZ-fil.</string>
<string name="no_description">Ingen utgreiing</string> <string name="no_description">Ingen utgreiing</string>
<string name="clear_pages_cache">Tøm mellomminnet til sida</string> <string name="clear_pages_cache">Tøm mellomminnet for sider</string>
<string name="standard">Vanleg</string> <string name="standard">Vanleg</string>
<string name="webtoon">Nettserie</string> <string name="webtoon">Nettserie</string>
<string name="grid_size">Rutenettstorleik</string> <string name="grid_size">Rutenettstorleik</string>
@@ -112,7 +112,7 @@
<string name="create_category">Ny hop</string> <string name="create_category">Ny hop</string>
<string name="zoom_mode_fit_center">Midtstill</string> <string name="zoom_mode_fit_center">Midtstill</string>
<string name="zoom_mode_keep_start">Auk byrjinga av sida</string> <string name="zoom_mode_keep_start">Auk byrjinga av sida</string>
<string name="restore_backup">Gjenopprett ifrå ein tryggleikskopi</string> <string name="restore_backup">Gjenopprett</string>
<string name="backup_information">Du kan tryggleikskopiera historikken og likerlista di til seinare gjenoppretting</string> <string name="backup_information">Du kan tryggleikskopiera historikken og likerlista di til seinare gjenoppretting</string>
<string name="preparing_">Førebur …</string> <string name="preparing_">Førebur …</string>
<string name="system_default">Forval</string> <string name="system_default">Forval</string>
@@ -163,7 +163,7 @@
<string name="exclude_nsfw_from_suggestions">Ikkje rå mangaar med vakse innhald</string> <string name="exclude_nsfw_from_suggestions">Ikkje rå mangaar med vakse innhald</string>
<string name="enabled">Påslegen</string> <string name="enabled">Påslegen</string>
<string name="filter_load_error">Kunne ikkje hente inn slaglista</string> <string name="filter_load_error">Kunne ikkje hente inn slaglista</string>
<string name="various_languages">Ulike mål</string> <string name="various_languages">Fleire språk</string>
<string name="percent_string_pattern">%1$s%%</string> <string name="percent_string_pattern">%1$s%%</string>
<string name="suggestions_updating">Oppdaterer råd</string> <string name="suggestions_updating">Oppdaterer råd</string>
<string name="suggestions_excluded_genres">Utelat slag</string> <string name="suggestions_excluded_genres">Utelat slag</string>
@@ -184,7 +184,7 @@
<string name="empty_favourite_categories">Ingen likte hopar</string> <string name="empty_favourite_categories">Ingen likte hopar</string>
<string name="logout">Logg ut</string> <string name="logout">Logg ut</string>
<string name="bookmark_add">Bokmerk</string> <string name="bookmark_add">Bokmerk</string>
<string name="bookmark_remove">Tak bort bokmerket</string> <string name="bookmark_remove">Ta bort bokmerket</string>
<string name="bookmarks">Bokmerke</string> <string name="bookmarks">Bokmerke</string>
<string name="removed_from_history">Teken bort ifrå historikken</string> <string name="removed_from_history">Teken bort ifrå historikken</string>
<string name="dns_over_https">DNS over HTTPS</string> <string name="dns_over_https">DNS over HTTPS</string>
@@ -218,7 +218,7 @@
<string name="reorder">Flytt</string> <string name="reorder">Flytt</string>
<string name="empty">Tom</string> <string name="empty">Tom</string>
<string name="confirm_exit">Trykk Attende att for å gå or appen</string> <string name="confirm_exit">Trykk Attende att for å gå or appen</string>
<string name="exit_confirmation">Utgåingsstadfesting</string> <string name="exit_confirmation">Stadfest apputgåing</string>
<string name="pages_cache">Mellominnet for sider</string> <string name="pages_cache">Mellominnet for sider</string>
<string name="other_cache">Mellomminnet for anna</string> <string name="other_cache">Mellomminnet for anna</string>
<string name="available">Tilgjengeleg</string> <string name="available">Tilgjengeleg</string>
@@ -237,17 +237,17 @@
<string name="error_no_space_left">Eininga er fylt</string> <string name="error_no_space_left">Eininga er fylt</string>
<string name="reader_slider">Vis ei rulleline til blading</string> <string name="reader_slider">Vis ei rulleline til blading</string>
<string name="webtoon_zoom">Auk/mink nettseriar</string> <string name="webtoon_zoom">Auk/mink nettseriar</string>
<string name="different_languages">Ulike mål</string> <string name="different_languages">Ulike språk</string>
<string name="network_unavailable">Nettverk ikkje tilgjengeleg</string> <string name="network_unavailable">Nettverk ikkje tilgjengeleg</string>
<string name="network_unavailable_hint">Slå på Wi-Fi eller mobilnettverk for å lesa mangaar på nett</string> <string name="network_unavailable_hint">Slå på Wi-Fi eller mobilnettverk for å lesa mangaar på nett</string>
<string name="server_error">Tjenarfeil (%1$d). Røyn att seinare</string> <string name="server_error">Tjenarfeil (%1$d). Røyn att seinare</string>
<string name="source_disabled">Kjelde avslegen</string> <string name="source_disabled">Kjelde avslegen</string>
<string name="prefetch_content">Hent inn innhald på forhand</string> <string name="prefetch_content">Hent inn innhald på forhand</string>
<string name="mark_as_current">Merk som aktuelt</string> <string name="mark_as_current">Merk som aktuelt</string>
<string name="language">Mål</string> <string name="language">Språk</string>
<string name="share_logs">Del loggføringar</string> <string name="share_logs">Del loggføringar</string>
<string name="enable_logging">Slå på loggføring</string> <string name="enable_logging">Loggfør</string>
<string name="enable_logging_summary">Tak opp nokre gjerder til bruk i istandsetjing.</string> <string name="enable_logging_summary">Ta opp nokre gjerder til bruk i istandsetjing</string>
<string name="theme_name_dynamic">Skiftande</string> <string name="theme_name_dynamic">Skiftande</string>
<string name="color_theme">Letar</string> <string name="color_theme">Letar</string>
<string name="show_in_grid_view">Vis som rutenett</string> <string name="show_in_grid_view">Vis som rutenett</string>
@@ -272,7 +272,7 @@
<string name="protect_application_subtitle">Vern appen med eit lykelord</string> <string name="protect_application_subtitle">Vern appen med eit lykelord</string>
<string name="suggestions_info">All data vert handsama på eininga di og vert ikkje førte over til noka teneste</string> <string name="suggestions_info">All data vert handsama på eininga di og vert ikkje førte over til noka teneste</string>
<string name="disabled">Avslegen</string> <string name="disabled">Avslegen</string>
<string name="onboard_text">Vel måla du vil lesa mangaar på. Du kan brigde på dette seinare i innstillingane.</string> <string name="onboard_text">Vel språka du vil lesa mangaar på. Du kan endre på dette seinare i innstillingane.</string>
<string name="never">Aldri</string> <string name="never">Aldri</string>
<string name="only_using_wifi">Berre på Wi-Fi</string> <string name="only_using_wifi">Berre på Wi-Fi</string>
<string name="nsfw">18+</string> <string name="nsfw">18+</string>
@@ -360,4 +360,48 @@
<string name="downloads_wifi_only">Hent berre på WiFi</string> <string name="downloads_wifi_only">Hent berre på WiFi</string>
<string name="downloads_wifi_only_summary">Stans all henting når du byter til eit mobilnettverk</string> <string name="downloads_wifi_only_summary">Stans all henting når du byter til eit mobilnettverk</string>
<string name="find_similar">Finn liknande</string> <string name="find_similar">Finn liknande</string>
<string name="sort_order">Skiljingsrekkjefølgd</string>
<string name="auth_not_supported_by">Innlogging på %s er ikkje stødd</string>
<string name="sync_host_description">Du kan nytte ein sjølvhusa synkroniseringstenar, eller den vanlege. Om du er uviss, ikkje rør.</string>
<string name="cancel_all_downloads_confirm">Alle pågåande hentingar vert avbrotne, og uheile data sletta</string>
<string name="network">Nettverk</string>
<string name="reader_info_bar_summary">Vis klokka og leseframgangen på toppen av skjermen</string>
<string name="default_s">Forvalt: %s</string>
<string name="reset_filter">Attendestill silen</string>
<string name="report">Sei ifrå</string>
<string name="storage_usage">Gøymebruk</string>
<string name="history_shortcuts">Vis snarvegen for nylege mangaar</string>
<string name="sync_settings">Synkroniseringsinnstillingar</string>
<string name="server_address">Tenaradresse</string>
<string name="resume">Hald fram</string>
<string name="remove_completed">Ta bort fullgjorde</string>
<string name="cancel_all">Avbryt alle</string>
<string name="type">Slag</string>
<string name="address">Adresse</string>
<string name="downloaded">Henta</string>
<string name="authorization_optional">Godkjenning (valfri)</string>
<string name="invert_colors">Snu på letane</string>
<string name="username">Brukarnamn</string>
<string name="password">Passord</string>
<string name="data_and_privacy">Data og personvern</string>
<string name="restore_summary">Gjenopprett ifrå ein tryggleiskopi</string>
<string name="show_pages_numbers_summary">Vis sidetal i nedre hjørne</string>
<string name="details_button_tip">Trykk og hald leseknappen for å sjå fleire val</string>
<string name="translations">Omsetjing</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="no_thanks">Nei takk</string>
<string name="enable">Slå på</string>
<string name="clear_network_cache">Tøm nettverksmellomminnet</string>
<string name="suggestion_manga">Råd: %s</string>
<string name="suggestions_notifications_summary">Ein gong iblant, vis varsel med rådde mangaar</string>
<string name="more">Meir</string>
<string name="remove_completed_downloads_confirm">Hentehistorikken din vil verta sletta for godt</string>
<string name="text_downloads_list_holder">Du har ikkje henta noko</string>
<string name="downloads_resumed">Heldt fram hentingane</string>
<string name="downloads_removed">Tok bort hentingane</string>
<string name="downloads_cancelled">Avbraut hentingane</string>
<string name="suggestions_enable_prompt">Vil du få personlege mangaråd\?</string>
<string name="downloads_paused">Stansa hentingane</string>
<string name="sync_auth_hint">Du kan logge inn på ein konto du alt har, eller lage ein ny ein</string>
<string name="invalid_value_message">Ugild verdi</string>
</resources> </resources>

View File

@@ -21,7 +21,7 @@
<string name="search_manga">Szukaj mang</string> <string name="search_manga">Szukaj mang</string>
<string name="manga_downloading_">Pobieranie…</string> <string name="manga_downloading_">Pobieranie…</string>
<string name="download_complete">Pobrano</string> <string name="download_complete">Pobrano</string>
<string name="downloads">Pobrane</string> <string name="downloads">Pobrania</string>
<string name="by_name">Nazwa</string> <string name="by_name">Nazwa</string>
<string name="popular">Popularność</string> <string name="popular">Popularność</string>
<string name="newest">Najnowsze</string> <string name="newest">Najnowsze</string>
@@ -413,4 +413,22 @@
<string name="port">Port</string> <string name="port">Port</string>
<string name="proxy">Proxy</string> <string name="proxy">Proxy</string>
<string name="sync_auth_hint">Możesz zalogować się na istniejące konto lub utworzyć nowe</string> <string name="sync_auth_hint">Możesz zalogować się na istniejące konto lub utworzyć nowe</string>
<string name="password">Hasło</string>
<string name="invalid_value_message">Nieprawidłowa wartość</string>
<string name="images_proxy_title">Proxy optymalizacji obrazów</string>
<string name="images_procy_description">Użyj wsrv.nl usługa zmniejszająca zużycie ruchu i przyspieszająca ładowanie obrazu, jeśli to możliwe</string>
<string name="downloaded">Pobrane</string>
<string name="username">Nazwa użytkownika</string>
<string name="authorization_optional">Autoryzacja (opcjonalnie)</string>
<string name="invert_colors">Odwróć kolory</string>
<string name="invalid_port_number">Nieprawidłowy numer portu</string>
<string name="network">Sieć</string>
<string name="data_and_privacy">Dane i prywatność</string>
<string name="restore_summary">Przywróć wcześniej utworzoną kopię zapasową</string>
<string name="webtoon_zoom_summary">Zezwalaj na powiększanie gestu w trybie webtoon</string>
<string name="show_pages_numbers_summary">Pokaż numery stron w dolnym rogu</string>
<string name="pages_animation_summary">Animacja przewracania stron</string>
<string name="reader_info_bar_summary">Pokaż aktualny czas i postęp czytania u góry ekranu</string>
<string name="details_button_tip">Naciśnij i przytrzymaj przycisk Czytaj, aby zobaczyć więcej opcji</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
</resources> </resources>

View File

@@ -100,10 +100,10 @@
<string name="manga_shelf">Estante</string> <string name="manga_shelf">Estante</string>
<string name="done">Feito</string> <string name="done">Feito</string>
<string name="zoom_mode_keep_start">Manter no início</string> <string name="zoom_mode_keep_start">Manter no início</string>
<string name="clear_updates_feed">Limpar feed de atualizações</string> <string name="clear_updates_feed">Limpar atualizações de fluxo</string>
<string name="updates_feed_cleared">Limpo</string> <string name="updates_feed_cleared">Limpo</string>
<string name="update">Atualizar</string> <string name="update">Atualizar</string>
<string name="feed_will_update_soon">A atualização do feed começará em breve</string> <string name="feed_will_update_soon">A atualização do fluxo começará em breve</string>
<string name="track_sources">Procure atualizações</string> <string name="track_sources">Procure atualizações</string>
<string name="dont_check">Não verifique</string> <string name="dont_check">Não verifique</string>
<string name="enter_password">Digite a senha</string> <string name="enter_password">Digite a senha</string>
@@ -137,7 +137,7 @@
<string name="captcha_required">CAPTCHA obrigatório</string> <string name="captcha_required">CAPTCHA obrigatório</string>
<string name="captcha_solve">Resolver</string> <string name="captcha_solve">Resolver</string>
<string name="cookies_cleared">Todos os cookies foram removidos</string> <string name="cookies_cleared">Todos os cookies foram removidos</string>
<string name="clear_feed">Limpar feed</string> <string name="clear_feed">Limpar o fluxo</string>
<string name="text_clear_updates_feed_prompt">Limpar todo o histórico de atualizações permanentemente\?</string> <string name="text_clear_updates_feed_prompt">Limpar todo o histórico de atualizações permanentemente\?</string>
<string name="check_for_new_chapters">Em busca de novos capítulos</string> <string name="check_for_new_chapters">Em busca de novos capítulos</string>
<string name="reverse">Reverter</string> <string name="reverse">Reverter</string>
@@ -185,7 +185,7 @@
<string name="protect_application">Proteja a app</string> <string name="protect_application">Proteja a app</string>
<string name="protect_application_summary">Peça a senha ao iniciar o Kotatsu</string> <string name="protect_application_summary">Peça a senha ao iniciar o Kotatsu</string>
<string name="zoom_mode_fit_height">Ajustar à altura</string> <string name="zoom_mode_fit_height">Ajustar à altura</string>
<string name="black_dark_theme">Escuro</string> <string name="black_dark_theme">Preto</string>
<string name="black_dark_theme_summary">Usa menos energia em telas AMOLED</string> <string name="black_dark_theme_summary">Usa menos energia em telas AMOLED</string>
<string name="reader_mode_hint">A configuração escolhida será lembrada para este mangá</string> <string name="reader_mode_hint">A configuração escolhida será lembrada para este mangá</string>
<string name="backup_information">Pode criar um backup do seu histórico e favoritos e restaurá-lo</string> <string name="backup_information">Pode criar um backup do seu histórico e favoritos e restaurá-lo</string>
@@ -341,7 +341,7 @@
<string name="import_completed">Importação concluída</string> <string name="import_completed">Importação concluída</string>
<string name="import_completed_hint">Você pode excluir o arquivo original do armazenamento para economizar espaço</string> <string name="import_completed_hint">Você pode excluir o arquivo original do armazenamento para economizar espaço</string>
<string name="import_will_start_soon">A importação começará em breve</string> <string name="import_will_start_soon">A importação começará em breve</string>
<string name="feed">Fluxo de conteúdo</string> <string name="feed">Fluxo</string>
<string name="manga_error_description_pattern">Detalhes do erro:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Tente &lt;a href=%2$s&gt;abra a página do mangá em um navegador da web&lt;/a&gt; para garantir que o mesmo esteja disponível em sua fonte&lt;br&gt;2. Se estiver disponível, envie um relatório de erro para os desenvolvedores.</string> <string name="manga_error_description_pattern">Detalhes do erro:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Tente &lt;a href=%2$s&gt;abra a página do mangá em um navegador da web&lt;/a&gt; para garantir que o mesmo esteja disponível em sua fonte&lt;br&gt;2. Se estiver disponível, envie um relatório de erro para os desenvolvedores.</string>
<string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita sempre muda para a próxima página</string> <string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita sempre muda para a próxima página</string>
<string name="reader_control_ltr">Controle ergonômico do leitor</string> <string name="reader_control_ltr">Controle ergonômico do leitor</string>
@@ -371,4 +371,64 @@
<string name="details_button_tip">Pressione e segure o botão Ler para ver mais opções</string> <string name="details_button_tip">Pressione e segure o botão Ler para ver mais opções</string>
<string name="webtoon_zoom">Zoom Webtoon</string> <string name="webtoon_zoom">Zoom Webtoon</string>
<string name="allow_unstable_updates_summary">Propor atualizações para versões beta do aplicativo</string> <string name="allow_unstable_updates_summary">Propor atualizações para versões beta do aplicativo</string>
<string name="got_it">Entendi</string>
<string name="sources_reorder_tip">Toque e segure em um item para reordená-lo</string>
<string name="comics_archive_import_description">Você pode selecionar um ou mais arquivos .cbz ou .zip, cada arquivo será reconhecido como um mangá separado.</string>
<string name="folder_with_images_import_description">Você pode selecionar um diretório com arquivos ou imagens. Cada arquivo (ou subdiretório) será reconhecido como um capítulo.</string>
<string name="sync_host_description">Você pode usar um servidor de sincronização auto-hospedado ou um padrão. Não mude isso se não tiver certeza do que está fazendo.</string>
<string name="address">Endereço</string>
<string name="port">Porta</string>
<string name="data_and_privacy">Dados e privacidade</string>
<string name="invalid_port_number">Número de porta inválido</string>
<string name="network">Rede</string>
<string name="restore_summary">Restaurar backup criado anteriormente</string>
<string name="webtoon_zoom_summary">Permitir zoom no gesto no modo webtoon</string>
<string name="reader_info_bar_summary">Mostrar a hora atual e o progresso da leitura na parte superior da tela</string>
<string name="pages_animation_summary">Animação de virada de página</string>
<string name="show_on_shelf">Mostrar na prateleira</string>
<string name="sync_auth_hint">Você pode entrar em uma conta existente ou criar uma nova</string>
<string name="find_similar">Encontrar semelhante</string>
<string name="sync_settings">Configurações de sincronização</string>
<string name="server_address">Endereço do servidor</string>
<string name="ignore_ssl_errors">Ignorar erros de SSL</string>
<string name="mirror_switching">Escolher espelho automaticamente</string>
<string name="mirror_switching_summary">Alternar domínios automaticamente para fontes remotas em caso de erros, se espelhos estiverem disponíveis</string>
<string name="pause">Pausa</string>
<string name="downloads_wifi_only">Baixe apenas via Wi-Fi</string>
<string name="downloads_wifi_only_summary">Interrompa o download ao mudar para uma rede móvel</string>
<string name="cancel_all_downloads_confirm">Todos os downloads ativos serão cancelados, dados parcialmente baixados serão perdidos</string>
<string name="text_downloads_list_holder">Você não tem nenhum download</string>
<string name="downloads_resumed">Os downloads foram retomados</string>
<string name="downloads_paused">Os downloads foram pausados</string>
<string name="downloads_removed">Os downloads foram removidos</string>
<string name="downloads_cancelled">Os downloads foram cancelados</string>
<string name="suggestions_enable_prompt">Quer receber sugestões personalizadas de mangás\?</string>
<string name="translations">Traduções</string>
<string name="web_view_unavailable">WebView não disponível: verifique se o provedor WebView está instalado</string>
<string name="clear_network_cache">Limpar cache de rede</string>
<string name="invalid_value_message">Valor inválido</string>
<string name="downloaded">Baixado</string>
<string name="authorization_optional">Autorização (opcional)</string>
<string name="show_pages_numbers_summary">Mostrar números de página no canto inferior</string>
<string name="user_agent">Cabeçalho UserAgent</string>
<string name="speed">Velocidade</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="images_proxy_title">Proxy de otimização de imagens</string>
<string name="images_procy_description">Use o serviço wsrv.nl para reduzir o uso de tráfego e acelerar o carregamento de imagens, se possível</string>
<string name="invert_colors">Cores invertidas</string>
<string name="username">Nome de usuário</string>
<string name="password">Senha</string>
<string name="type">Tipo</string>
<string name="proxy">Proxy</string>
<string name="resume">Retomar</string>
<string name="paused">Pausado</string>
<string name="cancel_all">Cancelar tudo</string>
<string name="restore_backup_description">Importar um backup criado anteriormente dos dados do usuário</string>
<string name="suggestion_manga">Sugestão: %s</string>
<string name="suggestions_notifications_summary">Às vezes, mostra notificações com mangás sugeridos</string>
<string name="remove_completed_downloads_confirm">Seu histórico de downloads será excluído permanentemente</string>
<string name="more">Mais</string>
<string name="enable">Activar</string>
<string name="no_thanks">Não obrigado</string>
<string name="remove_completed">Remoção concluída</string>
</resources> </resources>

View File

@@ -431,4 +431,12 @@
<string name="show_pages_numbers_summary">Показывать номера страниц в нижнем углу</string> <string name="show_pages_numbers_summary">Показывать номера страниц в нижнем углу</string>
<string name="pages_animation_summary">Анимация перелистывания страниц</string> <string name="pages_animation_summary">Анимация перелистывания страниц</string>
<string name="details_button_tip">Нажмите и удерживайте кнопку «Читать», чтобы просмотреть дополнительные параметры</string> <string name="details_button_tip">Нажмите и удерживайте кнопку «Читать», чтобы просмотреть дополнительные параметры</string>
<string name="clear_source_cookies_summary">Очистить куки для указанного домена. В большинстве случаев это аннулирует авторизацию</string>
<string name="download_option_next_unread_n_chapters">Первые непрочитанные %s</string>
<string name="download_option_all_unread">Все непрочитанные главы</string>
<string name="download_option_all_chapters">Все главы с переводом %s</string>
<string name="download_option_whole_manga">Мангу целиком</string>
<string name="download_option_first_n_chapters">Первые %s</string>
<string name="download_option_all_unread_b">Все непрочитанные главы (%s)</string>
<string name="download_option_manual_selection">Выбрать главы вручную</string>
</resources> </resources>

View File

@@ -12,7 +12,7 @@
<string name="grid">Табла</string> <string name="grid">Табла</string>
<string name="list_mode">Режим листе</string> <string name="list_mode">Режим листе</string>
<string name="settings">Подешавања</string> <string name="settings">Подешавања</string>
<string name="remote_sources">Извори садржаја</string> <string name="remote_sources">Манга извори</string>
<string name="loading_">Учитавање…</string> <string name="loading_">Учитавање…</string>
<string name="chapter_d_of_d">Поглавље %1$d од %2$d</string> <string name="chapter_d_of_d">Поглавље %1$d од %2$d</string>
<string name="close">Затворити</string> <string name="close">Затворити</string>
@@ -27,4 +27,95 @@
<string name="save">Сачувати</string> <string name="save">Сачувати</string>
<string name="share">Објави</string> <string name="share">Објави</string>
<string name="create_shortcut">Направити пречицу…</string> <string name="create_shortcut">Направити пречицу…</string>
<string name="invalid_port_number">Неважећи број порта</string>
<string name="search_manga">Претражи мангу</string>
<string name="by_name">Име</string>
<string name="clear_pages_cache">Обришите кеш странице</string>
<string name="manga_downloading_">Преузимање…</string>
<string name="switch_pages">Окретање страница</string>
<string name="delete_manga">Уклоните мангу</string>
<string name="search_on_s">Претрага по %s</string>
<string name="internal_storage">Интерна меморија</string>
<string name="external_storage">Спољна меморија</string>
<string name="language">Језик</string>
<string name="allow_unstable_updates">Дозволите нестабилна ажурирања</string>
<string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string>
<string name="standard">Стандардни</string>
<string name="webtoon">Манхва</string>
<string name="read_mode">Режим читања</string>
<string name="grid_size">Величина мреже</string>
<string name="clear_thumbs_cache">Обришите кеш сличица</string>
<string name="clear_search_history">Обришите историју претраге</string>
<string name="gestures_only">Само гестови</string>
<string name="app_update_available">Доступна је нова верзија апликације</string>
<string name="open_in_browser">Отворите у веб прегледачу</string>
<string name="notification_sound">Звук обавештења</string>
<string name="vibration">Вибрација</string>
<string name="text_empty_holder_primary">Овде је некако празно…</string>
<string name="pages_animation">Анимација превлачења</string>
<string name="about">О апликацији</string>
<string name="app_version">Верзија %s</string>
<string name="black_dark_theme">Црна</string>
<string name="black_dark_theme_summary">Троши мање енергије на AMOLED екранима</string>
<string name="show_pages_numbers">Нумерисање страница</string>
<string name="preload_pages">Претходно учитавање страница</string>
<string name="appearance">Изглед</string>
<string name="check_new_chapters_title">Проверите да ли постоје нова поглавља и обавестите их</string>
<string name="show_notification_new_chapters_on">Добићете обавештења о ажурирањима манге коју читате</string>
<string name="default_mode">Подразумевани режим</string>
<string name="network_unavailable">Мрежа није доступна</string>
<string name="network_unavailable_hint">Укључите Wi-Fi или мобилну мрежу да бисте читали мангу на мрежи</string>
<string name="allow_unstable_updates_summary">Понудите ажурирања за бета верзије апликације</string>
<string name="translations">Преводи</string>
<string name="data_and_privacy">Подаци и приватност</string>
<string name="network">Мрежа</string>
<string name="show_pages_numbers_summary">Прикажите бројеве страница у доњем углу</string>
<string name="pages_animation_summary">Анимација окретања страница</string>
<string name="images_proxy_title">Проки сервер за оптимизацију слика</string>
<string name="username">Корисничко име</string>
<string name="reader_info_bar_summary">Прикажите тренутно време и напредак читања на врху екрана</string>
<string name="authorization_optional">Ауторизација (опционално)</string>
<string name="images_procy_description">Користите wsrv.nl услуга за смањење употребе саобраћаја и убрзавање учитавања слика ако је могуће</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="details_button_tip">Притисните и држите дугме за читање да бисте видели више опција</string>
<string name="computing_">Рачунање…</string>
<string name="search">Претрага</string>
<string name="by_rating">Оцена</string>
<string name="sort_order">Редослед сортирања</string>
<string name="newest">Најновије</string>
<string name="light">Светла</string>
<string name="dark">Мрачна</string>
<string name="automatic">Како систем</string>
<string name="filter">Филтер</string>
<string name="theme">Тема</string>
<string name="pages">Странице</string>
<string name="operation_not_supported">Ова операција није подржана</string>
<string name="error">Грешка</string>
<string name="new_chapters">Нова поглавља</string>
<string name="notifications_settings">Подешавања обавештења</string>
<string name="new_version_s">Нова верзија: %s</string>
<string name="about_app_translation_summary">Преведите ову апликацију</string>
<string name="about_app_translation">Превод</string>
<string name="screenshots_policy">Политика снимања екрана</string>
<string name="clear_network_cache">Очистите мрежну кеш меморију</string>
<string name="password">Лозинка</string>
<string name="show_in_grid_view">Прикажи као мрежу</string>
<string name="page_saved">Сачувано</string>
<string name="text_delete_local_manga">Трајно уклоните \"%s\" са уређаја\?</string>
<string name="processing_">Обрада…</string>
<string name="download_complete">Преузето</string>
<string name="downloads">Преузимања</string>
<string name="popular">Популарно</string>
<string name="updated">Ажуриран</string>
<string name="text_clear_history_prompt">Трајно избрисати сву историју читања\?</string>
<string name="remove">Уклони</string>
<string name="save_page">Сачувај страницу</string>
<string name="share_image">Делите слику</string>
<string name="text_file_not_supported">Изаберите ZIP датотеку или CBZ датотеку.</string>
<string name="no_description">Нема описа</string>
<string name="taps_on_edges">Славине на ивицама</string>
<string name="reader_settings">Подешавања читача</string>
<string name="volume_buttons">Дугмад за јачину звука</string>
<string name="notifications">Обавештења</string>
<string name="pages_cache">Кеш страница</string>
</resources> </resources>

View File

@@ -431,4 +431,5 @@
<string name="pages_animation_summary">Анімація перегортання сторінок</string> <string name="pages_animation_summary">Анімація перегортання сторінок</string>
<string name="details_button_tip">Натисніть і утримуйте кнопку «Читати», щоб переглянути додаткові параметри</string> <string name="details_button_tip">Натисніть і утримуйте кнопку «Читати», щоб переглянути додаткові параметри</string>
<string name="webtoon_zoom_summary">Дозволити жест збільшення в режимі webtoon</string> <string name="webtoon_zoom_summary">Дозволити жест збільшення в режимі webtoon</string>
<string name="clear_source_cookies_summary">Очистити файли cookie лише для вказаного домену. У більшості випадків авторизація анулюється</string>
</resources> </resources>

View File

@@ -26,7 +26,7 @@
<string name="newest">Mới đăng</string> <string name="newest">Mới đăng</string>
<string name="by_rating">Đánh giá</string> <string name="by_rating">Đánh giá</string>
<string name="filter">Bộ lọc</string> <string name="filter">Bộ lọc</string>
<string name="theme">Giao diện</string> <string name="theme">Chủ đề</string>
<string name="pages">Danh sách trang</string> <string name="pages">Danh sách trang</string>
<string name="clear">Xoá</string> <string name="clear">Xoá</string>
<string name="text_clear_history_prompt">Xoá vĩnh viễn toàn bộ lịch sử đọc truyện\?</string> <string name="text_clear_history_prompt">Xoá vĩnh viễn toàn bộ lịch sử đọc truyện\?</string>
@@ -337,4 +337,67 @@
<string name="color_theme">Bảng màu</string> <string name="color_theme">Bảng màu</string>
<string name="nothing_here">Không có gì ở đây cả</string> <string name="nothing_here">Không có gì ở đây cả</string>
<string name="language">Ngôn ngữ</string> <string name="language">Ngôn ngữ</string>
<string name="compact">Nhỏ gọn</string>
<string name="services">Dịch vụ</string>
<string name="allow_unstable_updates">Cho phép các bản cập nhật không ổn định</string>
<string name="pages_animation_summary">Hiệu ứng chuyển trang</string>
<string name="_import">Nhập</string>
<string name="text_unsaved_changes_prompt">Lưu hay loại bỏ những thay đổi chưa được lưu\?</string>
<string name="download_started">Tải về bắt đầu</string>
<string name="theme_name_asuka">Asuka</string>
<string name="theme_name_mion">Mion</string>
<string name="theme_name_rikka">Rikka</string>
<string name="theme_name_sakura">Sakura</string>
<string name="theme_name_mamimi">Mamimi</string>
<string name="theme_name_kanade">Kanade</string>
<string name="got_it">Đã hiểu</string>
<string name="find_similar">Tìm truyện giống nhau</string>
<string name="sync_auth_hint">Đăng nhập vào tài khoản hoặc tạo một tài khoản mới</string>
<string name="sync_settings">Cài đặt đồng bộ hoá</string>
<string name="speed">Tốc độ</string>
<string name="restore_backup_description">Nhập một bản sao lưu dữ liệu người dùng đã được tạo trước đó</string>
<string name="pause">Tạm dừng</string>
<string name="suggestion_manga">Đề xuất: %s</string>
<string name="suggestions_notifications_summary">Đôi khi nhận thông báo với manga đã được đề xuất</string>
<string name="enable">Kích hoạt</string>
<string name="cancel_all_downloads_confirm">Tất cả tải xuống đang hoạt động sẽ bị huỷ bỏ, dữ liệu không hoàn toàn tải xuống sẽ bị mất</string>
<string name="downloads_paused">Tải xuống đã bị tạm dừng</string>
<string name="text_downloads_list_holder">Bạn chưa tải xuống manga nào</string>
<string name="downloads_cancelled">Tải xuống đã bị huỷ</string>
<string name="translations">Dịch thuật</string>
<string name="suggestions_enable_prompt">Bạn có muốn nhận đề xuất manga đã được cá nhân hoá không\?</string>
<string name="port">Cổng</string>
<string name="proxy">Proxy</string>
<string name="downloaded">Đã tải xuống</string>
<string name="password">Mật khẩu</string>
<string name="username">Tên người dùng</string>
<string name="invalid_value_message">Giá trị không hợp lệ</string>
<string name="webtoon_zoom_summary">Cho phép cử chỉ phóng to ở chế độ Webtoon</string>
<string name="data_and_privacy">Dữ liệu và quyền riêng tư</string>
<string name="details_button_tip">Chạm và giữ nút \"Đọc\" để hiển thị nhiều lựa chọn hơn</string>
<string name="show_pages_numbers_summary">Hiển thị số trang ở góc bên dưới</string>
<string name="clear_source_cookies_summary">Xoá cookies chỉ cho tên miền này. Trong hầu hết mọi trường hợp sẽ làm mất hiệu lực uỷ quyền</string>
<string name="ignore_ssl_errors">Bỏ qua lỗi SSL</string>
<string name="sources_reorder_tip">Chạm và giữ các nguồn truyện để sắp xếp</string>
<string name="settings_apply_restart_required">Vui lòng khởi động lại ứng dụng để áp dụng những thay đổi trên</string>
<string name="server_address">Địa chỉ máy chủ</string>
<string name="comics_archive_import_description">Bạn có thể chọn một hoặc nhiều file .cbz hoặc .zip, mỗi file sẽ được nhận dạng như một manga riêng biệt</string>
<string name="cancel_all">Huỷ bỏ tất cả</string>
<string name="downloads_wifi_only">Chỉ tải xuống qua Wi-Fi</string>
<string name="downloads_wifi_only_summary">Ngừng tải xuống khi chuyển qua mạng dữ liệu</string>
<string name="remove_completed_downloads_confirm">Lịch sử tải xuống của bạn sẽ bị xoá vĩnh viễn</string>
<string name="address">Địa chỉ</string>
<string name="invert_colors">Đảo màu</string>
<string name="invalid_port_number">Cổng không hợp lệ</string>
<string name="download_option_manual_selection">Chọn thủ công các chương</string>
<string name="download_option_all_unread">Tất cả các chương chưa đọc</string>
<string name="resume">Tiếp tục</string>
<string name="clear_new_chapters_counters">Xoá thông tin về chương mới</string>
<string name="source_disabled">Nguồn truyện đã được vô hiệu hoá</string>
<string name="sync_host_description">Bạn có thể dùng một máy chủ đồng bộ hoá của bạn (self-hosted) hoặc máy chủ đồng bộ hoá mặc định. Đừng thay đổi địa chủ máy chủ nếu bạn không chắc chắn mình đang làm gì.</string>
<string name="downloads_resumed">Tải xuống đã được tiếp tục</string>
<string name="downloads_removed">Tải xuống đã bị xoá bỏ</string>
<string name="more">Thêm nữa</string>
<string name="no_thanks">Không, cảm ơn</string>
<string name="download_option_all_chapters">Số chương đã được dịch: %s</string>
</resources> </resources>

View File

@@ -437,4 +437,16 @@
<string name="show_pages_numbers_summary">Show page numbers in bottom corner</string> <string name="show_pages_numbers_summary">Show page numbers in bottom corner</string>
<string name="pages_animation_summary">Animate page switching</string> <string name="pages_animation_summary">Animate page switching</string>
<string name="details_button_tip">Press and hold the Read button to see more options</string> <string name="details_button_tip">Press and hold the Read button to see more options</string>
<string name="clear_source_cookies_summary">Clear cookies for specified domain only. In most cases will invalidate authorization</string>
<string name="download_option_all_chapters">All chapters with translation %s</string>
<string name="download_option_whole_manga">The whole manga</string>
<string name="download_option_first_n_chapters">First %s</string>
<string name="download_option_next_unread_n_chapters">Next unread %s</string>
<string name="download_option_all_unread">All unread chapters</string>
<string name="download_option_all_unread_b">All unread chapters (%s)</string>
<string name="download_option_manual_selection">Select chapters manually</string>
<string name="custom_directory">Custom directory</string>
<string name="pick_custom_directory">Pick custom directory</string>
<string name="no_access_to_file">You have no access to this file or directory</string>
<string name="local_manga_directories">Local manga directories</string>
</resources> </resources>

View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:key="local_manga_dirs"
android:persistent="false"
android:title="@string/local_manga_directories" />
<Preference <Preference
android:key="local_storage" android:key="local_storage"
@@ -11,7 +17,8 @@
android:defaultValue="false" android:defaultValue="false"
android:key="downloads_wifi" android:key="downloads_wifi"
android:summary="@string/downloads_wifi_only_summary" android:summary="@string/downloads_wifi_only_summary"
android:title="@string/downloads_wifi_only" /> android:title="@string/downloads_wifi_only"
app:allowDividerAbove="true" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"

View File

@@ -10,4 +10,11 @@
android:title="@string/sign_in" android:title="@string/sign_in"
app:allowDividerAbove="true" /> app:allowDividerAbove="true" />
<Preference
android:key="cookies_clear"
android:order="101"
android:persistent="false"
android:summary="@string/clear_source_cookies_summary"
android:title="@string/clear_cookies" />
</PreferenceScreen> </PreferenceScreen>