Fix downloading manga into existing cbz
This commit is contained in:
4
app/src/debug/res/values/constants.xml
Normal file
4
app/src/debug/res/values/constants.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_type_sync" translatable="false">org.kotatsu.debug.sync</string>
|
||||
</resources>
|
||||
@@ -227,13 +227,13 @@
|
||||
</provider>
|
||||
<provider
|
||||
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider"
|
||||
android:authorities="org.koitharu.kotatsu.favourites"
|
||||
android:authorities="${applicationId}.favourites"
|
||||
android:exported="false"
|
||||
android:label="@string/favourites"
|
||||
android:syncable="true" />
|
||||
<provider
|
||||
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider"
|
||||
android:authorities="org.koitharu.kotatsu.history"
|
||||
android:authorities="${applicationId}.history"
|
||||
android:exported="false"
|
||||
android:label="@string/history"
|
||||
android:syncable="true" />
|
||||
|
||||
@@ -3,23 +3,28 @@ package org.koitharu.kotatsu.core.os
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.koitharu.kotatsu.utils.MediatorStateFlow
|
||||
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||
import org.koitharu.kotatsu.utils.ext.isOnline
|
||||
|
||||
class NetworkState(
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
) : MediatorStateFlow<Boolean>(connectivityManager.isNetworkAvailable) {
|
||||
) : MediatorStateFlow<Boolean>(connectivityManager.isOnline()) {
|
||||
|
||||
private val callback = NetworkCallbackImpl()
|
||||
|
||||
@Synchronized
|
||||
override fun onActive() {
|
||||
invalidate()
|
||||
val request = NetworkRequest.Builder().build()
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onInactive() {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
@@ -32,7 +37,7 @@ class NetworkState(
|
||||
}
|
||||
|
||||
private fun invalidate() {
|
||||
publishValue(connectivityManager.isNetworkAvailable)
|
||||
publishValue(connectivityManager.isOnline())
|
||||
}
|
||||
|
||||
private inner class NetworkCallbackImpl : NetworkCallback() {
|
||||
|
||||
@@ -107,7 +107,7 @@ class DownloadManager @Inject constructor(
|
||||
withMangaLock(manga) {
|
||||
semaphore.withPermit {
|
||||
outState.value = DownloadState.Preparing(startId, manga, null)
|
||||
val destination = localMangaRepository.getOutputDir()
|
||||
val destination = localMangaRepository.getOutputDir(manga)
|
||||
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
|
||||
val tempFileName = "${manga.id}_$startId.tmp"
|
||||
var output: LocalMangaOutput? = null
|
||||
|
||||
@@ -36,20 +36,14 @@ sealed class LocalMangaOutput(
|
||||
}
|
||||
|
||||
private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? {
|
||||
val name = manga.title.toFileNameSafe()
|
||||
val file = File(root, name)
|
||||
return if (file.exists()) {
|
||||
if (file.isDirectory) {
|
||||
LocalMangaDirOutput(file, manga)
|
||||
} else {
|
||||
LocalMangaZipOutput(file, manga)
|
||||
}
|
||||
} else {
|
||||
if (onlyIfExists) {
|
||||
null
|
||||
} else {
|
||||
LocalMangaDirOutput(file, manga)
|
||||
}
|
||||
val fileName = manga.title.toFileNameSafe()
|
||||
val dir = File(root, fileName)
|
||||
val zip = File(root, "$fileName.cbz")
|
||||
return when {
|
||||
dir.isDirectory -> LocalMangaDirOutput(dir, manga)
|
||||
zip.isFile -> LocalMangaZipOutput(zip, manga)
|
||||
!onlyIfExists -> LocalMangaDirOutput(dir, manga)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.data.TempFileFilter
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
||||
import org.koitharu.kotatsu.local.data.output.LocalMangaUtil
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
@@ -128,11 +129,21 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
||||
|
||||
override suspend fun getTags() = emptySet<MangaTag>()
|
||||
|
||||
suspend fun getOutputDir(): File? {
|
||||
return storageManager.getDefaultWriteableDir()
|
||||
suspend fun getOutputDir(manga: Manga): File? {
|
||||
val defaultDir = storageManager.getDefaultWriteableDir()
|
||||
if (defaultDir != null && LocalMangaOutput.get(defaultDir, manga) != null) {
|
||||
return defaultDir
|
||||
}
|
||||
return storageManager.getWriteableDirs()
|
||||
.firstOrNull {
|
||||
LocalMangaOutput.get(it, manga) != null
|
||||
} ?: defaultDir
|
||||
}
|
||||
|
||||
suspend fun cleanup() {
|
||||
suspend fun cleanup(): Boolean {
|
||||
if (locks.isNotEmpty()) {
|
||||
return false
|
||||
}
|
||||
val dirs = storageManager.getWriteableDirs()
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
dirs.flatMap { dir ->
|
||||
@@ -141,6 +152,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun lockManga(id: Long) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
@@ -35,7 +34,6 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
@@ -85,7 +83,6 @@ class LocalListViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
cleanup()
|
||||
watchDirectories()
|
||||
}
|
||||
|
||||
@@ -140,18 +137,6 @@ class LocalListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
if (!DownloadService.isRunning && !LocalChaptersRemoveService.isRunning) {
|
||||
viewModelScope.launch {
|
||||
runCatchingCancellable {
|
||||
repository.cleanup()
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchDirectories() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
repository.watchReadableDirs()
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.local.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@HiltWorker
|
||||
class LocalStorageCleanupWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return if (localMangaRepository.cleanup()) {
|
||||
Result.success()
|
||||
} else {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "cleanup"
|
||||
|
||||
fun enqueue(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<ImportWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -321,6 +322,7 @@ class MainActivity :
|
||||
withContext(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
LocalStorageCleanupWorker.enqueue(applicationContext)
|
||||
}
|
||||
withResumed {
|
||||
MangaPrefetchService.prefetchLast(this@MainActivity)
|
||||
|
||||
@@ -16,9 +16,12 @@ fun sourceLocaleAD(
|
||||
listener.onItemCheckedChanged(item, isChecked)
|
||||
}
|
||||
|
||||
bind {
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
|
||||
binding.textViewDescription.textAndVisible = item.summary
|
||||
binding.switchToggle.isChecked = item.isChecked
|
||||
if (payloads.isEmpty()) {
|
||||
binding.switchToggle.jumpDrawablesToCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,12 @@ fun shelfSectionAD(
|
||||
binding.switchToggle.setOnCheckedChangeListener(eventListener)
|
||||
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||
|
||||
bind {
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.setText(item.section.titleResId)
|
||||
binding.switchToggle.isChecked = item.isChecked
|
||||
binding.switchToggle.jumpDrawablesToCurrentState()
|
||||
if (payloads.isEmpty()) {
|
||||
binding.switchToggle.jumpDrawablesToCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +63,12 @@ fun shelfCategoryAD(
|
||||
end = binding.root.paddingStart,
|
||||
)
|
||||
|
||||
bind {
|
||||
bind { payloads ->
|
||||
binding.root.text = item.title
|
||||
binding.root.isChecked = item.isChecked
|
||||
binding.root.jumpDrawablesToCurrentState()
|
||||
if (payloads.isEmpty()) {
|
||||
binding.root.jumpDrawablesToCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ abstract class MediatorStateFlow<T>(initialValue: T) : StateFlow<T> {
|
||||
delegate.value = v
|
||||
}
|
||||
|
||||
abstract fun onActive()
|
||||
protected abstract fun onActive()
|
||||
|
||||
abstract fun onInactive()
|
||||
protected abstract fun onInactive()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import android.content.pm.ResolveInfo
|
||||
import android.content.res.Resources
|
||||
import android.database.SQLException
|
||||
import android.graphics.Color
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
@@ -49,17 +48,6 @@ import kotlin.math.roundToLong
|
||||
val Context.activityManager: ActivityManager?
|
||||
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
|
||||
|
||||
val Context.connectivityManager: ConnectivityManager
|
||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
val ConnectivityManager.isNetworkAvailable: Boolean
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
activeNetwork != null
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activeNetworkInfo?.isConnectedOrConnecting == true
|
||||
}
|
||||
|
||||
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
||||
|
||||
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
|
||||
|
||||
24
app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt
Normal file
24
app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
|
||||
val Context.connectivityManager: ConnectivityManager
|
||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
fun ConnectivityManager.isOnline(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
activeNetwork?.let { isOnline(it) } ?: false
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activeNetworkInfo?.isConnected == true
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConnectivityManager.isOnline(network: Network): Boolean {
|
||||
val capabilities = getNetworkCapabilities(network)
|
||||
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<sync-adapter
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@string/account_type_sync"
|
||||
android:allowParallelSyncs="false"
|
||||
android:contentAuthority="org.koitharu.kotatsu.favourites"
|
||||
android:contentAuthority="${applicationId}.favourites"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:supportsUploading="true"
|
||||
android:userVisible="true" />
|
||||
android:userVisible="true" />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<sync-adapter
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@string/account_type_sync"
|
||||
android:allowParallelSyncs="false"
|
||||
android:contentAuthority="org.koitharu.kotatsu.history"
|
||||
android:contentAuthority="${applicationId}.history"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:supportsUploading="true"
|
||||
android:userVisible="true" />
|
||||
android:userVisible="true" />
|
||||
|
||||
Reference in New Issue
Block a user