Option to automatically download new chapters (close #425, close #602, close #955)

This commit is contained in:
Koitharu
2024-09-14 12:12:55 +03:00
parent 98bd79f0be
commit 9b24c507c5
21 changed files with 143 additions and 27 deletions

View File

@@ -160,6 +160,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isTrackerNsfwDisabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NO_NSFW, false)
val trackerDownloadStrategy: TrackerDownloadStrategy
get() = prefs.getEnumValue(KEY_TRACKER_DOWNLOAD, TrackerDownloadStrategy.DISABLED)
var notificationSound: Uri
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
?: Settings.System.DEFAULT_NOTIFICATION_URI
@@ -600,6 +603,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_TRACK_WARNING = "track_warning"
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
const val KEY_TRACKER_NO_NSFW = "tracker_no_nsfw"
const val KEY_TRACKER_DOWNLOAD = "tracker_download"
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"

View File

@@ -1,11 +1,13 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import com.google.android.material.color.DynamicColors
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.find
@Keep
enum class ColorScheme(
@StyleRes val styleResId: Int,
@StringRes val titleResId: Int,

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class DownloadFormat {
AUTOMATIC,

View File

@@ -1,6 +1,9 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class ListMode {
LIST, DETAILED_LIST, GRID;
}
}

View File

@@ -2,9 +2,11 @@ package org.koitharu.kotatsu.core.prefs
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.Keep
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
@Keep
enum class NavItem(
@IdRes val id: Int,
@StringRes val title: Int,

View File

@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.core.prefs
import android.net.ConnectivityManager
import androidx.annotation.Keep
@Keep
enum class NetworkPolicy(
private val key: Int,
) {

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class ProgressIndicatorMode {
NONE, PERCENT_READ, PERCENT_LEFT, CHAPTERS_READ, CHAPTERS_LEFT;

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class ReaderAnimation {
// Do not rename this

View File

@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.view.ContextThemeWrapper
import androidx.annotation.Keep
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import org.koitharu.kotatsu.core.util.ext.getThemeDrawable
import com.google.android.material.R as materialR
@Keep
enum class ReaderBackground {
DEFAULT, LIGHT, DARK, WHITE, BLACK;

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class ReaderMode(val id: Int) {
STANDARD(1),

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class ScreenshotsPolicy {
// Do not rename this

View File

@@ -1,8 +1,10 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
@Keep
enum class SearchSuggestionType(
@StringRes val titleResId: Int,
) {

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class TrackerDownloadStrategy {
DISABLED, DOWNLOADED;
}

View File

@@ -166,8 +166,9 @@ abstract class ChaptersPagesViewModel(
fun download(chaptersIds: Set<Long>?) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(
requireManga(),
chaptersIds,
manga = requireManga(),
chaptersIds = chaptersIds,
isSilent = false,
)
onDownloadStarted.call(Unit)
}

View File

@@ -37,7 +37,8 @@ import org.koitharu.kotatsu.search.ui.MangaListActivity
import java.util.UUID
import com.google.android.material.R as materialR
private const val CHANNEL_ID = "download"
private const val CHANNEL_ID_DEFAULT = "download"
private const val CHANNEL_ID_SILENT = "download_bg"
private const val GROUP_ID = "downloads"
class DownloadNotificationFactory @AssistedInject constructor(
@@ -45,10 +46,11 @@ class DownloadNotificationFactory @AssistedInject constructor(
private val workManager: WorkManager,
private val coil: ImageLoader,
@Assisted private val uuid: UUID,
@Assisted val isSilent: Boolean,
) {
private val covers = HashMap<Manga, Drawable>()
private val builder = NotificationCompat.Builder(context, CHANNEL_ID)
private val builder = NotificationCompat.Builder(context, if (isSilent) CHANNEL_ID_SILENT else CHANNEL_ID_DEFAULT)
private val mutex = Mutex()
private val coverWidth = context.resources.getDimensionPixelSize(
@@ -106,14 +108,18 @@ class DownloadNotificationFactory @AssistedInject constructor(
}
init {
createChannel()
createChannels()
builder.setOnlyAlertOnce(true)
builder.setDefaults(0)
builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
builder.foregroundServiceBehavior = if (isSilent) {
NotificationCompat.FOREGROUND_SERVICE_DEFERRED
} else {
NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
}
builder.setSilent(true)
builder.setGroup(GROUP_ID)
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
builder.priority = if (isSilent) NotificationCompat.PRIORITY_MIN else NotificationCompat.PRIORITY_DEFAULT
}
suspend fun create(state: DownloadState?): Notification = mutex.withLock {
@@ -283,20 +289,30 @@ class DownloadNotificationFactory @AssistedInject constructor(
}.getOrNull()
}
private fun createChannel() {
private fun createChannels() {
val manager = NotificationManagerCompat.from(context)
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.downloads))
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setSound(null, null)
.build()
manager.createNotificationChannel(channel)
manager.createNotificationChannel(
NotificationChannelCompat.Builder(CHANNEL_ID_DEFAULT, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.downloads))
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setSound(null, null)
.build(),
)
manager.createNotificationChannel(
NotificationChannelCompat.Builder(CHANNEL_ID_SILENT, NotificationManagerCompat.IMPORTANCE_MIN)
.setName(context.getString(R.string.downloads_background))
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setSound(null, null)
.setShowBadge(false)
.build(),
)
}
@AssistedFactory
interface Factory {
fun create(uuid: UUID): DownloadNotificationFactory
fun create(uuid: UUID, isSilent: Boolean): DownloadNotificationFactory
}
}

View File

@@ -104,7 +104,10 @@ class DownloadWorker @AssistedInject constructor(
notificationFactoryFactory: DownloadNotificationFactory.Factory,
) : CoroutineWorker(appContext, params) {
private val notificationFactory = notificationFactoryFactory.create(params.id)
private val notificationFactory = notificationFactoryFactory.create(
uuid = params.id,
isSilent = params.inputData.getBoolean(IS_SILENT, false),
)
private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val slowdownDispatcher = DownloadSlowdownDispatcher(mangaRepositoryFactory, SLOWDOWN_DELAY)
@@ -120,8 +123,7 @@ class DownloadWorker @AssistedInject constructor(
setForeground(getForegroundInfo())
val mangaId = inputData.getLong(MANGA_ID, 0L)
val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure()
lastPublishedState = DownloadState(manga, isIndeterminate = true)
publishState(DownloadState(manga, isIndeterminate = true))
publishState(DownloadState(manga = manga, isIndeterminate = true).also { lastPublishedState = it })
val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
val downloadedIds = getDoneChapters(manga)
return try {
@@ -380,7 +382,9 @@ class DownloadWorker @AssistedInject constructor(
}
val notification = notificationFactory.create(state)
if (state.isFinalState) {
notificationManager.notify(id.toString(), id.hashCode(), notification)
if (!notificationFactory.isSilent) {
notificationManager.notify(id.toString(), id.hashCode(), notification)
}
} else if (notificationThrottler.throttle()) {
notificationManager.notify(id.hashCode(), notification)
} else {
@@ -426,10 +430,11 @@ class DownloadWorker @AssistedInject constructor(
private val settings: AppSettings,
) {
suspend fun schedule(manga: Manga, chaptersIds: Collection<Long>?) {
suspend fun schedule(manga: Manga, chaptersIds: Collection<Long>?, isSilent: Boolean) {
dataRepository.storeManga(manga)
val data = Data.Builder()
.putLong(MANGA_ID, manga.id)
.putBoolean(IS_SILENT, isSilent)
if (!chaptersIds.isNullOrEmpty()) {
data.putLongArray(CHAPTERS_IDS, chaptersIds.toLongArray())
}
@@ -549,6 +554,7 @@ class DownloadWorker @AssistedInject constructor(
const val SLOWDOWN_DELAY = 200L
const val MANGA_ID = "manga_id"
const val CHAPTERS_IDS = "chapters"
const val IS_SILENT = "silent"
const val TAG = "download"
}
}

View File

@@ -11,13 +11,17 @@ import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.fragment.app.viewModels
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.TrackerDownloadStrategy
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
import org.koitharu.kotatsu.settings.utils.DozeHelper
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
@@ -50,6 +54,10 @@ class TrackerSettingsFragment :
}
}
}
findPreference<ListPreference>(AppSettings.KEY_TRACKER_DOWNLOAD)?.run {
entryValues = TrackerDownloadStrategy.entries.names()
setDefaultValueCompat(TrackerDownloadStrategy.DISABLED.name)
}
dozeHelper.updatePreference()
updateCategoriesEnabled()
}

View File

@@ -25,6 +25,7 @@ import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import androidx.work.await
import dagger.Lazy
import dagger.Reusable
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -47,10 +48,14 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.TrackerDownloadStrategy
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.onEachIndexed
import org.koitharu.kotatsu.core.util.ext.trySetForeground
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toIntUp
import org.koitharu.kotatsu.settings.SettingsActivity
@@ -76,6 +81,8 @@ class TrackWorker @AssistedInject constructor(
private val checkNewChaptersUseCase: CheckNewChaptersUseCase,
private val workManager: WorkManager,
@TrackerLogger private val logger: FileLogger,
private val localRepositoryLazy: Lazy<LocalMangaRepository>,
private val downloadSchedulerLazy: Lazy<DownloadWorker.Scheduler>,
) : CoroutineWorker(context, workerParams) {
private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) }
@@ -144,12 +151,16 @@ class TrackWorker @AssistedInject constructor(
if (applicationContext.checkNotificationPermission(WORKER_CHANNEL_ID)) {
notificationManager.notify(WORKER_NOTIFICATION_ID, createWorkerNotification(tracks.size, index + 1))
}
if (it is MangaUpdates.Failure) {
val e = it.error
logger.log("checkUpdatesAsync", e)
if (e is CloudFlareProtectedException) {
CaptchaNotifier(applicationContext).notify(e)
when (it) {
is MangaUpdates.Failure -> {
val e = it.error
logger.log("checkUpdatesAsync", e)
if (e is CloudFlareProtectedException) {
CaptchaNotifier(applicationContext).notify(e)
}
}
is MangaUpdates.Success -> processDownload(it)
}
}.mapNotNull {
when (it) {
@@ -237,6 +248,25 @@ class TrackWorker @AssistedInject constructor(
}
}.build()
private suspend fun processDownload(mangaUpdates: MangaUpdates.Success) {
if (!mangaUpdates.isValid || mangaUpdates.newChapters.isEmpty()) {
return
}
when (settings.trackerDownloadStrategy) {
TrackerDownloadStrategy.DISABLED -> Unit
TrackerDownloadStrategy.DOWNLOADED -> {
val localManga = localRepositoryLazy.get().findSavedManga(mangaUpdates.manga)
if (localManga != null) {
downloadSchedulerLazy.get().schedule(
manga = mangaUpdates.manga,
chaptersIds = mangaUpdates.newChapters.mapToSet { it.id },
isSilent = true,
)
}
}
}
}
@Reusable
class Scheduler @Inject constructor(
private val workManager: WorkManager,

View File

@@ -109,4 +109,8 @@
<item>@string/chapters_read</item>
<item>@string/chapters_left</item>
</string-array>
<string-array name="tracker_download_strategies" translatable="false">
<item>@string/never</item>
<item>@string/manga_with_downloaded_chapters</item>
</string-array>
</resources>

View File

@@ -698,4 +698,7 @@
<string name="scrobbler_auth_intro">Sign in to set up integration with %s. This will allow you to track your manga reading progress and status</string>
<string name="unstable_feature">Unstable feature</string>
<string name="unstable_feature_summary">This function is experimental. Please make sure you have a backup to avoid data loss</string>
<string name="downloads_background">Background downloads</string>
<string name="download_new_chapters">Download new chapters</string>
<string name="manga_with_downloaded_chapters">Manga with downloaded chapters</string>
</resources>

View File

@@ -52,6 +52,13 @@
android:summary="@string/disable_nsfw_notifications_summary"
android:title="@string/disable_nsfw_notifications" />
<ListPreference
android:dependency="tracker_enabled"
android:entries="@array/tracker_download_strategies"
android:key="tracker_download"
android:title="@string/download_new_chapters"
app:useSimpleSummaryProvider="true" />
<Preference
android:dependency="tracker_enabled"
android:key="ignore_dose"