diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 22bad7dd9..444c9619a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -111,6 +111,7 @@ { val stateFlow = MutableStateFlow( - DownloadState.Queued(startId = startId, manga = manga, cover = null) + DownloadState.Queued(startId = startId, manga = manga, cover = null), ) val pausingHandle = PausingHandle() val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId) @@ -100,7 +100,7 @@ class DownloadManager( data.chapters } else { data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) } - } + }, ) { "Chapters list must not be null" } check(chapters.isNotEmpty()) { "Chapters list must not be empty" } check(chaptersIdsSet.isNullOrEmpty()) { @@ -118,7 +118,7 @@ class DownloadManager( chapter = chapter, file = file, pageNumber = pageIndex, - ext = MimeTypeMap.getFileExtensionFromUrl(url) + ext = MimeTypeMap.getFileExtensionFromUrl(url), ) } outState.value = DownloadState.Progress( @@ -128,7 +128,7 @@ class DownloadManager( totalChapters = chapters.size, currentChapter = chapterIndex, totalPages = pages.size, - currentPage = pageIndex + currentPage = pageIndex, ) if (settings.isDownloadsSlowdownEnabled) { @@ -209,7 +209,7 @@ class DownloadManager( manga = prevValue.manga, cover = prevValue.cover, error = throwable, - canRetry = false + canRetry = false, ) } @@ -220,7 +220,7 @@ class DownloadManager( .referer(manga.publicUrl) .size(coverWidth, coverHeight) .scale(Scale.FILL) - .build() + .build(), ).drawable }.getOrNull() @@ -240,7 +240,7 @@ class DownloadManager( okHttp = okHttp, cache = cache, localMangaRepository = localMangaRepository, - settings = settings + settings = settings, ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt index d7f86abfb..343507b94 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt @@ -108,34 +108,6 @@ sealed interface DownloadState { } } - @Deprecated("TODO: remove") - class WaitingForNetwork( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as WaitingForNetwork - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - return result - } - } - class Done( override val startId: Int, override val manga: Manga, diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt index 476f6efb6..c91517b42 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt @@ -18,9 +18,8 @@ fun downloadItemAD( scope: CoroutineScope, coil: ImageLoader, ) = adapterDelegateViewBinding, ProgressJob, ItemDownloadBinding>( - { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }, ) { - var job: Job? = null val percentPattern = context.resources.getString(R.string.percent_string_pattern) @@ -91,13 +90,6 @@ fun downloadItemAD( binding.textViewPercent.isVisible = false binding.textViewDetails.isVisible = false } - is DownloadState.WaitingForNetwork -> { - binding.textViewStatus.setText(R.string.waiting_for_network) - binding.progressBar.isIndeterminate = false - binding.progressBar.isVisible = false - binding.textViewPercent.isVisible = false - binding.textViewDetails.isVisible = false - } } }.launchIn(scope) } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index 399340aa3..12efd0d34 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -7,163 +7,282 @@ import android.app.PendingIntent import android.content.Context import android.os.Build import android.text.format.DateUtils +import android.util.SparseArray import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap -import com.google.android.material.R as materialR +import androidx.core.text.HtmlCompat +import androidx.core.text.htmlEncode +import androidx.core.text.parseAsHtml +import androidx.core.util.forEach +import androidx.core.util.size import org.koitharu.kotatsu.R import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import com.google.android.material.R as materialR -class DownloadNotification(private val context: Context, startId: Int) { +class DownloadNotification(private val context: Context) { + + private val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val states = SparseArray() + private val groupBuilder = NotificationCompat.Builder(context, CHANNEL_ID) - private val builder = NotificationCompat.Builder(context, CHANNEL_ID) - private val cancelAction = NotificationCompat.Action( - materialR.drawable.material_ic_clear_black_24dp, - context.getString(android.R.string.cancel), - PendingIntent.getBroadcast( - context, - startId * 2, - DownloadService.getCancelIntent(startId), - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - ) - private val retryAction = NotificationCompat.Action( - R.drawable.ic_restart_black, - context.getString(R.string.try_again), - PendingIntent.getBroadcast( - context, - startId * 2 + 1, - DownloadService.getResumeIntent(startId), - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - ) private val listIntent = PendingIntent.getActivity( context, REQUEST_LIST, DownloadsActivity.newIntent(context), - PendingIntentCompat.FLAG_IMMUTABLE + PendingIntentCompat.FLAG_IMMUTABLE, ) init { - builder.setOnlyAlertOnce(true) - builder.setDefaults(0) - builder.color = ContextCompat.getColor(context, R.color.blue_primary) - builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE - builder.setSilent(true) + groupBuilder.setOnlyAlertOnce(true) + groupBuilder.setDefaults(0) + groupBuilder.color = ContextCompat.getColor(context, R.color.blue_primary) + groupBuilder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE + groupBuilder.setSilent(true) + groupBuilder.setGroup(GROUP_ID) + groupBuilder.setContentIntent(listIntent) + groupBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + groupBuilder.setGroupSummary(true) + groupBuilder.setContentTitle(context.getString(R.string.downloading_manga)) } - fun create(state: DownloadState, timeLeft: Long): Notification { - builder.setContentTitle(state.manga.title) - builder.setContentText(context.getString(R.string.manga_downloading_)) - builder.setProgress(1, 0, true) - builder.setSmallIcon(android.R.drawable.stat_sys_download) - builder.setContentIntent(listIntent) - builder.setStyle(null) - builder.setLargeIcon(state.cover?.toBitmap()) - builder.clearActions() - builder.setVisibility( + fun buildGroupNotification(): Notification { + val style = NotificationCompat.InboxStyle(groupBuilder) + var progress = 0f + var isAllDone = true + groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + states.forEach { _, state -> if (state.manga.isNsfw) { - NotificationCompat.VISIBILITY_PRIVATE - } else { - NotificationCompat.VISIBILITY_PUBLIC + groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE) } - ) - when (state) { - is DownloadState.Cancelled -> { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.cancelling_)) - builder.setContentIntent(null) - builder.setStyle(null) - builder.setOngoing(true) - } - is DownloadState.Done -> { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.download_complete)) - builder.setContentIntent(createMangaIntent(context, state.localManga)) - builder.setAutoCancel(true) - builder.setSmallIcon(android.R.drawable.stat_sys_download_done) - builder.setCategory(null) - builder.setStyle(null) - builder.setOngoing(false) - } - is DownloadState.Error -> { - val message = state.error.getDisplayMessage(context.resources) - builder.setProgress(0, 0, false) - builder.setSmallIcon(android.R.drawable.stat_notify_error) - builder.setSubText(context.getString(R.string.error)) - builder.setContentText(message) - builder.setAutoCancel(!state.canRetry) - builder.setOngoing(state.canRetry) - builder.setCategory(NotificationCompat.CATEGORY_ERROR) - builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) - if (state.canRetry) { - builder.addAction(cancelAction) - builder.addAction(retryAction) + val summary = when (state) { + is DownloadState.Cancelled -> { + progress++ + context.getString(R.string.cancelling_) + } + is DownloadState.Done -> { + progress++ + context.getString(R.string.completed) + } + is DownloadState.Error -> { + isAllDone = false + context.getString(R.string.error) + } + is DownloadState.PostProcessing -> { + progress++ + isAllDone = false + context.getString(R.string.processing_) + } + is DownloadState.Preparing -> { + isAllDone = false + context.getString(R.string.preparing_) + } + is DownloadState.Progress -> { + isAllDone = false + progress += state.percent + context.getString(R.string.percent_string_pattern, (state.percent * 100).format()) + } + is DownloadState.Queued -> { + isAllDone = false + context.getString(R.string.queued) } } - is DownloadState.PostProcessing -> { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.processing_)) - builder.setStyle(null) - builder.setOngoing(true) - } - is DownloadState.Queued -> { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.queued)) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - } - is DownloadState.Preparing -> { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.preparing_)) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - } - is DownloadState.Progress -> { - builder.setProgress(state.max, state.progress, false) - if (timeLeft > 0L) { - val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS) - builder.setContentText(eta) - } else { - val percent = (state.percent * 100).format() - builder.setContentText(context.getString(R.string.percent_string_pattern, percent)) - } - builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - } - is DownloadState.WaitingForNetwork -> { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.waiting_for_network)) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - } + style.addLine( + context.getString( + R.string.download_summary_pattern, + state.manga.title.ellipsize(10).htmlEncode(), + summary.htmlEncode(), + ).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY), + ) } - return builder.build() + progress /= states.size.toFloat() + style.setBigContentTitle(context.getString(R.string.downloading_manga)) + groupBuilder.setContentText(context.resources.getQuantityString(R.plurals.items, states.size, states.size())) + groupBuilder.setNumber(states.size) + groupBuilder.setSmallIcon( + if (isAllDone) android.R.drawable.stat_sys_download_done else android.R.drawable.stat_sys_download, + ) + when (progress) { + 1f -> groupBuilder.setProgress(0, 0, false) + 0f -> groupBuilder.setProgress(1, 0, true) + else -> groupBuilder.setProgress(100, (progress * 100f).toInt(), progress == 0f) + } + return groupBuilder.build() + } + + fun dismiss() { + manager.cancel(ID_GROUP) + } + + fun newItem(startId: Int) = Item(startId) + + inner class Item( + private val startId: Int, + ) { + + private val builder = NotificationCompat.Builder(context, CHANNEL_ID) + private val cancelAction = NotificationCompat.Action( + materialR.drawable.material_ic_clear_black_24dp, + context.getString(android.R.string.cancel), + PendingIntent.getBroadcast( + context, + startId * 2, + DownloadService.getCancelIntent(startId), + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE, + ), + ) + private val retryAction = NotificationCompat.Action( + R.drawable.ic_restart_black, + context.getString(R.string.try_again), + PendingIntent.getBroadcast( + context, + startId * 2 + 1, + DownloadService.getResumeIntent(startId), + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE, + ), + ) + + init { + builder.setOnlyAlertOnce(true) + builder.setDefaults(0) + builder.color = ContextCompat.getColor(context, R.color.blue_primary) + builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE + builder.setSilent(true) + builder.setGroup(GROUP_ID) + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + } + + fun notify(state: DownloadState, timeLeft: Long) { + builder.setContentTitle(state.manga.title) + builder.setContentText(context.getString(R.string.manga_downloading_)) + builder.setProgress(1, 0, true) + builder.setSmallIcon(android.R.drawable.stat_sys_download) + builder.setContentIntent(listIntent) + builder.setStyle(null) + builder.setLargeIcon(state.cover?.toBitmap()) + builder.clearActions() + builder.setVisibility( + if (state.manga.isNsfw) { + NotificationCompat.VISIBILITY_PRIVATE + } else { + NotificationCompat.VISIBILITY_PUBLIC + }, + ) + when (state) { + is DownloadState.Cancelled -> { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.cancelling_)) + builder.setContentIntent(null) + builder.setStyle(null) + builder.setOngoing(true) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + is DownloadState.Done -> { + builder.setProgress(0, 0, false) + builder.setContentText(context.getString(R.string.download_complete)) + builder.setContentIntent(createMangaIntent(context, state.localManga)) + builder.setAutoCancel(true) + builder.setSmallIcon(android.R.drawable.stat_sys_download_done) + builder.setCategory(null) + builder.setStyle(null) + builder.setOngoing(false) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + is DownloadState.Error -> { + val message = state.error.getDisplayMessage(context.resources) + builder.setProgress(0, 0, false) + builder.setSmallIcon(android.R.drawable.stat_notify_error) + builder.setSubText(context.getString(R.string.error)) + builder.setContentText(message) + builder.setAutoCancel(!state.canRetry) + builder.setOngoing(state.canRetry) + builder.setCategory(NotificationCompat.CATEGORY_ERROR) + builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) + if (state.canRetry) { + builder.addAction(cancelAction) + builder.addAction(retryAction) + } + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + is DownloadState.PostProcessing -> { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.processing_)) + builder.setStyle(null) + builder.setOngoing(true) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + is DownloadState.Queued -> { + builder.setProgress(0, 0, false) + builder.setContentText(context.getString(R.string.queued)) + builder.setStyle(null) + builder.setOngoing(true) + builder.addAction(cancelAction) + builder.priority = NotificationCompat.PRIORITY_LOW + } + is DownloadState.Preparing -> { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.preparing_)) + builder.setStyle(null) + builder.setOngoing(true) + builder.addAction(cancelAction) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + is DownloadState.Progress -> { + builder.setProgress(state.max, state.progress, false) + if (timeLeft > 0L) { + val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS) + builder.setContentText(eta) + } else { + val percent = (state.percent * 100).format() + builder.setContentText(context.getString(R.string.percent_string_pattern, percent)) + } + builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) + builder.setStyle(null) + builder.setOngoing(true) + builder.addAction(cancelAction) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + } + val notification = builder.build() + states.append(startId, state) + updateGroupNotification() + manager.notify(TAG, startId, notification) + } + + fun dismiss() { + manager.cancel(TAG, startId) + states.remove(startId) + updateGroupNotification() + } + } + + private fun updateGroupNotification() { + val notification = buildGroupNotification() + manager.notify(ID_GROUP, notification) } private fun createMangaIntent(context: Context, manga: Manga) = PendingIntent.getActivity( context, manga.hashCode(), DetailsActivity.newIntent(context, manga), - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE, ) companion object { + private const val TAG = "download" private const val CHANNEL_ID = "download" + private const val GROUP_ID = "downloads" private const val REQUEST_LIST = 6 + const val ID_GROUP = 9999 fun createChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -172,7 +291,7 @@ class DownloadNotification(private val context: Context, startId: Int) { val channel = NotificationChannel( CHANNEL_ID, context.getString(R.string.downloads), - NotificationManager.IMPORTANCE_LOW + NotificationManager.IMPORTANCE_LOW, ) channel.enableVibration(false) channel.enableLights(false) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index 993eab52e..d75082e2a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -38,7 +38,7 @@ import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator class DownloadService : BaseService() { private lateinit var downloadManager: DownloadManager - private lateinit var notificationSwitcher: ForegroundNotificationSwitcher + private lateinit var downloadNotification: DownloadNotification private val jobs = LinkedHashMap>() private val jobCount = MutableStateFlow(0) @@ -47,13 +47,14 @@ class DownloadService : BaseService() { override fun onCreate() { super.onCreate() isRunning = true - notificationSwitcher = ForegroundNotificationSwitcher(this) + downloadNotification = DownloadNotification(this) val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") downloadManager = get().create( coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)), ) DownloadNotification.createChannel(this) + startForeground(DownloadNotification.ID_GROUP, downloadNotification.buildGroupNotification()) val intentFilter = IntentFilter() intentFilter.addAction(ACTION_DOWNLOAD_CANCEL) intentFilter.addAction(ACTION_DOWNLOAD_RESUME) @@ -80,6 +81,7 @@ class DownloadService : BaseService() { } override fun onDestroy() { + downloadNotification.dismiss() unregisterReceiver(controlReceiver) isRunning = false super.onDestroy() @@ -98,10 +100,10 @@ class DownloadService : BaseService() { private fun listenJob(job: ProgressJob) { lifecycleScope.launch { val startId = job.progressValue.startId - val notification = DownloadNotification(this@DownloadService, startId) + val notificationItem = downloadNotification.newItem(startId) try { val timeLeftEstimator = TimeLeftEstimator() - notificationSwitcher.notify(startId, notification.create(job.progressValue, -1L)) + notificationItem.notify(job.progressValue, -1L) job.progressAsFlow() .onEach { state -> if (state is DownloadState.Progress) { @@ -114,7 +116,7 @@ class DownloadService : BaseService() { .whileActive() .collect { state -> val timeLeft = timeLeftEstimator.getEstimatedTimeLeft() - notificationSwitcher.notify(startId, notification.create(state, timeLeft)) + notificationItem.notify(state, timeLeft) } job.join() } finally { @@ -124,14 +126,11 @@ class DownloadService : BaseService() { .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)), ) } - notificationSwitcher.detach( - startId, - if (job.isCancelled) { - null - } else { - notification.create(job.progressValue, -1L) - }, - ) + if (job.isCancelled) { + notificationItem.dismiss() + } else { + notificationItem.notify(job.progressValue, -1L) + } stopSelf(startId) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt deleted file mode 100644 index 679405295..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt +++ /dev/null @@ -1,62 +0,0 @@ -package org.koitharu.kotatsu.download.ui.service - -import android.app.Notification -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.util.SparseArray -import androidx.core.app.ServiceCompat -import androidx.core.util.isEmpty -import androidx.core.util.size - -private const val DEFAULT_DELAY = 500L - -class ForegroundNotificationSwitcher( - private val service: Service, -) { - - private val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val notifications = SparseArray() - private val handler = Handler(Looper.getMainLooper()) - - @Synchronized - fun notify(startId: Int, notification: Notification) { - if (notifications.isEmpty()) { - service.startForeground(startId, notification) - } else { - notificationManager.notify(startId, notification) - } - notifications[startId] = notification - } - - @Synchronized - fun detach(startId: Int, notification: Notification?) { - notifications.remove(startId) - if (notifications.isEmpty()) { - ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_DETACH) - } - val nextIndex = notifications.size - 1 - if (nextIndex >= 0) { - val nextStartId = notifications.keyAt(nextIndex) - val nextNotification = notifications.valueAt(nextIndex) - service.startForeground(nextStartId, nextNotification) - } - handler.postDelayed(NotifyRunnable(startId, notification), DEFAULT_DELAY) - } - - private inner class NotifyRunnable( - private val startId: Int, - private val notification: Notification?, - ) : Runnable { - - override fun run() { - if (notification != null) { - notificationManager.notify(startId, notification) - } else { - notificationManager.cancel(startId) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 07ab4f6bb..ab2697193 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,320 +1,322 @@ - Закрыть меню - Открыть меню - На устройстве - Избранное - История - Произошла ошибка - Не удалось подключиться к интернету - Подробности - Главы - Список - Подробный список - Таблица - Вид списка - Настройки - Онлайн каталоги - Загрузка… - Глава %1$d из %2$d - Закрыть - Повторить - Очистить историю - Ничего не найдено - Истории пока нет - Читать - Избранного пока нет - В избранное - Новая категория - Добавить - Введите название - Сохранить - Поделиться - Создать ярлык… - Поделиться %s - Поиск - Поиск манги - Загрузка… - Обработка… - Загружено - Загрузки - Имя - Популярная - Обновлённая - Новая - Рейтинг - Порядок сортировки - Фильтр - Тема - Светлая - Тёмная - Как в системе - Страницы - Очистить - Очистить всю историю чтения полностью\? - Удалить - «%s» удалено из истории - «%s» удалено с устройства - Дождитесь завершения загрузки… - Сохранить страницу - Сохранено - Поделиться изображением - Импорт - Удалить - Операция не поддерживается - Выберите CBZ-файл или ZIP - Нет описания - История и кэш - Очистить кэш страниц - Кэш - Б|кБ|МБ|ГБ|ТБ - Стандартный - Манхва - Режим чтения - Размер таблицы - Поиск по %s - Удалить мангу - Удалить \"%s\" с устройства навсегда\? - Настройки чтения - Листание страниц - Нажатия по краям - Кнопки громкости - Продолжить - Предупреждение - Это может привести к расходу большого количества трафика - Больше не спрашивать - Отмена… - Ошибка - Очистить кэш миниатюр - Очистить историю поиска - Очищено - Только жесты - Внутренний накопитель - Внешнее хранилище - Домен - Проверять наличие новых версий приложения - Доступна новая версия приложения - Показывать уведомление, если доступна новая версия - Открыть в веб-браузере - В этой манге %s. Сохранить их все\? - Сохранить - Уведомления - Включено %1$d из %2$d - Новые главы - Загрузить - Читать с начала - Перезапустить - Настройки уведомлений - Звук уведомления - Светодиодная индикация - Вибросигнал - Категории избранного - Категории… - Переименовать - Удалить категорию \"%s\" из избранного\? + Закрыть меню + Открыть меню + На устройстве + Избранное + История + Произошла ошибка + Не удалось подключиться к интернету + Подробности + Главы + Список + Подробный список + Таблица + Вид списка + Настройки + Онлайн каталоги + Загрузка… + Глава %1$d из %2$d + Закрыть + Повторить + Очистить историю + Ничего не найдено + Истории пока нет + Читать + Избранного пока нет + В избранное + Новая категория + Добавить + Введите название + Сохранить + Поделиться + Создать ярлык… + Поделиться %s + Поиск + Поиск манги + Загрузка… + Обработка… + Загружено + Загрузки + Имя + Популярная + Обновлённая + Новая + Рейтинг + Порядок сортировки + Фильтр + Тема + Светлая + Тёмная + Как в системе + Страницы + Очистить + Очистить всю историю чтения полностью\? + Удалить + «%s» удалено из истории + «%s» удалено с устройства + Дождитесь завершения загрузки… + Сохранить страницу + Сохранено + Поделиться изображением + Импорт + Удалить + Операция не поддерживается + Выберите CBZ-файл или ZIP + Нет описания + История и кэш + Очистить кэш страниц + Кэш + Б|кБ|МБ|ГБ|ТБ + Стандартный + Манхва + Режим чтения + Размер таблицы + Поиск по %s + Удалить мангу + Удалить \"%s\" с устройства навсегда\? + Настройки чтения + Листание страниц + Нажатия по краям + Кнопки громкости + Продолжить + Предупреждение + Это может привести к расходу большого количества трафика + Больше не спрашивать + Отмена… + Ошибка + Очистить кэш миниатюр + Очистить историю поиска + Очищено + Только жесты + Внутренний накопитель + Внешнее хранилище + Домен + Проверять наличие новых версий приложения + Доступна новая версия приложения + Показывать уведомление, если доступна новая версия + Открыть в веб-браузере + В этой манге %s. Сохранить их все\? + Сохранить + Уведомления + Включено %1$d из %2$d + Новые главы + Загрузить + Читать с начала + Перезапустить + Настройки уведомлений + Звук уведомления + Светодиодная индикация + Вибросигнал + Категории избранного + Категории… + Переименовать + Удалить категорию \"%s\" из избранного\? \nВся манга в ней будет потеряна. - Удалить - Как-то здесь пусто… - Попробуйте переформулировать запрос. - Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию - То, что вы прочитаете, будет отображено здесь - Найдите, что почитать, в боковом меню. - Сохраните что-нибудь - Сохраните что-нибудь из онлайн-каталога или импортируйте из файла. - Полка - Недавнее - Анимация листания - Папка для загрузок - Недоступно - Нет доступного хранилища - Другое хранилище - Готово - Всё избранное - Категория пуста - Прочитать позже - Обновления - Новые главы из того, что вы читаете, будут показаны здесь - Результаты поиска - Похожие - Новая версия: %s - Размер: %s - Ожидание подключения… - Очистить ленту обновлений - Очищено - Повернуть экран - Обновить - Обновление скоро начнётся - Следить за обновлениями - Не проверять - Введите пароль - Неверный пароль - Защитить приложение - Запрашивать пароль при запуске Kotatsu - Повторите пароль - Пароли не совпадают - О программе - Версия %s - Проверить обновления - Проверка обновления… - Не удалось проверить обновления - Нет доступных обновлений - Справа налево (←) - Создать категорию - Масштабирование - Вписать в экран - Подогнать по высоте - Подогнать по ширине - Исходный размер - Чёрная - Потребляет меньше энергии на экранах AMOLED - Резервное копирование и восстановление - Создать резервную копию - Восстановить данные - Восстановлено - Подготовка… - Файл не найден - Все данные были восстановлены - Данные были восстановлены, но возникли некоторые ошибки - Вы можете создать резервную копию избранного и истории и потом восстановить их - Только что - Вчера - Давно - Группировать - Сегодня - Попробовать ещё раз - Выбранный режим будет сохранён для текущей манги - Без звука - Необходимо пройти CAPTCHA - Пройти - Очистить куки - Все файлы cookie были удалены - Проверка новых глав: %1$d из %2$d - Очистить ленту - Удалить всю историю обновлений навсегда\? - Проверка новых глав - В обратном порядке - Войти - Авторизуйтесь, чтобы просмотреть этот контент - По умолчанию: %s - …и ещё %1$d - Далее - Введите пароль для запуска приложения - Подтвердить - Пароль должен состоять из 4 символов или более - Поиск только по %s - Другие - Добро пожаловать - Удалить все последние поисковые запросы навсегда\? - Резервная копия сохранена - Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач. - Подробнее - В очереди - Нет активных загрузок - Глава отсутствует - Скачайте или прочитайте эту недостающую главу онлайн. - Помочь с переводом приложения - Перевод - Тема на 4PDA - Обратная связь - Авторизация выполнена - Вход в %s не поддерживается - Вы выйдете из всех источников - Жанры - Завершено - Онгоинг - Формат даты - По умолчанию - Исключить NSFW мангу из истории - Вы должны ввести имя - Показывать номера страницы - Включенные источники - Доступные источники - Динамическая тема - Применяет тему приложения, основанную на цветовой палитре обоев на устройстве - Политика скриншотов - Разрешить - Запретить для NSFW - Всегда блокировать - Рекомендации - Включить рекомендации - Предлагать мангу на основе Ваших предпочтений - Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы - Начните читать мангу, чтобы получать персональные предложения - Не предлагать NSFW мангу - Включено - Выключено - Не удалось загрузить список жанров - Вычисление… - Создать проблему на GitHub - Импорт манги: %1$d из %2$d - Сбросить фильтр - Поиск по жанрам - Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках. - Никогда - Только по Wi-Fi - Всегда - Предварительная загрузка страниц - Вы авторизованы как %s - 18+ - Разные языки - Найти главу - В этой манге нет глав - Оформление - Контент - Обновление рекомендаций - Исключить жанры - Укажите жанры, которые Вы не хотите видеть в рекомендациях - Удалить выбранную мангу с накопителя? - Удаление завершено - Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе - Загружать параллельно - Замедление загрузки - Помогает избежать блокировки IP-адреса - Обработка сохранённой манги - Главы будут удалены в фоновом режиме. Это может занять какое-то время - Скрыть - Доступны новые источники манги - Проверять новые главы и уведомлять о них - Вы будете получать уведомления об обновлении манги, которую Вы читаете - Вы не будете получать уведомления, но новые главы будут отображаться в списке - Включить уведомления - Название - Изменить - Изменить категорию - Отслеживание - Нет категорий избранного - Добавить закладку - Удалить закладку - Закладки - Закладка удалена - Закладка добавлена - Отменить - Удалено из истории - DNS через HTTPS - Режим по умолчанию - Автоопределение режима чтения - Автоматически определяет, является ли манга веб-комиксом - Отключить оптимизацию батареи - Помогает с фоновой проверкой обновлений - Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить - Отправить - Отключить все - Использовать отпечаток пальца, если доступно - Манга из Вашего избранного - Манга, которую Вы недавно читали - Читаю - Запланировано - Отложено - Заброшено - Завершено - Показать процент прочитанного в истории и избранном - Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен - %1$s%% - Отчёт - Выйти - Перечитываю - Показать индикаторы прогресса чтения - Удаление данных - Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы - Показать все + Удалить + Как-то здесь пусто… + Попробуйте переформулировать запрос. + Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию + То, что вы прочитаете, будет отображено здесь + Найдите, что почитать, в боковом меню. + Сохраните что-нибудь + Сохраните что-нибудь из онлайн-каталога или импортируйте из файла. + Полка + Недавнее + Анимация листания + Папка для загрузок + Недоступно + Нет доступного хранилища + Другое хранилище + Готово + Всё избранное + Категория пуста + Прочитать позже + Обновления + Новые главы из того, что вы читаете, будут показаны здесь + Результаты поиска + Похожие + Новая версия: %s + Размер: %s + Ожидание подключения… + Очистить ленту обновлений + Очищено + Повернуть экран + Обновить + Обновление скоро начнётся + Следить за обновлениями + Не проверять + Введите пароль + Неверный пароль + Защитить приложение + Запрашивать пароль при запуске Kotatsu + Повторите пароль + Пароли не совпадают + О программе + Версия %s + Проверить обновления + Проверка обновления… + Не удалось проверить обновления + Нет доступных обновлений + Справа налево (←) + Создать категорию + Масштабирование + Вписать в экран + Подогнать по высоте + Подогнать по ширине + Исходный размер + Чёрная + Потребляет меньше энергии на экранах AMOLED + Резервное копирование и восстановление + Создать резервную копию + Восстановить данные + Восстановлено + Подготовка… + Файл не найден + Все данные были восстановлены + Данные были восстановлены, но возникли некоторые ошибки + Вы можете создать резервную копию избранного и истории и потом восстановить их + Только что + Вчера + Давно + Группировать + Сегодня + Попробовать ещё раз + Выбранный режим будет сохранён для текущей манги + Без звука + Необходимо пройти CAPTCHA + Пройти + Очистить куки + Все файлы cookie были удалены + Проверка новых глав: %1$d из %2$d + Очистить ленту + Удалить всю историю обновлений навсегда\? + Проверка новых глав + В обратном порядке + Войти + Авторизуйтесь, чтобы просмотреть этот контент + По умолчанию: %s + …и ещё %1$d + Далее + Введите пароль для запуска приложения + Подтвердить + Пароль должен состоять из 4 символов или более + Поиск только по %s + Другие + Добро пожаловать + Удалить все последние поисковые запросы навсегда\? + Резервная копия сохранена + Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач. + Подробнее + В очереди + Нет активных загрузок + Глава отсутствует + Скачайте или прочитайте эту недостающую главу онлайн. + Помочь с переводом приложения + Перевод + Тема на 4PDA + Обратная связь + Авторизация выполнена + Вход в %s не поддерживается + Вы выйдете из всех источников + Жанры + Завершено + Онгоинг + Формат даты + По умолчанию + Исключить NSFW мангу из истории + Вы должны ввести имя + Показывать номера страницы + Включенные источники + Доступные источники + Динамическая тема + Применяет тему приложения, основанную на цветовой палитре обоев на устройстве + Политика скриншотов + Разрешить + Запретить для NSFW + Всегда блокировать + Рекомендации + Включить рекомендации + Предлагать мангу на основе Ваших предпочтений + Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы + Начните читать мангу, чтобы получать персональные предложения + Не предлагать NSFW мангу + Включено + Выключено + Не удалось загрузить список жанров + Вычисление… + Создать проблему на GitHub + Импорт манги: %1$d из %2$d + Сбросить фильтр + Поиск по жанрам + Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках. + Никогда + Только по Wi-Fi + Всегда + Предварительная загрузка страниц + Вы авторизованы как %s + 18+ + Разные языки + Найти главу + В этой манге нет глав + Оформление + Контент + Обновление рекомендаций + Исключить жанры + Укажите жанры, которые Вы не хотите видеть в рекомендациях + Удалить выбранную мангу с накопителя? + Удаление завершено + Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе + Загружать параллельно + Замедление загрузки + Помогает избежать блокировки IP-адреса + Обработка сохранённой манги + Главы будут удалены в фоновом режиме. Это может занять какое-то время + Скрыть + Доступны новые источники манги + Проверять новые главы и уведомлять о них + Вы будете получать уведомления об обновлении манги, которую Вы читаете + Вы не будете получать уведомления, но новые главы будут отображаться в списке + Включить уведомления + Название + Изменить + Изменить категорию + Отслеживание + Нет категорий избранного + Добавить закладку + Удалить закладку + Закладки + Закладка удалена + Закладка добавлена + Отменить + Удалено из истории + DNS через HTTPS + Режим по умолчанию + Автоопределение режима чтения + Автоматически определяет, является ли манга веб-комиксом + Отключить оптимизацию батареи + Помогает с фоновой проверкой обновлений + Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить + Отправить + Отключить все + Использовать отпечаток пальца, если доступно + Манга из Вашего избранного + Манга, которую Вы недавно читали + Читаю + Запланировано + Отложено + Заброшено + Завершено + Показать процент прочитанного в истории и избранном + Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен + %1$s%% + Отчёт + Выйти + Перечитываю + Показать индикаторы прогресса чтения + Удаление данных + Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы + Показать все + Downloading manga + Completed \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a34f0fc89..d64335ae2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -323,4 +323,7 @@ Invalid domain Select range Content not found or removed + Downloading manga + <b>%1$s</b> %2$s + Completed \ No newline at end of file