Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c81e8749b6 | ||
|
|
5fa260a0c7 | ||
|
|
e0ba4e2686 | ||
|
|
f188d1c0f3 | ||
|
|
6de55afa27 | ||
|
|
21dcb5b754 | ||
|
|
9b3ea57db1 | ||
|
|
032a8607ba | ||
|
|
f7303c5957 | ||
|
|
d696606ef9 | ||
|
|
0a6e106a1d | ||
|
|
de1a7f0ca8 |
2
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
2
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
@@ -62,3 +62,5 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
|
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
|||||||
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@@ -1,3 +1,4 @@
|
|||||||
# Default ignored files
|
# Default ignored files
|
||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
/migrations.xml
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 556
|
versionCode 559
|
||||||
versionName '5.3'
|
versionName '5.3.2'
|
||||||
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:c2b79b55f8') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:92bfc7e9fa') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +136,8 @@ dependencies {
|
|||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
implementation 'ch.acra:acra-http:5.9.7'
|
implementation 'ch.acra:acra-http:5.10.1'
|
||||||
implementation 'ch.acra:acra-dialog:5.9.7'
|
implementation 'ch.acra:acra-dialog:5.10.1'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ dependencies {
|
|||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.5.1'
|
androidTestImplementation 'androidx.room:room-testing:2.5.2'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||||
|
|
||||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
|||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@@ -35,7 +36,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
}
|
}
|
||||||
with(viewBinding.webView.settings) {
|
with(viewBinding.webView.settings) {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
userAgentString = UserAgents.CHROME_MOBILE
|
||||||
}
|
}
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
|
|||||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||||
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@@ -49,10 +50,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
val url = intent?.dataString.orEmpty()
|
val url = intent?.dataString.orEmpty()
|
||||||
with(viewBinding.webView.settings) {
|
with(viewBinding.webView.settings) {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
domStorageEnabled = true
|
domStorageEnabled = true
|
||||||
databaseEnabled = true
|
databaseEnabled = true
|
||||||
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
|
userAgentString = intent?.getStringExtra(ARG_UA) ?: UserAgents.CHROME_MOBILE
|
||||||
}
|
}
|
||||||
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
||||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
|
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
@@ -10,11 +9,11 @@ import okhttp3.Response
|
|||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
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.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mergeWith
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||||
import java.net.IDN
|
import java.net.IDN
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
headersBuilder.mergeWith(it, replaceExisting = false)
|
headersBuilder.mergeWith(it, replaceExisting = false)
|
||||||
}
|
}
|
||||||
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
|
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
|
||||||
headersBuilder[CommonHeaders.USER_AGENT] = userAgentFallback
|
headersBuilder[CommonHeaders.USER_AGENT] = UserAgents.CHROME_MOBILE
|
||||||
}
|
}
|
||||||
if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) {
|
if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) {
|
||||||
val idn = IDN.toASCII(repository.domain)
|
val idn = IDN.toASCII(repository.domain)
|
||||||
@@ -62,26 +61,4 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
|
|
||||||
override fun request(): Request = request
|
override fun request(): Request = request
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val userAgentFallback
|
|
||||||
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
|
|
||||||
BuildConfig.VERSION_NAME,
|
|
||||||
Build.VERSION.RELEASE,
|
|
||||||
Build.MODEL,
|
|
||||||
Build.BRAND,
|
|
||||||
Build.DEVICE,
|
|
||||||
Locale.getDefault().language,
|
|
||||||
)
|
|
||||||
|
|
||||||
val userAgentChrome
|
|
||||||
get() = (
|
|
||||||
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
|
||||||
"Chrome/100.0.4896.127 Mobile Safari/537.36"
|
|
||||||
).format(
|
|
||||||
Build.VERSION.RELEASE,
|
|
||||||
Build.MODEL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaPa
|
|||||||
return if (source == MangaSource.DUMMY) {
|
return if (source == MangaSource.DUMMY) {
|
||||||
DummyParser(loaderContext)
|
DummyParser(loaderContext)
|
||||||
} else {
|
} else {
|
||||||
source.newParser(loaderContext)
|
loaderContext.newParserInstance(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.ActionBarContextView
|
import androidx.appcompat.widget.ActionBarContextView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
@@ -103,8 +104,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
// ActivityCompat.recreate(this)
|
ActivityCompat.recreate(this)
|
||||||
error("Test")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
fun interface BufferedObserver<T> {
|
|
||||||
|
|
||||||
fun onChanged(t: T, previous: T?)
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
|
|
||||||
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
|
||||||
|
|
||||||
override fun getItemCount() = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position) {
|
|
||||||
0 -> DetailsFragment()
|
|
||||||
1 -> ChaptersFragment()
|
|
||||||
else -> throw IndexOutOfBoundsException("No fragment for position $position")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FilenameFilter
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -192,7 +193,7 @@ class LocalMangaRepository @Inject constructor(
|
|||||||
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
|
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
|
||||||
files.map { file ->
|
files.map { file ->
|
||||||
async(dispatcher) {
|
async(dispatcher) {
|
||||||
runCatchingCancellable { LocalMangaInput.of(file).getManga() }.getOrNull()
|
runCatchingCancellable { LocalMangaInput.ofOrNull(file)?.getManga() }.getOrNull()
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
}.filterNotNullTo(ArrayList(files.size))
|
}.filterNotNullTo(ArrayList(files.size))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.net.toFile
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -16,6 +17,7 @@ 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.core.util.ext.resolveFile
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
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
|
||||||
@@ -83,8 +85,12 @@ class LocalStorageManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
|
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
|
||||||
|
if (uri.scheme == "file") {
|
||||||
|
uri.toFile()
|
||||||
|
} else {
|
||||||
uri.resolveFile(context)
|
uri.resolveFile(context)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setDirIsNoMedia(dir: File) = runInterruptible(Dispatchers.IO) {
|
suspend fun setDirIsNoMedia(dir: File) = runInterruptible(Dispatchers.IO) {
|
||||||
File(dir, NOMEDIA).createNewFile()
|
File(dir, NOMEDIA).createNewFile()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data.input
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
@@ -30,6 +31,12 @@ sealed class LocalMangaInput(
|
|||||||
else -> LocalMangaZipInput(file)
|
else -> LocalMangaZipInput(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ofOrNull(file: File): LocalMangaInput? = when {
|
||||||
|
file.isDirectory -> LocalMangaDirInput(file)
|
||||||
|
CbzFilter.isFileSupported(file.name) -> LocalMangaZipInput(file)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
protected fun zipUri(file: File, entryName: String): String =
|
protected fun zipUri(file: File, entryName: String): String =
|
||||||
Uri.fromParts("cbz", file.path, entryName).toString()
|
Uri.fromParts("cbz", file.path, entryName).toString()
|
||||||
|
|||||||
@@ -35,18 +35,6 @@ class LocalMangaUtil(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun writeIndex(index: MangaIndex) {
|
|
||||||
newOutput().use { output ->
|
|
||||||
when (output) {
|
|
||||||
is LocalMangaDirOutput -> {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
is LocalMangaZipOutput -> TODO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun newOutput(): LocalMangaOutput = runInterruptible(Dispatchers.IO) {
|
private suspend fun newOutput(): LocalMangaOutput = runInterruptible(Dispatchers.IO) {
|
||||||
val file = manga.url.toUri().toFile()
|
val file = manga.url.toUri().toFile()
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
|||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
|
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
|
||||||
.setOngoing(true)
|
.setOngoing(false)
|
||||||
.build()
|
.build()
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
|
|||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@@ -67,7 +68,9 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
|||||||
}
|
}
|
||||||
with(viewBinding.webView.settings) {
|
with(viewBinding.webView.settings) {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
userAgentString = UserAgents.CHROME_MOBILE
|
||||||
}
|
}
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ class MangaDirectorySelectViewModel @Inject constructor(
|
|||||||
fun onCustomDirectoryPicked(uri: Uri) {
|
fun onCustomDirectoryPicked(uri: Uri) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
storageManager.takePermissions(uri)
|
storageManager.takePermissions(uri)
|
||||||
val dir = storageManager.resolveUri(uri) ?: throw FileNotFoundException()
|
val dir = requireNotNull(storageManager.resolveUri(uri)) {
|
||||||
|
"Cannot resolve file name of \"$uri\""
|
||||||
|
}
|
||||||
if (!dir.canWrite()) {
|
if (!dir.canWrite()) {
|
||||||
throw AccessDeniedException(dir)
|
throw AccessDeniedException(dir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ class MangaDirectoriesViewModel @Inject constructor(
|
|||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
loadingJob?.cancelAndJoin()
|
loadingJob?.cancelAndJoin()
|
||||||
storageManager.takePermissions(uri)
|
storageManager.takePermissions(uri)
|
||||||
val dir = storageManager.resolveUri(uri) ?: throw FileNotFoundException()
|
val dir = requireNotNull(storageManager.resolveUri(uri)) {
|
||||||
|
"Cannot resolve file name of \"$uri\""
|
||||||
|
}
|
||||||
if (!dir.canWrite()) {
|
if (!dir.canWrite()) {
|
||||||
throw AccessDeniedException(dir)
|
throw AccessDeniedException(dir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -11,6 +12,8 @@ import kotlinx.coroutines.flow.mapLatest
|
|||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||||
@@ -32,6 +35,7 @@ class ShelfContentObserveUseCase @Inject constructor(
|
|||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val suggestionRepository: SuggestionRepository,
|
private val suggestionRepository: SuggestionRepository,
|
||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
|
private val settings: AppSettings,
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -46,7 +50,10 @@ class ShelfContentObserveUseCase @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeLocalManga(sortOrder: SortOrder, limit: Int): Flow<List<Manga>> {
|
private fun observeLocalManga(sortOrder: SortOrder, limit: Int): Flow<List<Manga>> {
|
||||||
return localStorageChanges
|
return combine<LocalManga?, String, Any?>(
|
||||||
|
localStorageChanges,
|
||||||
|
settings.observe().filter { it == AppSettings.KEY_LOCAL_MANGA_DIRS }
|
||||||
|
) { _, _ -> Any() }
|
||||||
.onStart { emit(null) }
|
.onStart { emit(null) }
|
||||||
.mapLatest {
|
.mapLatest {
|
||||||
localMangaRepository.getList(0, null, sortOrder).take(limit)
|
localMangaRepository.getList(0, null, sortOrder).take(limit)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
|||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.setDefaults(0)
|
.setDefaults(0)
|
||||||
.setOngoing(true)
|
.setOngoing(false)
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.setDefaults(0)
|
.setDefaults(0)
|
||||||
.setOngoing(true)
|
.setOngoing(false)
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class RecentListFactory(
|
|||||||
|
|
||||||
override fun getLoadingView() = null
|
override fun getLoadingView() = null
|
||||||
|
|
||||||
override fun getItemId(position: Int) = dataSet[position].id
|
override fun getItemId(position: Int) = dataSet.getOrNull(position)?.id ?: 0L
|
||||||
|
|
||||||
override fun onDataSetChanged() {
|
override fun onDataSetChanged() {
|
||||||
val data = runBlocking { historyRepository.getList(0, 10) }
|
val data = runBlocking { historyRepository.getList(0, 10) }
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ShelfListFactory(
|
|||||||
|
|
||||||
override fun getLoadingView() = null
|
override fun getLoadingView() = null
|
||||||
|
|
||||||
override fun getItemId(position: Int) = dataSet[position].id
|
override fun getItemId(position: Int) = dataSet.getOrNull(position)?.id ?: 0L
|
||||||
|
|
||||||
override fun onDataSetChanged() {
|
override fun onDataSetChanged() {
|
||||||
val data = runBlocking {
|
val data = runBlocking {
|
||||||
|
|||||||
@@ -431,4 +431,16 @@
|
|||||||
<string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string>
|
<string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string>
|
||||||
<string name="reader_info_bar_summary">Паказаць бягучы час і ход чытання ў верхняй частцы экрана</string>
|
<string name="reader_info_bar_summary">Паказаць бягучы час і ход чытання ў верхняй частцы экрана</string>
|
||||||
<string name="pages_animation_summary">Анімацыя перагортвання старонак</string>
|
<string name="pages_animation_summary">Анімацыя перагортвання старонак</string>
|
||||||
|
<string name="clear_source_cookies_summary">Выдаліць файлы cookie толькі для вызначанага дамена. У большасці выпадкаў гэта робіць аўтарызацыю несапраўднай</string>
|
||||||
|
<string name="download_option_whole_manga">Манга цалкам</string>
|
||||||
|
<string name="local_manga_directories">Лакальныя каталогі мангі</string>
|
||||||
|
<string name="download_option_all_chapters">Усе раздзелы з перакладам %s</string>
|
||||||
|
<string name="download_option_first_n_chapters">Першыя %s</string>
|
||||||
|
<string name="download_option_all_unread_b">Усе непрачытаныя раздзелы (%s)</string>
|
||||||
|
<string name="download_option_all_unread">Усе непрачытаныя раздзелы</string>
|
||||||
|
<string name="download_option_manual_selection">Выбірайце раздзелы ўручную</string>
|
||||||
|
<string name="pick_custom_directory">Выберыце карыстальніцкі каталог</string>
|
||||||
|
<string name="download_option_next_unread_n_chapters">Наступная непрачытаная %s</string>
|
||||||
|
<string name="custom_directory">Карыстацкі каталог</string>
|
||||||
|
<string name="no_access_to_file">У вас няма доступу да гэтага файла або каталога</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -439,4 +439,8 @@
|
|||||||
<string name="download_option_manual_selection">Selección manual de los capítulos</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_all_chapters">Todos los capítulos con traducción %s</string>
|
||||||
<string name="download_option_next_unread_n_chapters">Siguiente %s sin leer</string>
|
<string name="download_option_next_unread_n_chapters">Siguiente %s sin leer</string>
|
||||||
|
<string name="pick_custom_directory">Elegir un directorio personalizado</string>
|
||||||
|
<string name="no_access_to_file">No tienes acceso a este archivo o directorio</string>
|
||||||
|
<string name="custom_directory">Directorio personalizado</string>
|
||||||
|
<string name="local_manga_directories">Directorios locales del manga</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -431,4 +431,16 @@
|
|||||||
<string name="network">Réseau</string>
|
<string name="network">Réseau</string>
|
||||||
<string name="data_and_privacy">Données et confidentialité</string>
|
<string name="data_and_privacy">Données et confidentialité</string>
|
||||||
<string name="show_pages_numbers_summary">Afficher les numéros de page dans le coin inférieur</string>
|
<string name="show_pages_numbers_summary">Afficher les numéros de page dans le coin inférieur</string>
|
||||||
|
<string name="clear_source_cookies_summary">Effacer les cookies pour le domaine spécifié uniquement. Dans la plupart des cas, l\'autorisation sera invalidée</string>
|
||||||
|
<string name="download_option_first_n_chapters">%s premier(s)</string>
|
||||||
|
<string name="download_option_next_unread_n_chapters">%s prochain(s) non lu(s)</string>
|
||||||
|
<string name="download_option_all_unread_b">Tous les chapitres non lus (%s)</string>
|
||||||
|
<string name="custom_directory">Répertoire personnalisé</string>
|
||||||
|
<string name="download_option_all_chapters">Tous les chapitres avec traduction %s</string>
|
||||||
|
<string name="download_option_all_unread">Tous les chapitres non lus</string>
|
||||||
|
<string name="download_option_whole_manga">Tout le manga</string>
|
||||||
|
<string name="download_option_manual_selection">Sélection manuelle des chapitres</string>
|
||||||
|
<string name="no_access_to_file">Vous n\'avez pas accès à ce fichier ou répertoire</string>
|
||||||
|
<string name="pick_custom_directory">Choisir un répertoire personnalisé</string>
|
||||||
|
<string name="local_manga_directories">Annuaires locaux de mangas</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -431,4 +431,16 @@
|
|||||||
<string name="show_pages_numbers_summary">下隅にページ番号を表示する</string>
|
<string name="show_pages_numbers_summary">下隅にページ番号を表示する</string>
|
||||||
<string name="reader_info_bar_summary">画面上部に現在時刻と読書の進行状況を表示する</string>
|
<string name="reader_info_bar_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_all_chapters">翻訳付きのすべての章%s</string>
|
||||||
|
<string name="download_option_whole_manga">マンガ全体</string>
|
||||||
|
<string name="download_option_first_n_chapters">最初%s</string>
|
||||||
|
<string name="download_option_next_unread_n_chapters">次の未読%s</string>
|
||||||
|
<string name="download_option_all_unread">すべての未読の章</string>
|
||||||
|
<string name="download_option_all_unread_b">すべての未読チャプター (%s)</string>
|
||||||
|
<string name="download_option_manual_selection">手動でチャプターを選択</string>
|
||||||
|
<string name="custom_directory">カスタムディレクトリ</string>
|
||||||
|
<string name="no_access_to_file">このファイルまたはディレクトリにアクセスできません</string>
|
||||||
|
<string name="pick_custom_directory">カスタムディレクトリを選択</string>
|
||||||
|
<string name="local_manga_directories">ローカルマンガディレクトリ</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -439,4 +439,8 @@
|
|||||||
<string name="download_option_first_n_chapters">Первые %s</string>
|
<string name="download_option_first_n_chapters">Первые %s</string>
|
||||||
<string name="download_option_all_unread_b">Все непрочитанные главы (%s)</string>
|
<string name="download_option_all_unread_b">Все непрочитанные главы (%s)</string>
|
||||||
<string name="download_option_manual_selection">Выбрать главы вручную</string>
|
<string name="download_option_manual_selection">Выбрать главы вручную</string>
|
||||||
|
<string name="no_access_to_file">У вас нет доступа к этому файлу или каталогу</string>
|
||||||
|
<string name="custom_directory">Пользовательский каталог</string>
|
||||||
|
<string name="local_manga_directories">Локальные каталоги манги</string>
|
||||||
|
<string name="pick_custom_directory">Выбрать пользовательский каталог</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -118,4 +118,12 @@
|
|||||||
<string name="volume_buttons">Дугмад за јачину звука</string>
|
<string name="volume_buttons">Дугмад за јачину звука</string>
|
||||||
<string name="notifications">Обавештења</string>
|
<string name="notifications">Обавештења</string>
|
||||||
<string name="pages_cache">Кеш страница</string>
|
<string name="pages_cache">Кеш страница</string>
|
||||||
|
<string name="text_shelf_holder_secondary">Пронађите шта да читате у одељку „Преглед“</string>
|
||||||
|
<string name="manga_shelf">Полица</string>
|
||||||
|
<string name="check_for_updates">Провери ажурирања</string>
|
||||||
|
<string name="feed">Фид</string>
|
||||||
|
<string name="services">Услуге</string>
|
||||||
|
<string name="show_on_shelf">Прикажи на полици</string>
|
||||||
|
<string name="explore">Преглед</string>
|
||||||
|
<string name="options">Опције</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -432,4 +432,15 @@
|
|||||||
<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>
|
<string name="clear_source_cookies_summary">Очистити файли cookie лише для вказаного домену. У більшості випадків авторизація анулюється</string>
|
||||||
|
<string name="pick_custom_directory">Вибрати користувальницький каталог</string>
|
||||||
|
<string name="no_access_to_file">Ви не маєте доступу до цього файлу чи каталогу</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_all_unread_b">Усі непрочитані розділи (%s)</string>
|
||||||
|
<string name="download_option_manual_selection">Виберіть розділи вручну</string>
|
||||||
|
<string name="local_manga_directories">Локальні каталоги манґи</string>
|
||||||
|
<string name="custom_directory">Користувальницький каталог</string>
|
||||||
|
<string name="download_option_first_n_chapters">Перші %s</string>
|
||||||
|
<string name="download_option_next_unread_n_chapters">Перші непрочитані %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user