Fix downloading manga into existing cbz

This commit is contained in:
Koitharu
2023-04-13 19:38:17 +03:00
parent f32ff00b68
commit 277d575485
16 changed files with 135 additions and 64 deletions

View 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>

View File

@@ -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" />

View File

@@ -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() {

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}

View File

@@ -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 {

View 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)
}

View File

@@ -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" />

View File

@@ -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" />