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>
<provider <provider
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider" android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider"
android:authorities="org.koitharu.kotatsu.favourites" android:authorities="${applicationId}.favourites"
android:exported="false" android:exported="false"
android:label="@string/favourites" android:label="@string/favourites"
android:syncable="true" /> android:syncable="true" />
<provider <provider
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider" android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider"
android:authorities="org.koitharu.kotatsu.history" android:authorities="${applicationId}.history"
android:exported="false" android:exported="false"
android:label="@string/history" android:label="@string/history"
android:syncable="true" /> android:syncable="true" />

View File

@@ -3,23 +3,28 @@ package org.koitharu.kotatsu.core.os
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback import android.net.ConnectivityManager.NetworkCallback
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.utils.MediatorStateFlow import org.koitharu.kotatsu.utils.MediatorStateFlow
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable import org.koitharu.kotatsu.utils.ext.isOnline
class NetworkState( class NetworkState(
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
) : MediatorStateFlow<Boolean>(connectivityManager.isNetworkAvailable) { ) : MediatorStateFlow<Boolean>(connectivityManager.isOnline()) {
private val callback = NetworkCallbackImpl() private val callback = NetworkCallbackImpl()
@Synchronized
override fun onActive() { override fun onActive() {
invalidate() invalidate()
val request = NetworkRequest.Builder().build() val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback) connectivityManager.registerNetworkCallback(request, callback)
} }
@Synchronized
override fun onInactive() { override fun onInactive() {
connectivityManager.unregisterNetworkCallback(callback) connectivityManager.unregisterNetworkCallback(callback)
} }
@@ -32,7 +37,7 @@ class NetworkState(
} }
private fun invalidate() { private fun invalidate() {
publishValue(connectivityManager.isNetworkAvailable) publishValue(connectivityManager.isOnline())
} }
private inner class NetworkCallbackImpl : NetworkCallback() { private inner class NetworkCallbackImpl : NetworkCallback() {

View File

@@ -107,7 +107,7 @@ class DownloadManager @Inject constructor(
withMangaLock(manga) { withMangaLock(manga) {
semaphore.withPermit { semaphore.withPermit {
outState.value = DownloadState.Preparing(startId, manga, null) 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) } checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
val tempFileName = "${manga.id}_$startId.tmp" val tempFileName = "${manga.id}_$startId.tmp"
var output: LocalMangaOutput? = null var output: LocalMangaOutput? = null

View File

@@ -36,20 +36,14 @@ sealed class LocalMangaOutput(
} }
private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? { private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? {
val name = manga.title.toFileNameSafe() val fileName = manga.title.toFileNameSafe()
val file = File(root, name) val dir = File(root, fileName)
return if (file.exists()) { val zip = File(root, "$fileName.cbz")
if (file.isDirectory) { return when {
LocalMangaDirOutput(file, manga) dir.isDirectory -> LocalMangaDirOutput(dir, manga)
} else { zip.isFile -> LocalMangaZipOutput(zip, manga)
LocalMangaZipOutput(file, manga) !onlyIfExists -> LocalMangaDirOutput(dir, manga)
} else -> null
} else {
if (onlyIfExists) {
null
} else {
LocalMangaDirOutput(file, manga)
}
} }
} }
} }

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.LocalStorageManager
import org.koitharu.kotatsu.local.data.TempFileFilter import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.local.data.input.LocalMangaInput 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.local.data.output.LocalMangaUtil
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -128,11 +129,21 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
override suspend fun getTags() = emptySet<MangaTag>() override suspend fun getTags() = emptySet<MangaTag>()
suspend fun getOutputDir(): File? { suspend fun getOutputDir(manga: Manga): File? {
return storageManager.getDefaultWriteableDir() 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() val dirs = storageManager.getWriteableDirs()
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
dirs.flatMap { dir -> dirs.flatMap { dir ->
@@ -141,6 +152,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
file.delete() file.delete()
} }
} }
return true
} }
suspend fun lockManga(id: Long) { 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.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings 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.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider 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.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.IOException import java.io.IOException
import java.util.LinkedList import java.util.LinkedList
@@ -85,7 +83,6 @@ class LocalListViewModel @Inject constructor(
init { init {
onRefresh() onRefresh()
cleanup()
watchDirectories() 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() { private fun watchDirectories() {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
repository.watchReadableDirs() 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.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity 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.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -321,6 +322,7 @@ class MainActivity :
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext) TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext) SuggestionsWorker.setup(applicationContext)
LocalStorageCleanupWorker.enqueue(applicationContext)
} }
withResumed { withResumed {
MangaPrefetchService.prefetchLast(this@MainActivity) MangaPrefetchService.prefetchLast(this@MainActivity)

View File

@@ -16,9 +16,12 @@ fun sourceLocaleAD(
listener.onItemCheckedChanged(item, isChecked) listener.onItemCheckedChanged(item, isChecked)
} }
bind { bind { payloads ->
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages) binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
binding.textViewDescription.textAndVisible = item.summary binding.textViewDescription.textAndVisible = item.summary
binding.switchToggle.isChecked = item.isChecked binding.switchToggle.isChecked = item.isChecked
if (payloads.isEmpty()) {
binding.switchToggle.jumpDrawablesToCurrentState()
}
} }
} }

View File

@@ -40,10 +40,12 @@ fun shelfSectionAD(
binding.switchToggle.setOnCheckedChangeListener(eventListener) binding.switchToggle.setOnCheckedChangeListener(eventListener)
binding.imageViewHandle.setOnTouchListener(eventListener) binding.imageViewHandle.setOnTouchListener(eventListener)
bind { bind { payloads ->
binding.textViewTitle.setText(item.section.titleResId) binding.textViewTitle.setText(item.section.titleResId)
binding.switchToggle.isChecked = item.isChecked binding.switchToggle.isChecked = item.isChecked
binding.switchToggle.jumpDrawablesToCurrentState() if (payloads.isEmpty()) {
binding.switchToggle.jumpDrawablesToCurrentState()
}
} }
} }
@@ -61,10 +63,12 @@ fun shelfCategoryAD(
end = binding.root.paddingStart, end = binding.root.paddingStart,
) )
bind { bind { payloads ->
binding.root.text = item.title binding.root.text = item.title
binding.root.isChecked = item.isChecked 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 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.content.res.Resources
import android.database.SQLException import android.database.SQLException
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
@@ -49,17 +48,6 @@ import kotlin.math.roundToLong
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
get() = getSystemService(ACTIVITY_SERVICE) as? 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) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { 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"?> <?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:accountType="@string/account_type_sync"
android:allowParallelSyncs="false" android:allowParallelSyncs="false"
android:contentAuthority="org.koitharu.kotatsu.favourites" android:contentAuthority="${applicationId}.favourites"
android:isAlwaysSyncable="true" android:isAlwaysSyncable="true"
android:supportsUploading="true" android:supportsUploading="true"
android:userVisible="true" /> android:userVisible="true" />

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?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:accountType="@string/account_type_sync"
android:allowParallelSyncs="false" android:allowParallelSyncs="false"
android:contentAuthority="org.koitharu.kotatsu.history" android:contentAuthority="${applicationId}.history"
android:isAlwaysSyncable="true" android:isAlwaysSyncable="true"
android:supportsUploading="true" android:supportsUploading="true"
android:userVisible="true" /> android:userVisible="true" />