Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08764cb3cb | ||
|
|
9c52545f63 | ||
|
|
a6c30d33d4 | ||
|
|
25974af229 | ||
|
|
607dfc9be3 | ||
|
|
560e669700 | ||
|
|
ba403c9360 | ||
|
|
0f1c9ff05d | ||
|
|
662f08e115 | ||
|
|
d647a32e9f | ||
|
|
375e72cb98 | ||
|
|
34c7cafdfe | ||
|
|
03e0eefe4d | ||
|
|
f41425f03d | ||
|
|
400b91278f | ||
|
|
9088f77ae5 | ||
|
|
86da3217d1 | ||
|
|
24908e52af | ||
|
|
1261a6790d | ||
|
|
59fa61864a | ||
|
|
1cbfe017ea | ||
|
|
f469369b14 | ||
|
|
1ddcaed483 | ||
|
|
7bb7736f18 | ||
|
|
d1e7e7a2a6 | ||
|
|
0c4b7b0586 | ||
|
|
f320f22863 | ||
|
|
d224cd99bb | ||
|
|
b955d31770 | ||
|
|
b4eb8d56a6 | ||
|
|
c896ac72e8 | ||
|
|
b599cb33ff |
@@ -26,7 +26,7 @@ Download APK directly from GitHub:
|
||||
* Notifications about new chapters with updates feed
|
||||
* Shikimori integration (manga tracking)
|
||||
* 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
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 503
|
||||
versionName '4.0.3'
|
||||
versionCode 507
|
||||
versionName '4.1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -83,7 +83,7 @@ afterEvaluate {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:bf8a1f3db2') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:9ee1c21a67') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ dependencies {
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.6.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.4'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.5'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
||||
@@ -118,35 +118,35 @@ dependencies {
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||
|
||||
implementation "com.google.dagger:hilt-android:2.44"
|
||||
kapt "com.google.dagger:hilt-compiler:2.44"
|
||||
implementation 'com.google.dagger:hilt-android:2.44.2'
|
||||
kapt 'com.google.dagger:hilt-compiler:2.44.2'
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.2.2'
|
||||
implementation 'io.coil-kt:coil-svg:2.2.2'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:f8a38b08fe'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:95e6c188c6'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
|
||||
implementation 'ch.acra:acra-http:5.9.6'
|
||||
implementation 'ch.acra:acra-dialog:5.9.6'
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
implementation 'ch.acra:acra-dialog:5.9.7'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20220924'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.4'
|
||||
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
androidTestImplementation 'androidx.room:room-testing:2.4.3'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
||||
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44.2'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44.2'
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
|
||||
@@ -2,20 +2,20 @@ package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.util.ArrayMap
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.savedstate.SavedStateRegistry
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private const val PROVIDER_NAME = "selection_decoration_sectioned"
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.network.*
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -39,6 +40,7 @@ import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||
@@ -81,6 +83,12 @@ interface AppModule {
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkState(
|
||||
@ApplicationContext context: Context
|
||||
) = NetworkState(context.connectivityManager)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMangaDatabase(
|
||||
|
||||
@@ -14,11 +14,11 @@ abstract class MangaDao {
|
||||
abstract suspend fun find(id: Long): MangaWithTags?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@@ -47,4 +47,4 @@ abstract class MangaDao {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||
|
||||
import android.util.ArrayMap
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.koitharu.kotatsu.R
|
||||
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.utils.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.utils.isSuccess
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class ExceptionResolver private constructor(
|
||||
private val activity: FragmentActivity?,
|
||||
@@ -49,6 +49,7 @@ class ExceptionResolver private constructor(
|
||||
openInBrowser(e.url)
|
||||
false
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkRequest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.koitharu.kotatsu.utils.MediatorStateFlow
|
||||
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||
|
||||
class NetworkState(
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
) : MediatorStateFlow<Boolean>(connectivityManager.isNetworkAvailable) {
|
||||
|
||||
private val callback = NetworkCallbackImpl()
|
||||
|
||||
override fun onActive() {
|
||||
invalidate()
|
||||
val request = NetworkRequest.Builder().build()
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
|
||||
suspend fun awaitForConnection() {
|
||||
if (value) {
|
||||
return
|
||||
}
|
||||
first { it }
|
||||
}
|
||||
|
||||
private fun invalidate() {
|
||||
publishValue(connectivityManager.isNetworkAvailable)
|
||||
}
|
||||
|
||||
private inner class NetworkCallbackImpl : NetworkCallback() {
|
||||
|
||||
override fun onAvailable(network: Network) = invalidate()
|
||||
|
||||
override fun onLost(network: Network) = invalidate()
|
||||
|
||||
override fun onUnavailable() = invalidate()
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onSuccess
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NetworkStateObserver @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : StateFlow<Boolean> {
|
||||
|
||||
private val connectivityManager = context.connectivityManager
|
||||
|
||||
override val replayCache: List<Boolean>
|
||||
get() = listOf(value)
|
||||
|
||||
override val value: Boolean
|
||||
get() = connectivityManager.isNetworkAvailable
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<Boolean>): Nothing {
|
||||
collector.emit(value)
|
||||
while (true) {
|
||||
observeImpl().collect(collector)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeImpl() = callbackFlow<Boolean> {
|
||||
val request = NetworkRequest.Builder().build()
|
||||
val callback = FlowNetworkCallback(this)
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FlowNetworkCallback(
|
||||
private val producerScope: ProducerScope<Boolean>,
|
||||
) : NetworkCallback() {
|
||||
|
||||
private var prevValue = value
|
||||
|
||||
override fun onAvailable(network: Network) = update()
|
||||
|
||||
override fun onLost(network: Network) = update()
|
||||
|
||||
override fun onUnavailable() = update()
|
||||
|
||||
private fun update() {
|
||||
val newValue = connectivityManager.isNetworkAvailable
|
||||
if (newValue != prevValue) {
|
||||
producerScope.trySendBlocking(newValue).onSuccess {
|
||||
prevValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,4 +115,4 @@ class ZipOutput(
|
||||
closeEntry()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
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.MangaSource
|
||||
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.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
@@ -219,10 +219,8 @@ class DownloadManager @AssistedInject constructor(
|
||||
val call = okHttp.newCall(request)
|
||||
val file = File(destination, tempFileName)
|
||||
val response = call.clone().await()
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
file.outputStream().use { out ->
|
||||
checkNotNull(response.body).byteStream().copyTo(out)
|
||||
}
|
||||
file.outputStream().use { out ->
|
||||
checkNotNull(response.body).byteStream().copyToSuspending(out)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.explore.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -11,11 +13,12 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
||||
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
@@ -31,6 +34,7 @@ import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ExploreFragment :
|
||||
@@ -67,6 +71,7 @@ class ExploreFragment :
|
||||
}
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
|
||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -95,6 +100,7 @@ class ExploreFragment :
|
||||
viewModel.openRandom()
|
||||
return
|
||||
}
|
||||
|
||||
else -> return
|
||||
}
|
||||
startActivity(intent)
|
||||
@@ -105,6 +111,14 @@ class ExploreFragment :
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: ExploreItem.Source, view: View): Boolean {
|
||||
val menu = PopupMenu(view.context, view)
|
||||
menu.inflate(R.menu.popup_source)
|
||||
menu.setOnMenuItemClickListener(SourceMenuListener(item))
|
||||
menu.show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
|
||||
override fun onEmptyActionClick() = onManageClick(requireView())
|
||||
@@ -124,6 +138,37 @@ class ExploreFragment :
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun onActionDone(action: ReversibleAction) {
|
||||
val handle = action.handle
|
||||
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
||||
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
|
||||
if (handle != null) {
|
||||
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
|
||||
}
|
||||
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private inner class SourceMenuListener(
|
||||
private val sourceItem: ExploreItem.Source,
|
||||
) : PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
|
||||
}
|
||||
|
||||
R.id.action_hide -> {
|
||||
viewModel.hideSource(sourceItem.source)
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = ExploreFragment()
|
||||
|
||||
@@ -4,11 +4,17 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||
@@ -16,6 +22,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ExploreViewModel @Inject constructor(
|
||||
@@ -24,6 +31,7 @@ class ExploreViewModel @Inject constructor(
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onOpenManga = SingleLiveEvent<Manga>()
|
||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||
|
||||
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
||||
if (loading) {
|
||||
@@ -40,6 +48,16 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun hideSource(source: MangaSource) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
settings.hiddenSources += source.name
|
||||
val rollback = ReversibleHandle {
|
||||
settings.hiddenSources -= source.name
|
||||
}
|
||||
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createContentFlow() = settings.observe()
|
||||
.filter {
|
||||
it == AppSettings.KEY_SOURCES_HIDDEN ||
|
||||
|
||||
@@ -3,22 +3,30 @@ package org.koitharu.kotatsu.local.data
|
||||
import android.content.Context
|
||||
import com.tomclaw.cache.DiskLruCache
|
||||
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 org.koitharu.kotatsu.utils.ext.takeIfWriteable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
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
|
||||
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val cacheDir = context.externalCacheDir ?: context.cacheDir
|
||||
private val cacheDir = checkNotNull(findSuitableDir(context)) {
|
||||
val dirs = (context.externalCacheDirs + context.cacheDir).joinToString(";") {
|
||||
it.absolutePath
|
||||
}
|
||||
"Cannot find any suitable directory for PagesCache: [$dirs]"
|
||||
}
|
||||
private val lruCache = createDiskLruCacheSafe(
|
||||
dir = cacheDir.subdir(CacheDir.PAGES.dir),
|
||||
dir = cacheDir,
|
||||
size = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||
)
|
||||
|
||||
@@ -26,42 +34,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
return lruCache.get(url)?.takeIfReadable()
|
||||
}
|
||||
|
||||
fun put(url: String, inputStream: InputStream): File {
|
||||
val file = File(cacheDir, url.longHashCode().toString())
|
||||
file.outputStream().use { out ->
|
||||
inputStream.copyTo(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)
|
||||
suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
|
||||
val file = File(cacheDir.parentFile, url.longHashCode().toString())
|
||||
try {
|
||||
file.outputStream().use { out ->
|
||||
inputStream.copyToSuspending(out)
|
||||
}
|
||||
}
|
||||
val res = lruCache.put(url, file)
|
||||
file.delete()
|
||||
return res
|
||||
}
|
||||
|
||||
private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) {
|
||||
if (contentLength > 0) {
|
||||
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
|
||||
lruCache.put(url, file)
|
||||
} finally {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,3 +56,10 @@ private fun createDiskLruCacheSafe(dir: File, size: Long): DiskLruCache {
|
||||
DiskLruCache.create(dir, size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSuitableDir(context: Context): File? {
|
||||
val dirs = context.externalCacheDirs + context.cacheDir
|
||||
return dirs.firstNotNullOfOrNull {
|
||||
it.subdir(CacheDir.PAGES.dir).takeIfWriteable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
@@ -14,8 +13,11 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||
import org.koitharu.kotatsu.utils.AlphanumComparator
|
||||
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.utils.ext.longOf
|
||||
import java.io.File
|
||||
|
||||
// TODO: Add support for chapters in cbz
|
||||
// https://github.com/KotatsuApp/Kotatsu/issues/31
|
||||
@@ -57,11 +59,12 @@ class DirMangaImporter(
|
||||
|
||||
private suspend fun addPages(output: CbzMangaOutput, root: DocumentFile, path: String, state: State) {
|
||||
var number = 0
|
||||
for (file in root.listFiles()) {
|
||||
for (file in root.listFiles().sortedWith(compareBy(AlphanumComparator()) { it.name.orEmpty() })) {
|
||||
when {
|
||||
file.isDirectory -> {
|
||||
addPages(output, file, path + "/" + file.name, state)
|
||||
}
|
||||
|
||||
file.isFile -> {
|
||||
val tempFile = file.asTempFile()
|
||||
if (!state.hasCover) {
|
||||
@@ -86,7 +89,7 @@ class DirMangaImporter(
|
||||
"Cannot open input stream for $uri"
|
||||
}.use { input ->
|
||||
file.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
input.copyToSuspending(output)
|
||||
}
|
||||
}
|
||||
return file
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.koitharu.kotatsu.local.domain.importer
|
||||
|
||||
import android.net.Uri
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
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.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||
import org.koitharu.kotatsu.utils.ext.resolveName
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ZipMangaImporter(
|
||||
storageManager: LocalStorageManager,
|
||||
@@ -27,10 +28,10 @@ class ZipMangaImporter(
|
||||
}
|
||||
val dest = File(getOutputDir(), name)
|
||||
runInterruptible {
|
||||
contentResolver.openInputStream(uri)?.use { source ->
|
||||
dest.outputStream().use { output ->
|
||||
source.copyTo(output)
|
||||
}
|
||||
contentResolver.openInputStream(uri)
|
||||
}?.use { source ->
|
||||
dest.outputStream().use { output ->
|
||||
source.copyToSuspending(output)
|
||||
}
|
||||
} ?: throw IOException("Cannot open input stream: $uri")
|
||||
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.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.withProgress
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||
import java.io.File
|
||||
import java.util.LinkedList
|
||||
@@ -179,9 +180,12 @@ class PageLoader @Inject constructor(
|
||||
val uri = Uri.parse(pageUrl)
|
||||
return if (uri.scheme == "cbz") {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
ZipFile(uri.schemeSpecificPart)
|
||||
}.use { zip ->
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry)
|
||||
}.use {
|
||||
cache.put(pageUrl, it)
|
||||
}
|
||||
}
|
||||
@@ -200,10 +204,8 @@ class PageLoader @Inject constructor(
|
||||
val body = checkNotNull(response.body) {
|
||||
"Null response"
|
||||
}
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
body.byteStream().use {
|
||||
cache.put(pageUrl, it, body.contentLength(), progress)
|
||||
}
|
||||
body.withProgress(progress).byteStream().use {
|
||||
cache.put(pageUrl, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@ import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.core.net.toUri
|
||||
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.runInterruptible
|
||||
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.util.toFileNameSafe
|
||||
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 EXTENSION_FALLBACK = "png"
|
||||
@@ -48,12 +49,12 @@ class PageSaveHelper @Inject constructor(
|
||||
}
|
||||
}
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
contentResolver.openOutputStream(destination)?.use { output ->
|
||||
pageFile.inputStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} ?: throw IOException("Output stream is null")
|
||||
}
|
||||
contentResolver.openOutputStream(destination)
|
||||
}?.use { output ->
|
||||
pageFile.inputStream().use { input ->
|
||||
input.copyToSuspending(output)
|
||||
}
|
||||
} ?: throw IOException("Output stream is null")
|
||||
return destination
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.annotation.CallSuper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -13,11 +14,12 @@ abstract class BasePageHolder<B : ViewBinding>(
|
||||
protected val binding: B,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver)
|
||||
protected val delegate = PageHolderDelegate(loader, settings, this, networkState, exceptionResolver)
|
||||
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
|
||||
|
||||
val context: Context
|
||||
|
||||
@@ -4,17 +4,19 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
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.os.NetworkState
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
private val loader: PageLoader,
|
||||
private val readerSettings: ReaderSettings,
|
||||
private val networkState: NetworkState,
|
||||
private val exceptionResolver: ExceptionResolver,
|
||||
) : RecyclerView.Adapter<H>() {
|
||||
|
||||
@@ -56,9 +58,9 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
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) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
@@ -68,6 +70,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
): H
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -16,9 +17,11 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@@ -26,6 +29,7 @@ class PageHolderDelegate(
|
||||
private val loader: PageLoader,
|
||||
private val readerSettings: ReaderSettings,
|
||||
private val callback: Callback,
|
||||
private val networkState: NetworkState,
|
||||
private val exceptionResolver: ExceptionResolver,
|
||||
) : DefaultOnImageEventListener, Observer<ReaderSettings> {
|
||||
|
||||
@@ -118,29 +122,36 @@ class PageHolderDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CoroutineScope.doLoad(data: MangaPage, force: Boolean) {
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) {
|
||||
state = State.LOADING
|
||||
error = null
|
||||
callback.onLoadingStarted()
|
||||
try {
|
||||
val task = loader.loadPageAsync(data, force)
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
progressObserver.cancel()
|
||||
this@PageHolderDelegate.file = file
|
||||
file = coroutineScope {
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
progressObserver.cancel()
|
||||
file
|
||||
}
|
||||
state = State.LOADED
|
||||
callback.onImageReady(file.toUri())
|
||||
callback.onImageReady(checkNotNull(file).toUri())
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
state = State.ERROR
|
||||
error = e
|
||||
callback.onError(e)
|
||||
if (e is IOException && !networkState.value) {
|
||||
networkState.awaitForConnection()
|
||||
retry(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||
.debounce(500)
|
||||
.debounce(250)
|
||||
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
||||
.launchIn(scope)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.widget.FrameLayout
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -15,8 +16,9 @@ class ReversedPageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : PageHolder(binding, loader, settings, exceptionResolver) {
|
||||
) : PageHolder(binding, loader, settings, networkState, exceptionResolver) {
|
||||
|
||||
init {
|
||||
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
||||
@@ -35,6 +37,7 @@ class ReversedPageHolder(
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
resetScaleAndCenter()
|
||||
}
|
||||
|
||||
ZoomMode.FIT_HEIGHT -> {
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
minScale = height / sHeight.toFloat()
|
||||
@@ -43,6 +46,7 @@ class ReversedPageHolder(
|
||||
PointF(sWidth.toFloat(), sHeight / 2f),
|
||||
)
|
||||
}
|
||||
|
||||
ZoomMode.FIT_WIDTH -> {
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
minScale = width / sWidth.toFloat()
|
||||
@@ -51,6 +55,7 @@ class ReversedPageHolder(
|
||||
PointF(sWidth / 2f, 0f),
|
||||
)
|
||||
}
|
||||
|
||||
ZoomMode.KEEP_START -> {
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
setScaleAndCenter(
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class ReversedPagesAdapter(
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) {
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = ReversedPageHolder(
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlinx.coroutines.async
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
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.resetTransformations
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
|
||||
private var pagerAdapter: ReversedPagesAdapter? = null
|
||||
|
||||
override fun onInflateView(
|
||||
@@ -33,7 +38,12 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
|
||||
pagerAdapter = ReversedPagesAdapter(
|
||||
viewModel.pageLoader,
|
||||
viewModel.readerSettings,
|
||||
networkState,
|
||||
exceptionResolver,
|
||||
)
|
||||
with(binding.pager) {
|
||||
adapter = pagerAdapter
|
||||
offscreenPageLimit = 2
|
||||
@@ -44,8 +54,8 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
val transformer = if (it) ReversedPageAnimTransformer() else null
|
||||
binding.pager.setPageTransformer(transformer)
|
||||
if (transformer == null) {
|
||||
binding.pager.recyclerView?.children?.forEach {
|
||||
it.resetTransformations()
|
||||
binding.pager.recyclerView?.children?.forEach { v ->
|
||||
v.resetTransformations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -21,8 +22,9 @@ open class PageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver),
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver),
|
||||
View.OnClickListener {
|
||||
|
||||
init {
|
||||
@@ -74,6 +76,7 @@ open class PageHolder(
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
binding.ssiv.resetScaleAndCenter()
|
||||
}
|
||||
|
||||
ZoomMode.FIT_HEIGHT -> {
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
|
||||
@@ -82,6 +85,7 @@ open class PageHolder(
|
||||
PointF(0f, binding.ssiv.sHeight / 2f),
|
||||
)
|
||||
}
|
||||
|
||||
ZoomMode.FIT_WIDTH -> {
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
|
||||
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
|
||||
@@ -90,6 +94,7 @@ open class PageHolder(
|
||||
PointF(binding.ssiv.sWidth / 2f, 0f),
|
||||
)
|
||||
}
|
||||
|
||||
ZoomMode.KEEP_START -> {
|
||||
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
binding.ssiv.setScaleAndCenter(
|
||||
|
||||
@@ -7,8 +7,8 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlinx.coroutines.async
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
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.resetTransformations
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
|
||||
private var pagesAdapter: PagesAdapter? = null
|
||||
|
||||
override fun onInflateView(
|
||||
@@ -32,7 +37,12 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagesAdapter = PagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
|
||||
pagesAdapter = PagesAdapter(
|
||||
viewModel.pageLoader,
|
||||
viewModel.readerSettings,
|
||||
networkState,
|
||||
exceptionResolver,
|
||||
)
|
||||
with(binding.pager) {
|
||||
adapter = pagesAdapter
|
||||
offscreenPageLimit = 2
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class PagesAdapter(
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
|
||||
) : BaseReaderAdapter<PageHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = PageHolder(
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -12,13 +12,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class WebtoonAdapter(
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = WebtoonHolder(
|
||||
binding = ItemPageWebtoonBinding.inflate(
|
||||
@@ -28,6 +30,7 @@ class WebtoonAdapter(
|
||||
),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,20 +8,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
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(
|
||||
binding: ItemPageWebtoonBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, networkState, exceptionResolver),
|
||||
View.OnClickListener {
|
||||
|
||||
private var scrollToRestore = 0
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.async
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
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.firstVisibleItemPosition
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
|
||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||
private var webtoonAdapter: WebtoonAdapter? = null
|
||||
|
||||
@@ -29,7 +34,12 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
|
||||
webtoonAdapter = WebtoonAdapter(
|
||||
viewModel.pageLoader,
|
||||
viewModel.readerSettings,
|
||||
networkState,
|
||||
exceptionResolver,
|
||||
)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = webtoonAdapter
|
||||
|
||||
@@ -86,7 +86,6 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
||||
}.onSuccess { username ->
|
||||
preference.title = getString(R.string.logged_in_as, username)
|
||||
}.onFailure { error ->
|
||||
preference.isEnabled = error is AuthRequiredException
|
||||
when {
|
||||
error is AuthRequiredException -> Unit
|
||||
ExceptionResolver.canResolve(error) -> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
@@ -46,14 +46,14 @@ class ShelfViewModel @Inject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
networkStateObserver: NetworkStateObserver,
|
||||
networkState: NetworkState,
|
||||
) : BaseViewModel(), ListExtraProvider {
|
||||
|
||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||
|
||||
val content: LiveData<List<ListModel>> = combine(
|
||||
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||
networkStateObserver,
|
||||
networkState,
|
||||
repository.observeShelfContent(),
|
||||
) { sections, isConnected, content ->
|
||||
mapList(content, sections, isConnected)
|
||||
|
||||
@@ -5,16 +5,17 @@ import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.ArrayMap
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.room.InvalidationTracker
|
||||
import androidx.room.withTransaction
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
@@ -22,6 +23,9 @@ import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SyncController @Inject constructor(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.util.ArrayMap
|
||||
import java.util.*
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.LinkedList
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class CompositeMutex<T : Any> : Set<T> {
|
||||
|
||||
@@ -27,7 +27,7 @@ class CompositeMutex<T : Any> : Set<T> {
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return data.isEmpty()
|
||||
return data.isEmpty
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
@@ -59,7 +59,7 @@ class CompositeMutex<T : Any> : Set<T> {
|
||||
|
||||
private suspend fun waitForRemoval(element: T) {
|
||||
val list = data[element] ?: return
|
||||
suspendCancellableCoroutine<Unit> { continuation ->
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
list.add(continuation)
|
||||
continuation.invokeOnCancellation {
|
||||
list.remove(continuation)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
abstract class MediatorStateFlow<T>(initialValue: T) : StateFlow<T> {
|
||||
|
||||
private val delegate = MutableStateFlow(initialValue)
|
||||
private val collectors = AtomicInteger(0)
|
||||
|
||||
final override val replayCache: List<T>
|
||||
get() = delegate.replayCache
|
||||
|
||||
final override val value: T
|
||||
get() = delegate.value
|
||||
|
||||
final override suspend fun collect(collector: FlowCollector<T>): Nothing {
|
||||
try {
|
||||
if (collectors.getAndIncrement() == 0) {
|
||||
onActive()
|
||||
}
|
||||
delegate.collect(collector)
|
||||
} finally {
|
||||
if (collectors.decrementAndGet() == 0) {
|
||||
onInactive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun publishValue(v: T) {
|
||||
delegate.value = v
|
||||
}
|
||||
|
||||
abstract fun onActive()
|
||||
|
||||
abstract fun onInactive()
|
||||
}
|
||||
@@ -5,12 +5,16 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.speech.RecognizerIntent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.util.Locale
|
||||
|
||||
class VoiceInputContract : ActivityResultContract<String?, String?>() {
|
||||
|
||||
override fun createIntent(context: Context, input: String?): Intent {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||
val locale = ConfigurationCompat.getLocales(context.resources.configuration).get(0) ?: Locale.getDefault()
|
||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, locale.toLanguageTag())
|
||||
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, input)
|
||||
return intent
|
||||
}
|
||||
@@ -23,4 +27,4 @@ class VoiceInputContract : ActivityResultContract<String?, String?>() {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ fun File.subdir(name: String) = File(this, name).also {
|
||||
|
||||
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
|
||||
|
||||
fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() }
|
||||
|
||||
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
@@ -74,4 +76,4 @@ private fun computeSizeInternal(file: File): Long {
|
||||
} else {
|
||||
return file.length()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
13
app/src/main/res/menu/popup_source.xml
Normal file
13
app/src/main/res/menu/popup_source.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:title="@string/settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_hide"
|
||||
android:title="@string/hide" />
|
||||
|
||||
</menu>
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="list">Liste</string>
|
||||
<string name="chapters">Kapitel</string>
|
||||
<string name="details">Einzelheiten</string>
|
||||
<string name="network_error">Keine Verbindung zum Internet möglich</string>
|
||||
<string name="network_error">Netzwerkfehler</string>
|
||||
<string name="error_occurred">Es ist ein Fehler aufgetreten</string>
|
||||
<string name="history">Verlauf</string>
|
||||
<string name="favourites">Favoriten</string>
|
||||
@@ -372,4 +372,22 @@
|
||||
<string name="import_completed">Import abgeschlossen</string>
|
||||
<string name="import_completed_hint">Du kannst die Originaldatei aus dem Speicher löschen, um Platz zu sparen</string>
|
||||
<string name="import_will_start_soon">Import wird bald beginnen</string>
|
||||
<string name="server_error">Serverseitiger Fehler (%1$d). Bitte versuchen Sie es später noch einmal</string>
|
||||
<string name="compact">Kompakt</string>
|
||||
<string name="contrast">Kontrast</string>
|
||||
<string name="network_unavailable_hint">Schalten Sie Wi-Fi oder ein mobiles Netzwerk ein, um Manga online zu lesen</string>
|
||||
<string name="clear_new_chapters_counters">Auch klare Informationen über neue Kapitel</string>
|
||||
<string name="text_unsaved_changes_prompt">Ungespeicherte Änderungen speichern oder verwerfen\?</string>
|
||||
<string name="discard">Verwerfen</string>
|
||||
<string name="reset">Zurücksetzen</string>
|
||||
<string name="brightness">Helligkeit</string>
|
||||
<string name="color_correction_hint">Die gewählten Farbeinstellungen werden für diesen Manga in Erinnerung bleiben</string>
|
||||
<string name="color_correction">Farbkorrektur</string>
|
||||
<string name="error_no_space_left">Kein Platz mehr auf dem Gerät</string>
|
||||
<string name="different_languages">Verschiedene Sprachen</string>
|
||||
<string name="network_unavailable">Netzwerk ist nicht verfügbar</string>
|
||||
<string name="webtoon_zoom_summary">Vergrößerungs-/Verkleinerungsgesten im Webtoon-Modus zulassen (beta)</string>
|
||||
<string name="reader_control_ltr">Ergonomische Leserkontrolle</string>
|
||||
<string name="reader_control_ltr_summary">Tippe auf den rechten Rand oder drücke die rechte Taste, um immer zur nächsten Seite zu wechseln</string>
|
||||
<string name="reader_slider">Seitenwechsel-Schieberegler anzeigen</string>
|
||||
</resources>
|
||||
35
app/src/main/res/values-el/plurals.xml
Normal file
35
app/src/main/res/values-el/plurals.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">%1$d μέρα πριν</item>
|
||||
<item quantity="other">%1$d μέρες πριν</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$dστοιχείο</item>
|
||||
<item quantity="other">%1$dστοιχεία</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%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="other">%1$dκεφάλαια%2$d</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">%1$dώρα πριν</item>
|
||||
<item quantity="other">%1$dώρες πριν</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$dκεφάλαιο</item>
|
||||
<item quantity="other">%1$dκεφάλαια</item>
|
||||
</plurals>
|
||||
<plurals name="pages">
|
||||
<item quantity="one">Σύνολο%1$dσελίδα</item>
|
||||
<item quantity="other">Σύνολο%1$dσελίδες</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">%1$dλεπτό πριν</item>
|
||||
<item quantity="other">%1$d λεπτά πριν</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
81
app/src/main/res/values-el/strings.xml
Normal file
81
app/src/main/res/values-el/strings.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="local_storage">Εσωτερικός χώρος</string>
|
||||
<string name="close_menu">Κλείσιμο μενού</string>
|
||||
<string name="open_menu">Άνοιγμα μενού</string>
|
||||
<string name="favourites">Αγαπημένα</string>
|
||||
<string name="history">Ιστορικό</string>
|
||||
<string name="error_occurred">Προέκυψε σφάλμα</string>
|
||||
<string name="try_again">Επανάληψη</string>
|
||||
<string name="grid">Πλέγμα</string>
|
||||
<string name="list_mode">Εμφάνιση ως λίστα</string>
|
||||
<string name="settings">Ρυθμίσεις</string>
|
||||
<string name="remote_sources">Απομακρυσμένες πηγές</string>
|
||||
<string name="computing_">Επεξεργασία…</string>
|
||||
<string name="close">Κλείσιμο</string>
|
||||
<string name="clear_history">Εκκαθάριση ιστορικού</string>
|
||||
<string name="nothing_found">Δεν βρέθηκε τίποτα</string>
|
||||
<string name="history_is_empty">Κενό ιστορικό</string>
|
||||
<string name="read">Διάβασε</string>
|
||||
<string name="add_to_favourites">Προσθήκη στα αγαπημένα</string>
|
||||
<string name="add_new_category">Νέα κατηγορία</string>
|
||||
<string name="save">Αποθήκευση</string>
|
||||
<string name="share">Κοινοποιήση</string>
|
||||
<string name="create_shortcut">Δημιουργία συντόμευσης…</string>
|
||||
<string name="share_s">Κοινοποίηση %s</string>
|
||||
<string name="search">Αναζήτηση</string>
|
||||
<string name="search_manga">Αναζήτηση μάνγκα</string>
|
||||
<string name="manga_downloading_">Λήψη…</string>
|
||||
<string name="download_complete">Κατεβασμένο</string>
|
||||
<string name="downloads">Λήψεις</string>
|
||||
<string name="updated">Ενημερωμένο</string>
|
||||
<string name="newest">Νεότερο</string>
|
||||
<string name="by_rating">Βαθμολογία</string>
|
||||
<string name="filter">Φίλτρο</string>
|
||||
<string name="dark">Σκοτεινό</string>
|
||||
<string name="automatic">Όπως στο σύστημα</string>
|
||||
<string name="clear">Εκκαθάριση</string>
|
||||
<string name="text_clear_history_prompt">Να διαγράψετε μόνιμα όλο το ιστορικό ανάγνωσης;</string>
|
||||
<string name="remove">Διαγραφή</string>
|
||||
<string name="save_page">Αποθήκευση σελίδας</string>
|
||||
<string name="page_saved">Αποθηκευμένα</string>
|
||||
<string name="share_image">Κοινή χρήση εικόνας</string>
|
||||
<string name="_import">Εισαγωγή</string>
|
||||
<string name="delete">Διαγραφή</string>
|
||||
<string name="text_file_not_supported">Επιλέξτε ένα αρχείο ZIP ή CBZ.</string>
|
||||
<string name="no_description">Χωρίς περιγραφή</string>
|
||||
<string name="history_and_cache">Ιστορικό και μνήμη cache</string>
|
||||
<string name="clear_pages_cache">Εκκαθάριση μνήμης cache της σελίδας</string>
|
||||
<string name="cache">Προσωρινή Μνήμη</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="standard">Τυπικό</string>
|
||||
<string name="webtoon">Μάνχγουα</string>
|
||||
<string name="search_on_s">Αναζήτηση στο %s</string>
|
||||
<string name="delete_manga">Διαγραφή μάνγκα</string>
|
||||
<string name="text_delete_local_manga">Μόνιμη διαγραφή του \"%s\" από τη συσκευή;</string>
|
||||
<string name="reader_settings">Ρυθμίσεις λειτουργίας ανάγνωσης</string>
|
||||
<string name="switch_pages">Αλλαγή σελίδων</string>
|
||||
<string name="network_error">Αδυναμία σύνδεσης στο ίντερνετ</string>
|
||||
<string name="chapters">Κεφάλαια</string>
|
||||
<string name="details">Πληροφορίες</string>
|
||||
<string name="list">Λίστα</string>
|
||||
<string name="detailed_list">Λεπτομερής λίστα</string>
|
||||
<string name="loading_">Φόρτωση…</string>
|
||||
<string name="chapter_d_of_d">Κεφάλαιο%1$d από %2$d</string>
|
||||
<string name="you_have_not_favourites_yet">Δεν υπάρχουν αγαπημένα</string>
|
||||
<string name="add">Προσθήκη</string>
|
||||
<string name="enter_category_name">Εισαγωγή ονόματος κατηγορίας</string>
|
||||
<string name="processing_">Επεξεργασία…</string>
|
||||
<string name="by_name">Όνομα</string>
|
||||
<string name="popular">Δημοφιλή</string>
|
||||
<string name="sort_order">Τρόπος Ταξινόμησης</string>
|
||||
<string name="_s_removed_from_history">Το \"%s\" αφαιρέθηκε από το ιστορικό</string>
|
||||
<string name="theme">Θέμα</string>
|
||||
<string name="light">Φωτεινό</string>
|
||||
<string name="pages">Σελίδες</string>
|
||||
<string name="wait_for_loading_finish">Περιμένετε να ολοκληρωθεί η φόρτωση…</string>
|
||||
<string name="_s_deleted_from_local_storage">Το \"%s\" διαγράφηκε από τον τοπικό χώρο αποθήκευσης</string>
|
||||
<string name="operation_not_supported">Αυτή η λειτουργία δεν υποστηρίζεται</string>
|
||||
<string name="read_mode">Λειτουργία ανάγνωσης</string>
|
||||
<string name="grid_size">Μέγεθος πλέγματος</string>
|
||||
</resources>
|
||||
@@ -6,7 +6,7 @@
|
||||
<string name="favourites">Favoritos</string>
|
||||
<string name="history">Historial</string>
|
||||
<string name="error_occurred">Ocurrió un error</string>
|
||||
<string name="network_error">No se pudo conectar a Internet</string>
|
||||
<string name="network_error">Error en la red</string>
|
||||
<string name="details">Detalles</string>
|
||||
<string name="chapters">Capítulos</string>
|
||||
<string name="list">Lista</string>
|
||||
@@ -266,7 +266,7 @@
|
||||
<string name="suggestions_excluded_genres">Excluir géneros</string>
|
||||
<string name="suggestions_excluded_genres_summary">Especifica los géneros que no quieres ver en las sugerencias</string>
|
||||
<string name="removal_completed">Remoción completada</string>
|
||||
<string name="batch_manga_save_confirm">¿Estás seguro de que quieres descargar todos los manga seleccionados con todos sus capítulos\? Esta acción puede consumir mucho tráfico y almacenamiento</string>
|
||||
<string name="batch_manga_save_confirm">¿Descargar todos los mangas seleccionados y sus capítulos\? Esto puede consumir mucho tráfico y almacenamiento.</string>
|
||||
<string name="text_delete_local_manga_batch">¿Eliminar elementos seleccionados del dispositivo de forma permanente\?</string>
|
||||
<string name="hide">Ocultar</string>
|
||||
<string name="download_slowdown">Ralentización de la descarga</string>
|
||||
@@ -375,7 +375,7 @@
|
||||
<string name="manga_error_description_pattern">Detalles del error:<br><tt>%1$s</tt><br><br>1. Intenta <a href=%2$s>abrir el manga en un navegador web</a> para asegurarte de que está disponible en tu fuente<br>2. Si está disponible, envía un informe de error a los desarrolladores.</string>
|
||||
<string name="history_shortcuts_summary">Hacer que los mangas recientes estén disponibles mediante una pulsación larga en el icono de la aplicación</string>
|
||||
<string name="color_correction_hint">Los ajustes de color elegidos serán recordados para este manga</string>
|
||||
<string name="feed">Feed</string>
|
||||
<string name="feed">Fuente</string>
|
||||
<string name="downloading_manga">Descargando manga</string>
|
||||
<string name="history_shortcuts">Mostrar los accesos directos a los mangas recientes</string>
|
||||
<string name="reader_control_ltr_summary">Tocando el borde derecho o pulsando la tecla derecha se pasa siempre a la página siguiente</string>
|
||||
@@ -384,10 +384,16 @@
|
||||
<string name="brightness">Brillo</string>
|
||||
<string name="contrast">Contraste</string>
|
||||
<string name="reset">Restablecer</string>
|
||||
<string name="text_unsaved_changes_prompt">Tienes cambios sin guardar. ¿Quieres guardarlos o descartarlos\?</string>
|
||||
<string name="text_unsaved_changes_prompt">¿Guardar o descartar los cambios no guardados\?</string>
|
||||
<string name="discard">Descartar</string>
|
||||
<string name="error_no_space_left">Sin espacio en dispositivo</string>
|
||||
<string name="webtoon_zoom">Zoom de webtoon</string>
|
||||
<string name="webtoon_zoom_summary">Permitir el gesto de acercamiento/alejamiento en modo webtoon (beta)</string>
|
||||
<string name="reader_slider">Mostrar el deslizador de cambio de página</string>
|
||||
<string name="server_error">Error del servidor (%1$d). Vuelva a intentarlo más tarde</string>
|
||||
<string name="clear_new_chapters_counters">Información clara sobre los nuevos capítulos</string>
|
||||
<string name="different_languages">Diferentes idiomas</string>
|
||||
<string name="network_unavailable">La red no está disponible</string>
|
||||
<string name="compact">Compacta</string>
|
||||
<string name="network_unavailable_hint">Enciende la Wi-Fi o la red móvil para leer los mangas en línea</string>
|
||||
</resources>
|
||||
@@ -202,7 +202,7 @@
|
||||
<string name="list">Liste</string>
|
||||
<string name="chapters">Chapitres</string>
|
||||
<string name="details">Détails</string>
|
||||
<string name="network_error">Impossible de se connecter à Internet</string>
|
||||
<string name="network_error">Erreur réseau</string>
|
||||
<string name="error_occurred">Une erreur s\'est produite</string>
|
||||
<string name="history">Historique</string>
|
||||
<string name="favourites">Favoris</string>
|
||||
|
||||
@@ -102,15 +102,15 @@
|
||||
<string name="notification_sound">Suara pemberitahuan</string>
|
||||
<string name="categories_">Kategori…</string>
|
||||
<string name="rename">Ubah Nama</string>
|
||||
<string name="category_delete_confirm">"Hapus kategori \"%s\" dari favorit Anda\?
|
||||
\nSemua manga disana akan hilang."</string>
|
||||
<string name="category_delete_confirm">Hapus kategori \"%s\" dari favorit Anda\?
|
||||
\nSemua manga di situ akan hilang.</string>
|
||||
<string name="text_empty_holder_primary">Sepi juga di sini…</string>
|
||||
<string name="text_categories_holder">Anda bisa menggunakan kategori untuk mengelola favorit Anda. Tekan «+» untuk membuat kategori</string>
|
||||
<string name="text_history_holder_primary">Apa yang Anda baca akan ditampilkan di sini</string>
|
||||
<string name="text_history_holder_secondary">Cari apa untuk di baca di bilah samping.</string>
|
||||
<string name="text_local_holder_primary">Simpan sesuatu dulu</string>
|
||||
<string name="manga_shelf">Rak</string>
|
||||
<string name="recent_manga">Terbaru</string>
|
||||
<string name="recent_manga">Baru-baru ini</string>
|
||||
<string name="pages_animation">Animasi halaman</string>
|
||||
<string name="manga_save_location">Folder untuk unduhan</string>
|
||||
<string name="not_available">Tidak tersedia</string>
|
||||
@@ -298,7 +298,7 @@
|
||||
<string name="status_completed">Selesai</string>
|
||||
<string name="canceled">Dibatalkan</string>
|
||||
<string name="sync_title">Sinkronisasi data Anda</string>
|
||||
<string name="enter_email_text">Masukkan email Anda untuk melanjutkan</string>
|
||||
<string name="enter_email_text">Masukkan surel Anda untuk melanjutkan</string>
|
||||
<string name="tracking">Pelacakan</string>
|
||||
<string name="logout">Keluar</string>
|
||||
<string name="sync">Sinkronisasi</string>
|
||||
@@ -329,7 +329,7 @@
|
||||
<string name="exit_confirmation_summary">Tekan Kembali dua kali untuk keluar dari aplikasi</string>
|
||||
<string name="exit_confirmation">Konfirmasi keluar</string>
|
||||
<string name="pages_cache">Tembolok halaman</string>
|
||||
<string name="other_cache">Cache lainnya</string>
|
||||
<string name="other_cache">Tembolok lainnya</string>
|
||||
<string name="storage_usage">Penggunaan penyimpanan</string>
|
||||
<string name="available">Tersedia</string>
|
||||
<string name="incognito_mode">Mode Incognito</string>
|
||||
@@ -356,14 +356,14 @@
|
||||
<string name="crash_text">Ada sesuatu yang salah. Mohon untuk mengirim laporan kutu (bug) ke pengembang untuk membantu kami memperbaikinya.</string>
|
||||
<string name="report">Lapor</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Manga yang ditandai sebagai NSFW tidak akan ditambahkan ke riwayat dan progres Anda tidak akan disimpan</string>
|
||||
<string name="clear_cookies_summary">Bisa membantu dalam beberapa masalah. Seluruh otorisasi akan menjadi tidak valid.</string>
|
||||
<string name="clear_cookies_summary">Bisa membantu dalam beberapa masalah. Seluruh otorisasi akan menjadi tidak valid</string>
|
||||
<string name="manage">Kelola</string>
|
||||
<string name="no_manga_sources_text">Aktifkan sumber manga untuk membaca manga daring</string>
|
||||
<string name="categories_delete_confirm">Apakah Anda yakin ingin menghapus kategori favorit yang dipilih\?
|
||||
\n Semua manga di sana akan hilang dan ini tidak bisa diurungkan.</string>
|
||||
<string name="reorder">Atur Ulang</string>
|
||||
<string name="saved_manga">Manga tersimpan</string>
|
||||
<string name="confirm_exit">Tekan lagi untuk keluar</string>
|
||||
<string name="confirm_exit">Tekan Kembali lagi untuk keluar</string>
|
||||
<string name="no_chapters">Tidak ada bab</string>
|
||||
<string name="history_shortcuts">Tampilkan pintasan manga baru-baru ini</string>
|
||||
<string name="history_shortcuts_summary">Buat manga baru-baru ini tersedia dengan menekan panjang pada ikon aplikasi</string>
|
||||
@@ -372,4 +372,5 @@
|
||||
<string name="gestures_only">Hanya gestur</string>
|
||||
<string name="dns_over_https">DNS melalui HTTPS</string>
|
||||
<string name="status_dropped">Istirahat</string>
|
||||
<string name="off_short">Mati</string>
|
||||
</resources>
|
||||
@@ -63,7 +63,7 @@
|
||||
<string name="dark">ダークテーマ</string>
|
||||
<string name="pages">ページ</string>
|
||||
<string name="theme">テーマ</string>
|
||||
<string name="network_error">インターネットに接続出来ませんでした</string>
|
||||
<string name="network_error">ネットワークエラー</string>
|
||||
<string name="enter_category_name">カテゴリー名を入力してください</string>
|
||||
<string name="updated">アップデート</string>
|
||||
<string name="cache">キャッシュ</string>
|
||||
@@ -395,4 +395,5 @@
|
||||
<string name="network_unavailable_hint">Wi-Fiまたはモバイルネットワークをオンにして、オンラインでマンガを読むことができます</string>
|
||||
<string name="webtoon_zoom">Webtoonズーム</string>
|
||||
<string name="webtoon_zoom_summary">ウェブトゥーンモードでズームイン/ズームアウトのジェスチャーを可能にする(ベータ版)</string>
|
||||
<string name="compact">コンパクト</string>
|
||||
</resources>
|
||||
2
app/src/main/res/values-ko/plurals.xml
Normal file
2
app/src/main/res/values-ko/plurals.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
108
app/src/main/res/values-ko/strings.xml
Normal file
108
app/src/main/res/values-ko/strings.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="sort_order">정렬 기준</string>
|
||||
<string name="_import">불러오기</string>
|
||||
<string name="network_error">네트워크 오류</string>
|
||||
<string name="list">목록</string>
|
||||
<string name="save">저장</string>
|
||||
<string name="share">공유하기</string>
|
||||
<string name="share_s">%s 공유</string>
|
||||
<string name="search">검색하기</string>
|
||||
<string name="warning">경고</string>
|
||||
<string name="internal_storage">내부 저장소</string>
|
||||
<string name="external_storage">외부 저장소</string>
|
||||
<string name="domain">도메인</string>
|
||||
<string name="app_update_available">새 버전이 존재합니다</string>
|
||||
<string name="open_in_browser">웹 브라우저에서 열기</string>
|
||||
<string name="save_manga">저장</string>
|
||||
<string name="notifications">알림</string>
|
||||
<string name="read_from_start">처음부터 읽기</string>
|
||||
<string name="light_indicator">LED 표시</string>
|
||||
<string name="vibration">진동</string>
|
||||
<string name="rename">이름 바꾸기</string>
|
||||
<string name="category_delete_confirm">즐겨찾기에서 \"%s\" 카테고리를 제거하시겠습니까\?
|
||||
\n포함된 모든 만화가 지워집니다.</string>
|
||||
<string name="remove_category">지우기</string>
|
||||
<string name="text_search_holder_secondary">쿼리를 재구성하십시오.</string>
|
||||
<string name="text_history_holder_secondary">사이드 메뉴에서 만화를 탐색해보세요.</string>
|
||||
<string name="text_shelf_holder_primary">만화가 여기에 표시됩니다</string>
|
||||
<string name="text_shelf_holder_secondary">«탐색» 섹션에서 만화를 탐색해보세요</string>
|
||||
<string name="pages_animation">페이지 전환 효과</string>
|
||||
<string name="cannot_find_available_storage">사용 가능한 저장소 없음</string>
|
||||
<string name="done">완료</string>
|
||||
<string name="favourites_category_empty">빈 카테고리</string>
|
||||
<string name="updates">업데이트</string>
|
||||
<string name="new_version_s">새 버전: %s</string>
|
||||
<string name="waiting_for_network">네트워크 연결을 기다리는 중…</string>
|
||||
<string name="clear_updates_feed">업데이트 피드 지우기</string>
|
||||
<string name="close_menu">메뉴 닫기</string>
|
||||
<string name="open_menu">메뉴 열기</string>
|
||||
<string name="local_storage">내장 메모리</string>
|
||||
<string name="favourites">즐겨찾기</string>
|
||||
<string name="remove">지우기</string>
|
||||
<string name="settings">설정</string>
|
||||
<string name="loading_">불러오는 중…</string>
|
||||
<string name="close">닫기</string>
|
||||
<string name="try_again">다시 시도</string>
|
||||
<string name="you_have_not_favourites_yet">즐겨찾기가 비어있음</string>
|
||||
<string name="filter">필터링</string>
|
||||
<string name="light">밝게</string>
|
||||
<string name="dark">어둡게</string>
|
||||
<string name="pages">페이지</string>
|
||||
<string name="read">지금 읽기</string>
|
||||
<string name="by_name">이름 순</string>
|
||||
<string name="popular">인기 순</string>
|
||||
<string name="chapter_d_of_d">%2$d화 중 %1$d화</string>
|
||||
<string name="downloads">다운로드</string>
|
||||
<string name="by_rating">평점 순</string>
|
||||
<string name="save_page">페이지 저장</string>
|
||||
<string name="page_saved">저장됨</string>
|
||||
<string name="share_image">이미지 공유하기</string>
|
||||
<string name="text_file_not_supported">ZIP 혹은 CBZ 파일을 선택하세요.</string>
|
||||
<string name="history_and_cache">기록 및 캐시</string>
|
||||
<string name="cache">캐시</string>
|
||||
<string name="delete_manga">만화 제거</string>
|
||||
<string name="volume_buttons">볼륨 키</string>
|
||||
<string name="nothing_found">결과 없음</string>
|
||||
<string name="add_to_favourites">즐겨찾기 추가</string>
|
||||
<string name="download_complete">다운로드 완료</string>
|
||||
<string name="add_new_category">새 카테고리</string>
|
||||
<string name="search_manga">만화를 검색하세요</string>
|
||||
<string name="manga_downloading_">다운로드 중…</string>
|
||||
<string name="processing_">처리중…</string>
|
||||
<string name="updated">최근 업데이트 순</string>
|
||||
<string name="newest">최근 발간 순</string>
|
||||
<string name="automatic">시스템 설정</string>
|
||||
<string name="delete">지우기</string>
|
||||
<string name="wait_for_loading_finish">잠시만 기다려주세요…</string>
|
||||
<string name="text_file_sizes">바이트|kB|MB|GB|TB</string>
|
||||
<string name="clear_pages_cache">페이지 캐시 지우기</string>
|
||||
<string name="read_mode">읽기 모드</string>
|
||||
<string name="grid_size">격자 크기</string>
|
||||
<string name="search_on_s">%s에서 검색</string>
|
||||
<string name="text_delete_local_manga">장치에서 \"%s\"를 영구적으로 삭제하시겠습니까\?</string>
|
||||
<string name="switch_pages">페이지 전환</string>
|
||||
<string name="taps_on_edges">가장자리 탭</string>
|
||||
<string name="webtoon">웹툰</string>
|
||||
<string name="clear_search_history">검색 기록 지우기</string>
|
||||
<string name="reader_settings">읽기 모드</string>
|
||||
<string name="network_consumption_warning">이 동작은 많은 데이터 사용을</string>
|
||||
<string name="clear_thumbs_cache">썸네일 캐시 지우기</string>
|
||||
<string name="dont_ask_again">다시 묻지 않음</string>
|
||||
<string name="cancelling_">취소 중…</string>
|
||||
<string name="error">오류</string>
|
||||
<string name="application_update">업데이트 확인</string>
|
||||
<string name="show_notification_app_update">업데이트 가능 시 알림 설정</string>
|
||||
<string name="large_manga_save_confirm">이 만화에는 %s가 있습니다. 모두 저장하시겠습니까\?</string>
|
||||
<string name="favourites_categories">즐겨찾기 카테고리</string>
|
||||
<string name="download">다운로드</string>
|
||||
<string name="notifications_settings">알림 설정</string>
|
||||
<string name="notification_sound">알림음</string>
|
||||
<string name="categories_">카테고리…</string>
|
||||
<string name="text_history_holder_primary">읽은 내용이 여기에 표시됩니다</string>
|
||||
<string name="not_available">사용할 수 없음</string>
|
||||
<string name="all_favourites">모든 즐겨찾기</string>
|
||||
<string name="read_later">나중에 읽기</string>
|
||||
<string name="search_results">검색 결과</string>
|
||||
<string name="size_s">크기: %s</string>
|
||||
</resources>
|
||||
@@ -1,44 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="pages">
|
||||
<item quantity="one">Всего %1$d страница</item>
|
||||
<item quantity="few">Всего %1$d страницы</item>
|
||||
<item quantity="many">Всего %1$d страниц</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$d элемент</item>
|
||||
<item quantity="few">%1$d элемента</item>
|
||||
<item quantity="many">%1$d элементов</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%1$d новая глава</item>
|
||||
<item quantity="few">%1$d новых главы</item>
|
||||
<item quantity="many">%1$d новых глав</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$d глава</item>
|
||||
<item quantity="few">%1$d главы</item>
|
||||
<item quantity="many">%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="many">%1$d глав из %2$d</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">%1$d минуту назад</item>
|
||||
<item quantity="few">%1$d минуты назад</item>
|
||||
<item quantity="many">%1$d минут назад</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">%1$d час назад</item>
|
||||
<item quantity="few">%1$d часа назад</item>
|
||||
<item quantity="many">%1$d часов назад</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">%1$d день назад</item>
|
||||
<item quantity="few">%1$d дня назад</item>
|
||||
<item quantity="many">%1$d дней назад</item>
|
||||
</plurals>
|
||||
<plurals name="pages">
|
||||
<item quantity="one">Всего %1$d страница</item>
|
||||
<item quantity="few">Всего %1$d страницы</item>
|
||||
<item quantity="many">Всего %1$d страниц</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$d элемент</item>
|
||||
<item quantity="few">%1$d элемента</item>
|
||||
<item quantity="many">%1$d элементов</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%1$d новая глава</item>
|
||||
<item quantity="few">%1$d новые главы</item>
|
||||
<item quantity="many">%1$d новых глав</item>
|
||||
<item quantity="other">%1$d новых глав</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$d глава</item>
|
||||
<item quantity="few">%1$d главы</item>
|
||||
<item quantity="many">%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="many">%1$d глав из %2$d</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">%1$d минуту назад</item>
|
||||
<item quantity="few">%1$d минуты назад</item>
|
||||
<item quantity="many">%1$d минут назад</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">%1$d час назад</item>
|
||||
<item quantity="few">%1$d часа назад</item>
|
||||
<item quantity="many">%1$d часов назад</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">%1$d день назад</item>
|
||||
<item quantity="few">%1$d дня назад</item>
|
||||
<item quantity="many">%1$d дней назад</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -6,7 +6,7 @@
|
||||
<string name="favourites">Избранное</string>
|
||||
<string name="history">История</string>
|
||||
<string name="error_occurred">Произошла ошибка</string>
|
||||
<string name="network_error">Не удалось подключиться к интернету</string>
|
||||
<string name="network_error">Ошибка сети</string>
|
||||
<string name="details">Подробности</string>
|
||||
<string name="chapters">Главы</string>
|
||||
<string name="list">Список</string>
|
||||
@@ -396,4 +396,5 @@
|
||||
<string name="clear_new_chapters_counters">Также очистить информацию о новых главах</string>
|
||||
<string name="server_error">Внутренняя ошибка сервера (%1$d). Повторите попытку позже</string>
|
||||
<string name="compact">Компактно</string>
|
||||
<string name="source_disabled">Источник отключен</string>
|
||||
</resources>
|
||||
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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="network_error">İnternete bağlı olduğunuzdan emin olunuz</string>
|
||||
<string name="network_error">Ağ hatası</string>
|
||||
<string name="close_menu">Menüyü kapat</string>
|
||||
<string name="open_menu">Menüyü aç</string>
|
||||
<string name="local_storage">Dahili Depolama</string>
|
||||
@@ -395,4 +395,5 @@
|
||||
<string name="saved_manga">Kaydedilen mangalar</string>
|
||||
<string name="history_shortcuts_summary">Uygulama simgesine uzun basarak son mangaları kullanılabilir hale getirin</string>
|
||||
<string name="reader_control_ltr_summary">Sağ kenara dokunulduğunda veya sağ tuşa basıldığında her zaman bir sonraki sayfaya geçilir</string>
|
||||
<string name="source_disabled">Kaynak devre dışı</string>
|
||||
</resources>
|
||||
@@ -15,4 +15,10 @@
|
||||
<plurals name="days_ago">
|
||||
<item quantity="other">%1$d ngày trước</item>
|
||||
</plurals>
|
||||
<plurals name="chapters_from_x">
|
||||
<item quantity="other">%1$d chương từ %2$d</item>
|
||||
</plurals>
|
||||
<plurals name="pages">
|
||||
<item quantity="other">Tổng %1$d trang</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -5,7 +5,7 @@
|
||||
<string name="favourites">喜欢</string>
|
||||
<string name="history">历史</string>
|
||||
<string name="error_occurred">发生了一个错误</string>
|
||||
<string name="network_error">未能连接到互联网</string>
|
||||
<string name="network_error">网络错误</string>
|
||||
<string name="chapters">章节</string>
|
||||
<string name="list">列表</string>
|
||||
<string name="data_restored_with_errors">数据被恢复了,但有错误</string>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<string name="favourites">Favourites</string>
|
||||
<string name="history">History</string>
|
||||
<string name="error_occurred">An error occurred</string>
|
||||
<string name="network_error">Could not connect to the Internet</string>
|
||||
<string name="network_error">Network error</string>
|
||||
<string name="details">Details</string>
|
||||
<string name="chapters">Chapters</string>
|
||||
<string name="list">List</string>
|
||||
@@ -398,4 +398,5 @@
|
||||
<string name="server_error">Server side error (%1$d). Please try again later</string>
|
||||
<string name="clear_new_chapters_counters">Also clear information about new chapters</string>
|
||||
<string name="compact">Compact</string>
|
||||
<string name="source_disabled">Source disabled</string>
|
||||
</resources>
|
||||
|
||||
28
app/src/main/res/xml/locales.xml
Normal file
28
app/src/main/res/xml/locales.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="ar" />
|
||||
<locale android:name="be" />
|
||||
<locale android:name="bn" />
|
||||
<locale android:name="de" />
|
||||
<locale android:name="el" />
|
||||
<locale android:name="es" />
|
||||
<locale android:name="fa" />
|
||||
<locale android:name="fi" />
|
||||
<locale android:name="fr" />
|
||||
<locale android:name="in" />
|
||||
<locale android:name="it" />
|
||||
<locale android:name="ja" />
|
||||
<locale android:name="nb-rNO" />
|
||||
<locale android:name="pt" />
|
||||
<locale android:name="pt-rBR" />
|
||||
<locale android:name="ru" />
|
||||
<locale android:name="si" />
|
||||
<locale android:name="sr" />
|
||||
<locale android:name="sv" />
|
||||
<locale android:name="tr" />
|
||||
<locale android:name="uk" />
|
||||
<locale android:name="zh-rCN" />
|
||||
<locale android:name="zh-rTW" />
|
||||
</locale-config>
|
||||
Reference in New Issue
Block a user