Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d31e76cc7 | ||
|
|
20910ffb5d | ||
|
|
7497ee6364 | ||
|
|
0f2ed50e18 | ||
|
|
ba066b577b | ||
|
|
4496fe876f | ||
|
|
a9f5abebf0 | ||
|
|
bebee2ef27 | ||
|
|
4ec2b0c8fe | ||
|
|
4a7be70898 | ||
|
|
2bcba1eb21 | ||
|
|
feca7ba3fc | ||
|
|
745b349e5e | ||
|
|
13946783a5 | ||
|
|
84e5400522 | ||
|
|
02c9a933d2 | ||
|
|
92af851d3b |
@@ -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'
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
117
app/src/main/kotlin/org/koitharu/kotatsu/core/util/FileUtil.java
Normal file
117
app/src/main/kotlin/org/koitharu/kotatsu/core/util/FileUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ class ChaptersBottomSheetMediator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionModeStarted(mode: ActionMode) {
|
override fun onActionModeStarted(mode: ActionMode) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
lock()
|
lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
12
app/src/main/res/drawable/ic_list_end.xml
Normal file
12
app/src/main/res/drawable/ic_list_end.xml
Normal 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>
|
||||||
12
app/src/main/res/drawable/ic_list_next.xml
Normal file
12
app/src/main/res/drawable/ic_list_next.xml
Normal 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>
|
||||||
12
app/src/main/res/drawable/ic_list_start.xml
Normal file
12
app/src/main/res/drawable/ic_list_start.xml
Normal 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>
|
||||||
11
app/src/main/res/drawable/ic_select_group.xml
Normal file
11
app/src/main/res/drawable/ic_select_group.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
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>
|
||||||
@@ -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"
|
||||||
|
|||||||
57
app/src/main/res/layout/activity_manga_directories.xml
Normal file
57
app/src/main/res/layout/activity_manga_directories.xml
Normal 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>
|
||||||
13
app/src/main/res/layout/dialog_directory_select.xml
Normal file
13
app/src/main/res/layout/dialog_directory_select.xml
Normal 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" />
|
||||||
16
app/src/main/res/layout/item_download_option.xml
Normal file
16
app/src/main/res/layout/item_download_option.xml
Normal 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" />
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
53
app/src/main/res/layout/item_storage_config.xml
Normal file
53
app/src/main/res/layout/item_storage_config.xml
Normal 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>
|
||||||
29
app/src/main/res/menu/mode_updates.xml
Normal file
29
app/src/main/res/menu/mode_updates.xml
Normal 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>
|
||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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:<br><tt>%1$s</tt><br><br>1. Tente <a href=%2$s>abra a página do mangá em um navegador da web</a> para garantir que o mesmo esteja disponível em sua fonte<br>2. Se estiver disponível, envie um relatório de erro para os desenvolvedores.</string>
|
<string name="manga_error_description_pattern">Detalhes do erro:<br><tt>%1$s</tt><br><br>1. Tente <a href=%2$s>abra a página do mangá em um navegador da web</a> para garantir que o mesmo esteja disponível em sua fonte<br>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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user