Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d224cd99bb | ||
|
|
b955d31770 | ||
|
|
b4eb8d56a6 | ||
|
|
c896ac72e8 | ||
|
|
b599cb33ff |
@@ -26,7 +26,7 @@ Download APK directly from GitHub:
|
|||||||
* Notifications about new chapters with updates feed
|
* Notifications about new chapters with updates feed
|
||||||
* Shikimori integration (manga tracking)
|
* Shikimori integration (manga tracking)
|
||||||
* Password/fingerprint protect access to the app
|
* Password/fingerprint protect access to the app
|
||||||
* History and favourites synchronization across devices (coming soon)
|
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 503
|
versionCode 504
|
||||||
versionName '4.0.3'
|
versionName '4.0.4'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:bf8a1f3db2') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:1e49d4095b') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +137,10 @@ dependencies {
|
|||||||
testImplementation 'org.json:json:20220924'
|
testImplementation 'org.json:json:20220924'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.5.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.4'
|
||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
android:fullBackupOnly="true"
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||||
@@ -20,6 +17,9 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||||
import org.koitharu.kotatsu.utils.isSuccess
|
import org.koitharu.kotatsu.utils.isSuccess
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class ExceptionResolver private constructor(
|
class ExceptionResolver private constructor(
|
||||||
private val activity: FragmentActivity?,
|
private val activity: FragmentActivity?,
|
||||||
@@ -49,6 +49,7 @@ class ExceptionResolver private constructor(
|
|||||||
openInBrowser(e.url)
|
openInBrowser(e.url)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.channels.trySendBlocking
|
|||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -37,6 +38,13 @@ class NetworkStateObserver @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun awaitForConnection(): Unit {
|
||||||
|
if (value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
first { it }
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeImpl() = callbackFlow<Boolean> {
|
private fun observeImpl() = callbackFlow<Boolean> {
|
||||||
val request = NetworkRequest.Builder().build()
|
val request = NetworkRequest.Builder().build()
|
||||||
val callback = FlowNetworkCallback(this)
|
val callback = FlowNetworkCallback(this)
|
||||||
|
|||||||
@@ -115,4 +115,4 @@ class ZipOutput(
|
|||||||
closeEntry()
|
closeEntry()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -36,6 +35,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.referer
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
@@ -219,10 +219,8 @@ class DownloadManager @AssistedInject constructor(
|
|||||||
val call = okHttp.newCall(request)
|
val call = okHttp.newCall(request)
|
||||||
val file = File(destination, tempFileName)
|
val file = File(destination, tempFileName)
|
||||||
val response = call.clone().await()
|
val response = call.clone().await()
|
||||||
runInterruptible(Dispatchers.IO) {
|
file.outputStream().use { out ->
|
||||||
file.outputStream().use { out ->
|
checkNotNull(response.body).byteStream().copyToSuspending(out)
|
||||||
checkNotNull(response.body).byteStream().copyTo(out)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,17 @@ package org.koitharu.kotatsu.local.data
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.tomclaw.cache.DiskLruCache
|
import com.tomclaw.cache.DiskLruCache
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
|
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||||
|
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||||
|
import org.koitharu.kotatsu.utils.ext.subdir
|
||||||
|
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import org.koitharu.kotatsu.utils.FileSize
|
|
||||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
|
||||||
import org.koitharu.kotatsu.utils.ext.subdir
|
|
||||||
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||||
@@ -26,42 +28,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
return lruCache.get(url)?.takeIfReadable()
|
return lruCache.get(url)?.takeIfReadable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(url: String, inputStream: InputStream): File {
|
suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
|
||||||
val file = File(cacheDir, url.longHashCode().toString())
|
val file = File(cacheDir, url.longHashCode().toString())
|
||||||
file.outputStream().use { out ->
|
try {
|
||||||
inputStream.copyTo(out)
|
file.outputStream().use { out ->
|
||||||
}
|
inputStream.copyToSuspending(out)
|
||||||
val res = lruCache.put(url, file)
|
|
||||||
file.delete()
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
fun put(
|
|
||||||
url: String,
|
|
||||||
inputStream: InputStream,
|
|
||||||
contentLength: Long,
|
|
||||||
progress: MutableStateFlow<Float>,
|
|
||||||
): File {
|
|
||||||
val file = File(cacheDir, url.longHashCode().toString())
|
|
||||||
file.outputStream().use { out ->
|
|
||||||
var bytesCopied: Long = 0
|
|
||||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
|
||||||
var bytes = inputStream.read(buffer)
|
|
||||||
while (bytes >= 0) {
|
|
||||||
out.write(buffer, 0, bytes)
|
|
||||||
bytesCopied += bytes
|
|
||||||
publishProgress(contentLength, bytesCopied, progress)
|
|
||||||
bytes = inputStream.read(buffer)
|
|
||||||
}
|
}
|
||||||
}
|
lruCache.put(url, file)
|
||||||
val res = lruCache.put(url, file)
|
} finally {
|
||||||
file.delete()
|
file.delete()
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) {
|
|
||||||
if (contentLength > 0) {
|
|
||||||
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
@@ -14,8 +13,10 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
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.model.RATING_UNKNOWN
|
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||||
|
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.utils.ext.longOf
|
import org.koitharu.kotatsu.utils.ext.longOf
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
// TODO: Add support for chapters in cbz
|
// TODO: Add support for chapters in cbz
|
||||||
// https://github.com/KotatsuApp/Kotatsu/issues/31
|
// https://github.com/KotatsuApp/Kotatsu/issues/31
|
||||||
@@ -62,6 +63,7 @@ class DirMangaImporter(
|
|||||||
file.isDirectory -> {
|
file.isDirectory -> {
|
||||||
addPages(output, file, path + "/" + file.name, state)
|
addPages(output, file, path + "/" + file.name, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
file.isFile -> {
|
file.isFile -> {
|
||||||
val tempFile = file.asTempFile()
|
val tempFile = file.asTempFile()
|
||||||
if (!state.hasCover) {
|
if (!state.hasCover) {
|
||||||
@@ -86,7 +88,7 @@ class DirMangaImporter(
|
|||||||
"Cannot open input stream for $uri"
|
"Cannot open input stream for $uri"
|
||||||
}.use { input ->
|
}.use { input ->
|
||||||
file.outputStream().use { output ->
|
file.outputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyToSuspending(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.local.domain.importer
|
package org.koitharu.kotatsu.local.domain.importer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -11,7 +9,10 @@ import org.koitharu.kotatsu.local.data.CbzFilter
|
|||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||||
import org.koitharu.kotatsu.utils.ext.resolveName
|
import org.koitharu.kotatsu.utils.ext.resolveName
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class ZipMangaImporter(
|
class ZipMangaImporter(
|
||||||
storageManager: LocalStorageManager,
|
storageManager: LocalStorageManager,
|
||||||
@@ -27,10 +28,10 @@ class ZipMangaImporter(
|
|||||||
}
|
}
|
||||||
val dest = File(getOutputDir(), name)
|
val dest = File(getOutputDir(), name)
|
||||||
runInterruptible {
|
runInterruptible {
|
||||||
contentResolver.openInputStream(uri)?.use { source ->
|
contentResolver.openInputStream(uri)
|
||||||
dest.outputStream().use { output ->
|
}?.use { source ->
|
||||||
source.copyTo(output)
|
dest.outputStream().use { output ->
|
||||||
}
|
source.copyToSuspending(output)
|
||||||
}
|
}
|
||||||
} ?: throw IOException("Cannot open input stream: $uri")
|
} ?: throw IOException("Cannot open input stream: $uri")
|
||||||
localMangaRepository.getFromFile(dest)
|
localMangaRepository.getFromFile(dest)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.parsers.util.await
|
|||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.withProgress
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
@@ -179,9 +180,12 @@ class PageLoader @Inject constructor(
|
|||||||
val uri = Uri.parse(pageUrl)
|
val uri = Uri.parse(pageUrl)
|
||||||
return if (uri.scheme == "cbz") {
|
return if (uri.scheme == "cbz") {
|
||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
val zip = ZipFile(uri.schemeSpecificPart)
|
ZipFile(uri.schemeSpecificPart)
|
||||||
val entry = zip.getEntry(uri.fragment)
|
}.use { zip ->
|
||||||
zip.getInputStream(entry).use {
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
val entry = zip.getEntry(uri.fragment)
|
||||||
|
zip.getInputStream(entry)
|
||||||
|
}.use {
|
||||||
cache.put(pageUrl, it)
|
cache.put(pageUrl, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,10 +204,8 @@ class PageLoader @Inject constructor(
|
|||||||
val body = checkNotNull(response.body) {
|
val body = checkNotNull(response.body) {
|
||||||
"Null response"
|
"Null response"
|
||||||
}
|
}
|
||||||
runInterruptible(Dispatchers.IO) {
|
body.withProgress(progress).byteStream().use {
|
||||||
body.byteStream().use {
|
cache.put(pageUrl, it)
|
||||||
cache.put(pageUrl, it, body.contentLength(), progress)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ import android.webkit.MimeTypeMap
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -20,6 +16,11 @@ import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
|
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
private const val MAX_FILENAME_LENGTH = 10
|
private const val MAX_FILENAME_LENGTH = 10
|
||||||
private const val EXTENSION_FALLBACK = "png"
|
private const val EXTENSION_FALLBACK = "png"
|
||||||
@@ -48,12 +49,12 @@ class PageSaveHelper @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
contentResolver.openOutputStream(destination)?.use { output ->
|
contentResolver.openOutputStream(destination)
|
||||||
pageFile.inputStream().use { input ->
|
}?.use { output ->
|
||||||
input.copyTo(output)
|
pageFile.inputStream().use { input ->
|
||||||
}
|
input.copyToSuspending(output)
|
||||||
} ?: throw IOException("Output stream is null")
|
}
|
||||||
}
|
} ?: throw IOException("Output stream is null")
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.annotation.CallSuper
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
|
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -13,11 +14,12 @@ abstract class BasePageHolder<B : ViewBinding>(
|
|||||||
protected val binding: B,
|
protected val binding: B,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkStateObserver: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
|
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver)
|
protected val delegate = PageHolderDelegate(loader, settings, this, networkStateObserver, exceptionResolver)
|
||||||
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
|
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
|
||||||
|
|
||||||
val context: Context
|
val context: Context
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||||
private val loader: PageLoader,
|
private val loader: PageLoader,
|
||||||
private val readerSettings: ReaderSettings,
|
private val readerSettings: ReaderSettings,
|
||||||
|
private val networkState: NetworkStateObserver,
|
||||||
private val exceptionResolver: ExceptionResolver,
|
private val exceptionResolver: ExceptionResolver,
|
||||||
) : RecyclerView.Adapter<H>() {
|
) : RecyclerView.Adapter<H>() {
|
||||||
|
|
||||||
@@ -56,9 +58,9 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
|||||||
final override fun onCreateViewHolder(
|
final override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int,
|
viewType: Int,
|
||||||
): H = onCreateViewHolder(parent, loader, readerSettings, exceptionResolver)
|
): H = onCreateViewHolder(parent, loader, readerSettings, networkState, exceptionResolver)
|
||||||
|
|
||||||
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
|
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine { cont ->
|
||||||
differ.submitList(items) {
|
differ.submitList(items) {
|
||||||
cont.resume(Unit)
|
cont.resume(Unit)
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
|||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
): H
|
): H
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -26,6 +28,7 @@ class PageHolderDelegate(
|
|||||||
private val loader: PageLoader,
|
private val loader: PageLoader,
|
||||||
private val readerSettings: ReaderSettings,
|
private val readerSettings: ReaderSettings,
|
||||||
private val callback: Callback,
|
private val callback: Callback,
|
||||||
|
private val networkState: NetworkStateObserver,
|
||||||
private val exceptionResolver: ExceptionResolver,
|
private val exceptionResolver: ExceptionResolver,
|
||||||
) : DefaultOnImageEventListener, Observer<ReaderSettings> {
|
) : DefaultOnImageEventListener, Observer<ReaderSettings> {
|
||||||
|
|
||||||
@@ -118,29 +121,35 @@ class PageHolderDelegate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun CoroutineScope.doLoad(data: MangaPage, force: Boolean) {
|
private suspend fun doLoad(data: MangaPage, force: Boolean) {
|
||||||
state = State.LOADING
|
state = State.LOADING
|
||||||
error = null
|
error = null
|
||||||
callback.onLoadingStarted()
|
callback.onLoadingStarted()
|
||||||
try {
|
try {
|
||||||
val task = loader.loadPageAsync(data, force)
|
val task = loader.loadPageAsync(data, force)
|
||||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
file = coroutineScope {
|
||||||
val file = task.await()
|
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||||
progressObserver.cancel()
|
val file = task.await()
|
||||||
this@PageHolderDelegate.file = file
|
progressObserver.cancel()
|
||||||
|
file
|
||||||
|
}
|
||||||
state = State.LOADED
|
state = State.LOADED
|
||||||
callback.onImageReady(file.toUri())
|
callback.onImageReady(checkNotNull(file).toUri())
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
state = State.ERROR
|
state = State.ERROR
|
||||||
error = e
|
error = e
|
||||||
callback.onError(e)
|
callback.onError(e)
|
||||||
|
if (e is IOException && !networkState.value) {
|
||||||
|
networkState.awaitForConnection()
|
||||||
|
retry(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||||
.debounce(500)
|
.debounce(250)
|
||||||
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.widget.FrameLayout
|
|||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -15,8 +16,9 @@ class ReversedPageHolder(
|
|||||||
binding: ItemPageBinding,
|
binding: ItemPageBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : PageHolder(binding, loader, settings, exceptionResolver) {
|
) : PageHolder(binding, loader, settings, networkState, exceptionResolver) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
||||||
@@ -35,6 +37,7 @@ class ReversedPageHolder(
|
|||||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||||
resetScaleAndCenter()
|
resetScaleAndCenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomMode.FIT_HEIGHT -> {
|
ZoomMode.FIT_HEIGHT -> {
|
||||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||||
minScale = height / sHeight.toFloat()
|
minScale = height / sHeight.toFloat()
|
||||||
@@ -43,6 +46,7 @@ class ReversedPageHolder(
|
|||||||
PointF(sWidth.toFloat(), sHeight / 2f),
|
PointF(sWidth.toFloat(), sHeight / 2f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomMode.FIT_WIDTH -> {
|
ZoomMode.FIT_WIDTH -> {
|
||||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||||
minScale = width / sWidth.toFloat()
|
minScale = width / sWidth.toFloat()
|
||||||
@@ -51,6 +55,7 @@ class ReversedPageHolder(
|
|||||||
PointF(sWidth / 2f, 0f),
|
PointF(sWidth / 2f, 0f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomMode.KEEP_START -> {
|
ZoomMode.KEEP_START -> {
|
||||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||||
setScaleAndCenter(
|
setScaleAndCenter(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|||||||
class ReversedPagesAdapter(
|
class ReversedPagesAdapter(
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) {
|
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, networkState, exceptionResolver) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = ReversedPageHolder(
|
) = ReversedPageHolder(
|
||||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
settings = settings,
|
||||||
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
@@ -19,10 +19,15 @@ import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
|||||||
import org.koitharu.kotatsu.utils.ext.recyclerView
|
import org.koitharu.kotatsu.utils.ext.recyclerView
|
||||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkStateObserver: NetworkStateObserver
|
||||||
|
|
||||||
private var pagerAdapter: ReversedPagesAdapter? = null
|
private var pagerAdapter: ReversedPagesAdapter? = null
|
||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
@@ -33,7 +38,12 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
|||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
|
pagerAdapter = ReversedPagesAdapter(
|
||||||
|
viewModel.pageLoader,
|
||||||
|
viewModel.readerSettings,
|
||||||
|
networkStateObserver,
|
||||||
|
exceptionResolver,
|
||||||
|
)
|
||||||
with(binding.pager) {
|
with(binding.pager) {
|
||||||
adapter = pagerAdapter
|
adapter = pagerAdapter
|
||||||
offscreenPageLimit = 2
|
offscreenPageLimit = 2
|
||||||
@@ -44,8 +54,8 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
|||||||
val transformer = if (it) ReversedPageAnimTransformer() else null
|
val transformer = if (it) ReversedPageAnimTransformer() else null
|
||||||
binding.pager.setPageTransformer(transformer)
|
binding.pager.setPageTransformer(transformer)
|
||||||
if (transformer == null) {
|
if (transformer == null) {
|
||||||
binding.pager.recyclerView?.children?.forEach {
|
binding.pager.recyclerView?.children?.forEach { v ->
|
||||||
it.resetTransformations()
|
v.resetTransformations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|||||||
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.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -21,8 +22,9 @@ open class PageHolder(
|
|||||||
binding: ItemPageBinding,
|
binding: ItemPageBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver),
|
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -74,6 +76,7 @@ open class PageHolder(
|
|||||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||||
binding.ssiv.resetScaleAndCenter()
|
binding.ssiv.resetScaleAndCenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomMode.FIT_HEIGHT -> {
|
ZoomMode.FIT_HEIGHT -> {
|
||||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||||
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
|
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
|
||||||
@@ -82,6 +85,7 @@ open class PageHolder(
|
|||||||
PointF(0f, binding.ssiv.sHeight / 2f),
|
PointF(0f, binding.ssiv.sHeight / 2f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomMode.FIT_WIDTH -> {
|
ZoomMode.FIT_WIDTH -> {
|
||||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||||
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
|
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
|
||||||
@@ -90,6 +94,7 @@ open class PageHolder(
|
|||||||
PointF(binding.ssiv.sWidth / 2f, 0f),
|
PointF(binding.ssiv.sWidth / 2f, 0f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomMode.KEEP_START -> {
|
ZoomMode.KEEP_START -> {
|
||||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||||
binding.ssiv.setScaleAndCenter(
|
binding.ssiv.setScaleAndCenter(
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
@@ -18,10 +18,15 @@ import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
|||||||
import org.koitharu.kotatsu.utils.ext.recyclerView
|
import org.koitharu.kotatsu.utils.ext.recyclerView
|
||||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkStateObserver: NetworkStateObserver
|
||||||
|
|
||||||
private var pagesAdapter: PagesAdapter? = null
|
private var pagesAdapter: PagesAdapter? = null
|
||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
@@ -32,7 +37,12 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
|||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
pagesAdapter = PagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
|
pagesAdapter = PagesAdapter(
|
||||||
|
viewModel.pageLoader,
|
||||||
|
viewModel.readerSettings,
|
||||||
|
networkStateObserver,
|
||||||
|
exceptionResolver,
|
||||||
|
)
|
||||||
with(binding.pager) {
|
with(binding.pager) {
|
||||||
adapter = pagesAdapter
|
adapter = pagesAdapter
|
||||||
offscreenPageLimit = 2
|
offscreenPageLimit = 2
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|||||||
class PagesAdapter(
|
class PagesAdapter(
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkStateObserver: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
|
) : BaseReaderAdapter<PageHolder>(loader, settings, networkStateObserver, exceptionResolver) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = PageHolder(
|
) = PageHolder(
|
||||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
settings = settings,
|
||||||
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
@@ -12,13 +12,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|||||||
class WebtoonAdapter(
|
class WebtoonAdapter(
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
|
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, networkState, exceptionResolver) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = WebtoonHolder(
|
) = WebtoonHolder(
|
||||||
binding = ItemPageWebtoonBinding.inflate(
|
binding = ItemPageWebtoonBinding.inflate(
|
||||||
@@ -28,6 +30,7 @@ class WebtoonAdapter(
|
|||||||
),
|
),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
settings = settings,
|
||||||
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|||||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
||||||
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.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
|
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.hideCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.ifZero
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setProgressCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.showCompat
|
||||||
|
|
||||||
class WebtoonHolder(
|
class WebtoonHolder(
|
||||||
binding: ItemPageWebtoonBinding,
|
binding: ItemPageWebtoonBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
settings: ReaderSettings,
|
||||||
|
networkState: NetworkStateObserver,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
|
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, networkState, exceptionResolver),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
private var scrollToRestore = 0
|
private var scrollToRestore = 0
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup
|
|||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
@@ -15,10 +16,14 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
|||||||
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
||||||
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
|
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkStateObserver: NetworkStateObserver
|
||||||
|
|
||||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||||
private var webtoonAdapter: WebtoonAdapter? = null
|
private var webtoonAdapter: WebtoonAdapter? = null
|
||||||
|
|
||||||
@@ -29,7 +34,12 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
|
webtoonAdapter = WebtoonAdapter(
|
||||||
|
viewModel.pageLoader,
|
||||||
|
viewModel.readerSettings,
|
||||||
|
networkStateObserver,
|
||||||
|
exceptionResolver,
|
||||||
|
)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = webtoonAdapter
|
adapter = webtoonAdapter
|
||||||
|
|||||||
34
app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt
Normal file
34
app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.koitharu.kotatsu.utils.progress.ProgressResponseBody
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
suspend fun InputStream.copyToSuspending(
|
||||||
|
out: OutputStream,
|
||||||
|
bufferSize: Int = DEFAULT_BUFFER_SIZE
|
||||||
|
): Long = withContext(Dispatchers.IO) {
|
||||||
|
val job = currentCoroutineContext()[Job]
|
||||||
|
var bytesCopied: Long = 0
|
||||||
|
val buffer = ByteArray(bufferSize)
|
||||||
|
var bytes = read(buffer)
|
||||||
|
while (bytes >= 0) {
|
||||||
|
out.write(buffer, 0, bytes)
|
||||||
|
bytesCopied += bytes
|
||||||
|
job?.ensureActive()
|
||||||
|
bytes = read(buffer)
|
||||||
|
job?.ensureActive()
|
||||||
|
}
|
||||||
|
bytesCopied
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ResponseBody.withProgress(progressState: MutableStateFlow<Float>): ResponseBody {
|
||||||
|
return ProgressResponseBody(this, progressState)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.progress
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.ForwardingSource
|
||||||
|
import okio.Source
|
||||||
|
import okio.buffer
|
||||||
|
|
||||||
|
class ProgressResponseBody(
|
||||||
|
private val delegate: ResponseBody,
|
||||||
|
private val progressState: MutableStateFlow<Float>,
|
||||||
|
) : ResponseBody() {
|
||||||
|
|
||||||
|
private var bufferedSource: BufferedSource? = null
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
delegate.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentLength(): Long = delegate.contentLength()
|
||||||
|
|
||||||
|
override fun contentType(): MediaType? = delegate.contentType()
|
||||||
|
|
||||||
|
override fun source(): BufferedSource {
|
||||||
|
return bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also {
|
||||||
|
bufferedSource = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProgressSource(
|
||||||
|
delegate: Source,
|
||||||
|
private val contentLength: Long,
|
||||||
|
private val progressState: MutableStateFlow<Float>,
|
||||||
|
) : ForwardingSource(delegate) {
|
||||||
|
|
||||||
|
private var totalBytesRead = 0L
|
||||||
|
|
||||||
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
|
val bytesRead = super.read(sink, byteCount)
|
||||||
|
if (contentLength > 0) {
|
||||||
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||||
|
progressState.value = (totalBytesRead.toDouble() / contentLength.toDouble()).toFloat()
|
||||||
|
}
|
||||||
|
return bytesRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/src/main/res/values-sr/plurals.xml
Normal file
43
app/src/main/res/values-sr/plurals.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<plurals name="pages">
|
||||||
|
<item quantity="one">Тотално %1$d странa</item>
|
||||||
|
<item quantity="few">Тотално %1$d странице</item>
|
||||||
|
<item quantity="other">Тотално %1$d странице</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="items">
|
||||||
|
<item quantity="one">%1$d ставке</item>
|
||||||
|
<item quantity="few">%1$d ставки</item>
|
||||||
|
<item quantity="other">%1$d ставка</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="chapters_from_x">
|
||||||
|
<item quantity="one">%1$d поглавља од %2$d</item>
|
||||||
|
<item quantity="few">%1$d поглавља од %2$d</item>
|
||||||
|
<item quantity="other">%1$d поглавља од %2$d</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="minutes_ago">
|
||||||
|
<item quantity="one">пре %1$d минута</item>
|
||||||
|
<item quantity="few">пре %1$d минута</item>
|
||||||
|
<item quantity="other">пре %1$d минута</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="hours_ago">
|
||||||
|
<item quantity="one">пре %1$d сата</item>
|
||||||
|
<item quantity="few">пре %1$d сата</item>
|
||||||
|
<item quantity="other">пре %1$d сата</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="days_ago">
|
||||||
|
<item quantity="one">пре %1$d дана</item>
|
||||||
|
<item quantity="few">пре %1$d дана</item>
|
||||||
|
<item quantity="other">пре %1$d дана</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="new_chapters">
|
||||||
|
<item quantity="one">%1$d нова поглавља</item>
|
||||||
|
<item quantity="few">%1$d нових поглавља</item>
|
||||||
|
<item quantity="other">%1$d нових поглавља</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="chapters">
|
||||||
|
<item quantity="one">%1$d поглављe</item>
|
||||||
|
<item quantity="few">%1$d поглавља</item>
|
||||||
|
<item quantity="other">%1$d поглавља</item>
|
||||||
|
</plurals>
|
||||||
|
</resources>
|
||||||
15
app/src/main/res/values-sr/strings.xml
Normal file
15
app/src/main/res/values-sr/strings.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="local_storage">Локално складиште</string>
|
||||||
|
<string name="close_menu">Затвори мени</string>
|
||||||
|
<string name="error_occurred">Грешка се појавила</string>
|
||||||
|
<string name="open_menu">Отвори мени</string>
|
||||||
|
<string name="favourites">Фаворити</string>
|
||||||
|
<string name="history">Историја</string>
|
||||||
|
<string name="network_error">Неуспешно повезивање са интернетом</string>
|
||||||
|
<string name="details">Детаљи</string>
|
||||||
|
<string name="chapters">Поглавља</string>
|
||||||
|
<string name="list">Листа</string>
|
||||||
|
<string name="detailed_list">Детаљна листа</string>
|
||||||
|
<string name="grid">Табла</string>
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user