Group notifcations for Library updates (#2582)
This commit is contained in:
parent
12aa04be93
commit
7f115f2e83
@ -52,6 +52,14 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapter(url: String, mangaId: Long) = db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(url, mangaId)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|
||||||
|
@ -30,27 +30,11 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private var isDownloading = false
|
private var isDownloading = false
|
||||||
|
|
||||||
/**
|
|
||||||
* The size of queue on start download.
|
|
||||||
*/
|
|
||||||
var initialQueueSize = 0
|
|
||||||
set(value) {
|
|
||||||
if (value != 0) {
|
|
||||||
isSingleChapter = (value == 1)
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
var errorThrown = false
|
var errorThrown = false
|
||||||
|
|
||||||
/**
|
|
||||||
* Updated when only single page is downloaded
|
|
||||||
*/
|
|
||||||
var isSingleChapter = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when paused
|
* Updated when paused
|
||||||
*/
|
*/
|
||||||
@ -144,39 +128,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Reset initial values
|
// Reset initial values
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
initialQueueSize = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when chapter is downloaded.
|
|
||||||
*
|
|
||||||
* @param download download object containing download information.
|
|
||||||
*/
|
|
||||||
fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
|
|
||||||
// Check if last download
|
|
||||||
if (!queue.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Create notification.
|
|
||||||
with(notificationBuilder) {
|
|
||||||
val title = download.manga.title.chop(15)
|
|
||||||
val quotedTitle = Pattern.quote(title)
|
|
||||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
setAutoCancel(true)
|
|
||||||
clearActions()
|
|
||||||
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show notification.
|
|
||||||
notificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset initial values
|
|
||||||
isDownloading = false
|
|
||||||
initialQueueSize = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,8 +126,6 @@ class Downloader(
|
|||||||
if (notifier.paused) {
|
if (notifier.paused) {
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onDownloadPaused()
|
||||||
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
|
|
||||||
notifier.isSingleChapter = false
|
|
||||||
} else {
|
} else {
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
}
|
}
|
||||||
@ -229,9 +227,6 @@ class Downloader(
|
|||||||
if (chaptersToQueue.isNotEmpty()) {
|
if (chaptersToQueue.isNotEmpty()) {
|
||||||
queue.addAll(chaptersToQueue)
|
queue.addAll(chaptersToQueue)
|
||||||
|
|
||||||
// Initialize queue size.
|
|
||||||
notifier.initialQueueSize = queue.size
|
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
// Send the list of downloads to the downloader.
|
// Send the list of downloads to the downloader.
|
||||||
downloadsRelay.call(chaptersToQueue)
|
downloadsRelay.call(chaptersToQueue)
|
||||||
@ -428,9 +423,6 @@ class Downloader(
|
|||||||
queue.remove(download)
|
queue.remove(download)
|
||||||
}
|
}
|
||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
|
||||||
notifier.onDownloadCompleted(download, queue)
|
|
||||||
}
|
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,10 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
@ -202,9 +206,9 @@ class LibraryUpdateService(
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
?: return Service.START_NOT_STICKY
|
?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
@ -276,7 +280,7 @@ class LibraryUpdateService(
|
|||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
// List containing new updates
|
// List containing new updates
|
||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
// list containing failed updates
|
// list containing failed updates
|
||||||
val failedUpdates = ArrayList<Manga>()
|
val failedUpdates = ArrayList<Manga>()
|
||||||
// List containing categories that get included in downloads.
|
// List containing categories that get included in downloads.
|
||||||
@ -309,7 +313,8 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Convert to the manga that contains new chapters.
|
// Convert to the manga that contains new chapters.
|
||||||
.map { manga }
|
.map { Pair(manga, (it.first.sortedByDescending { ch -> ch
|
||||||
|
.source_order }.toTypedArray())) }
|
||||||
}
|
}
|
||||||
// Add manga with new chapters to the list.
|
// Add manga with new chapters to the list.
|
||||||
.doOnNext { manga ->
|
.doOnNext { manga ->
|
||||||
@ -331,6 +336,7 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
.map { manga -> manga.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
@ -444,39 +450,76 @@ class LibraryUpdateService(
|
|||||||
*
|
*
|
||||||
* @param updates a list of manga with new updates.
|
* @param updates a list of manga with new updates.
|
||||||
*/
|
*/
|
||||||
private fun showResultNotification(updates: List<Manga>) {
|
private fun showResultNotification(updates: List<Pair<Manga, Array<Chapter>>>) {
|
||||||
val newUpdates = updates.map { it.title.chop(45) }.toMutableSet()
|
val notifications = ArrayList<Pair<Notification, Int>>()
|
||||||
|
updates.forEach {
|
||||||
// Append new chapters from a previous, existing notification
|
val manga = it.first
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val chapters = it.second
|
||||||
val previousNotification = notificationManager.activeNotifications
|
val chapterNames = chapters.map { chapter -> chapter.name }.toSet()
|
||||||
.find { it.id == Notifications.ID_LIBRARY_RESULT }
|
notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
if (previousNotification != null) {
|
try {
|
||||||
val oldUpdates = previousNotification.notification.extras
|
val icon = Glide.with(this@LibraryUpdateService)
|
||||||
.getString(Notification.EXTRA_BIG_TEXT)
|
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
|
||||||
|
.override(256, 256).submit().get()
|
||||||
if (!oldUpdates.isNullOrEmpty()) {
|
setLargeIcon(icon)
|
||||||
newUpdates += oldUpdates.split("\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (e: Exception) { }
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
|
setContentTitle(manga.title)
|
||||||
|
val chaptersNames = if (chapterNames.size > 5) {
|
||||||
|
"${chapterNames.take(4).joinToString(", ")}, " +
|
||||||
|
resources.getQuantityString(R.plurals.notification_and_n_more,
|
||||||
|
(chapterNames.size - 4), (chapterNames.size - 4))
|
||||||
|
} else chapterNames.joinToString(", ")
|
||||||
|
setContentText(chaptersNames)
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setContentIntent(
|
||||||
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
|
this@LibraryUpdateService, manga, chapters.first()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
|
||||||
|
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService,
|
||||||
|
manga, chapters, Notifications.ID_NEW_CHAPTERS))
|
||||||
|
addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
|
||||||
|
NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService,
|
||||||
|
manga, Notifications.ID_NEW_CHAPTERS))
|
||||||
|
setAutoCancel(true)
|
||||||
|
}, manga.id.hashCode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
|
NotificationManagerCompat.from(this).apply {
|
||||||
setSmallIcon(R.drawable.ic_book_white_24dp)
|
|
||||||
|
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(getString(R.string.notification_new_chapters))
|
setContentTitle(getString(R.string.notification_new_chapters))
|
||||||
if (newUpdates.size > 1) {
|
if (updates.size > 1) {
|
||||||
setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
|
setContentText(resources.getQuantityString(R.plurals
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
|
.notification_new_chapters_text,
|
||||||
setNumber(newUpdates.size)
|
updates.size, updates.size))
|
||||||
} else {
|
setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") {
|
||||||
setContentText(newUpdates.first())
|
it.first.title.chop(45)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setContentText(updates.first().first.title.chop(45))
|
||||||
}
|
}
|
||||||
priority = NotificationCompat.PRIORITY_HIGH
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
|
setGroupSummary(true)
|
||||||
setContentIntent(getNotificationIntent())
|
setContentIntent(getNotificationIntent())
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
notifications.forEach {
|
||||||
|
notify(it.second, it.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,7 @@ import android.app.PendingIntent
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -12,11 +13,17 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
@ -60,6 +67,15 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
|
intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
|
||||||
}
|
}
|
||||||
|
ACTION_MARK_AS_READ -> {
|
||||||
|
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
if (notificationId > -1) dismissNotification(
|
||||||
|
context, notificationId, intent.getIntExtra(EXTRA_GROUP_ID, 0)
|
||||||
|
)
|
||||||
|
val urls = intent.getStringArrayExtra(EXTRA_CHAPTER_URL) ?: return
|
||||||
|
val mangaId = intent.getLongExtra(EXTRA_MANGA_ID, -1)
|
||||||
|
markAsRead(urls, mangaId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +120,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val db = DatabaseHelper(context)
|
val db = DatabaseHelper(context)
|
||||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||||
|
|
||||||
if (manga != null && chapter != null) {
|
if (manga != null && chapter != null) {
|
||||||
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
|
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
@ -143,6 +158,28 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
Handler().post { dismissNotification(context, notificationId) }
|
Handler().post { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when user wants to mark as read
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun markAsRead(chapterUrls: Array<String>, mangaId: Long) {
|
||||||
|
val db: DatabaseHelper = Injekt.get()
|
||||||
|
chapterUrls.forEach {
|
||||||
|
val chapter = db.getChapter(it, mangaId).executeAsBlocking() ?: return
|
||||||
|
chapter.read = true
|
||||||
|
db.updateChapterProgress(chapter).executeAsBlocking()
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
if (preferences.removeAfterMarkedAsRead()) {
|
||||||
|
val manga = db.getManga(mangaId).executeAsBlocking() ?: return
|
||||||
|
val sourceManager: SourceManager = Injekt.get()
|
||||||
|
val source = sourceManager.get(manga.source) ?: return
|
||||||
|
downloadManager.deleteChapters(listOf(chapter), manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@ -155,6 +192,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to cancel library update.
|
// Called to cancel library update.
|
||||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||||
|
|
||||||
|
// Called to mark as read
|
||||||
|
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||||
|
|
||||||
// Called to open chapter
|
// Called to open chapter
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
|
||||||
@ -179,12 +219,18 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Value containing notification id.
|
// Value containing notification id.
|
||||||
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
||||||
|
|
||||||
|
// Value containing group id.
|
||||||
|
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
||||||
|
|
||||||
// Value containing manga id.
|
// Value containing manga id.
|
||||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||||
|
|
||||||
// Value containing chapter id.
|
// Value containing chapter id.
|
||||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||||
|
|
||||||
|
// Value containing chapter url.
|
||||||
|
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||||
*
|
*
|
||||||
@ -246,6 +292,32 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? =
|
||||||
|
null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
val groupKey = context.notificationManager.activeNotifications.find {
|
||||||
|
it.id == notificationId
|
||||||
|
}?.groupKey
|
||||||
|
if (groupId != null && groupId != 0 && groupKey != null && groupKey.isNotEmpty()) {
|
||||||
|
val notifications = context.notificationManager.activeNotifications.filter {
|
||||||
|
it.groupKey == groupKey
|
||||||
|
}
|
||||||
|
if (notifications.size == 2) {
|
||||||
|
context.notificationManager.cancel(groupId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.notificationManager.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
|
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
|
||||||
*
|
*
|
||||||
@ -281,19 +353,55 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that start a reader activity containing chapter.
|
* Returns [PendingIntent] that starts a reader activity containing chapter.
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param manga manga of chapter
|
* @param manga manga of chapter
|
||||||
* @param chapter chapter that needs to be opened
|
* @param chapter chapter that needs to be opened
|
||||||
*/
|
*/
|
||||||
internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
|
internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter:
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
Chapter): PendingIntent {
|
||||||
action = ACTION_OPEN_CHAPTER
|
val newIntent = ReaderActivity.newIntent(context, manga, chapter)
|
||||||
putExtra(EXTRA_MANGA_ID, manga.id)
|
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent
|
||||||
putExtra(EXTRA_CHAPTER_ID, chapter.id)
|
.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that opens the manga info controller.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
*/
|
||||||
|
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int):
|
||||||
|
PendingIntent {
|
||||||
|
val newIntent =
|
||||||
|
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
.putExtra(MangaController.MANGA_EXTRA, manga.id)
|
||||||
|
.putExtra("notificationId", manga.id.hashCode())
|
||||||
|
.putExtra("groupId", groupId)
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
*/
|
||||||
|
internal fun markAsReadPendingBroadcast(context: Context, manga: Manga, chapters:
|
||||||
|
Array<Chapter>, groupId: Int):
|
||||||
|
PendingIntent {
|
||||||
|
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_MARK_AS_READ
|
||||||
|
putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray())
|
||||||
|
putExtra(EXTRA_MANGA_ID, manga.id)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode())
|
||||||
|
putExtra(EXTRA_GROUP_ID, groupId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,15 +23,21 @@ object Notifications {
|
|||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_LIBRARY = "library_channel"
|
const val CHANNEL_LIBRARY = "library_channel"
|
||||||
const val ID_LIBRARY_PROGRESS = 101
|
const val ID_LIBRARY_PROGRESS = -101
|
||||||
const val ID_LIBRARY_RESULT = 102
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER = 201
|
const val ID_DOWNLOAD_CHAPTER = -201
|
||||||
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
|
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the library updater.
|
||||||
|
*/
|
||||||
|
const val CHANNEL_NEW_CHAPTERS = "new_chapters_channel"
|
||||||
|
const val ID_NEW_CHAPTERS = -301
|
||||||
|
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the notification channels introduced in Android Oreo.
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
@ -45,9 +51,15 @@ object Notifications {
|
|||||||
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
|
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||||
NotificationManager.IMPORTANCE_LOW),
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
||||||
NotificationManager.IMPORTANCE_LOW),
|
NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
||||||
NotificationManager.IMPORTANCE_LOW)
|
NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
)
|
)
|
||||||
context.notificationManager.createNotificationChannels(channels)
|
context.notificationManager.createNotificationChannels(channels)
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import androidx.drawerlayout.widget.DrawerLayout
|
|||||||
import com.bluelinelabs.conductor.*
|
import com.bluelinelabs.conductor.*
|
||||||
import eu.kanade.tachiyomi.Migrations
|
import eu.kanade.tachiyomi.Migrations
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
@ -136,6 +137,10 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIntentAction(intent: Intent): Boolean {
|
private fun handleIntentAction(intent: Intent): Boolean {
|
||||||
|
val notificationId = intent.getIntExtra("notificationId", -1)
|
||||||
|
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
||||||
|
applicationContext, notificationId, intent.getIntExtra("groupId", 0)
|
||||||
|
)
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
|
SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
|
||||||
SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
|
SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
|
||||||
|
@ -19,6 +19,8 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||||
@ -104,10 +106,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
const val VERTICAL = 3
|
const val VERTICAL = 3
|
||||||
const val WEBTOON = 4
|
const val WEBTOON = 4
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
fun newIntent(context: Context, manga: Manga, chapter: Chapter):
|
||||||
|
Intent {
|
||||||
val intent = Intent(context, ReaderActivity::class.java)
|
val intent = Intent(context, ReaderActivity::class.java)
|
||||||
intent.putExtra("manga", manga.id)
|
intent.putExtra("manga", manga.id)
|
||||||
intent.putExtra("chapter", chapter.id)
|
intent.putExtra("chapter", chapter.id)
|
||||||
|
// chapters just added from library updates don't have an id yet
|
||||||
|
intent.putExtra("chapterUrl", chapter.url)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,13 +132,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
if (presenter.needsInit()) {
|
if (presenter.needsInit()) {
|
||||||
val manga = intent.extras!!.getLong("manga", -1)
|
val manga = intent.extras!!.getLong("manga", -1)
|
||||||
val chapter = intent.extras!!.getLong("chapter", -1)
|
val chapter = intent.extras!!.getLong("chapter", -1)
|
||||||
|
val chapterUrl = intent.extras!!.getString("chapterUrl", "")
|
||||||
if (manga == -1L || chapter == -1L) {
|
if (manga == -1L || chapterUrl == "" && chapter == -1L) {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
|
||||||
presenter.init(manga, chapter)
|
if (chapter > -1) presenter.init(manga, chapter)
|
||||||
|
else presenter.init(manga, chapterUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedState != null) {
|
if (savedState != null) {
|
||||||
|
@ -185,6 +185,19 @@ class ReaderPresenter(
|
|||||||
}, ReaderActivity::setInitialChapterError)
|
}, ReaderActivity::setInitialChapterError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes this presenter with the given [mangaId] and [chapterUrl]. This method will
|
||||||
|
* fetch the manga from the database and initialize the initial chapter.
|
||||||
|
*/
|
||||||
|
fun init(mangaId: Long, chapterUrl: String) {
|
||||||
|
if (!needsInit()) return
|
||||||
|
val context = Injekt.get<Application>()
|
||||||
|
val db = DatabaseHelper(context)
|
||||||
|
val chapterId = db.getChapter(chapterUrl, mangaId).executeAsBlocking()?.id
|
||||||
|
if (chapterId != null)
|
||||||
|
init(mangaId, chapterId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes this presenter with the given [manga] and [initialChapterId]. This method will
|
* Initializes this presenter with the given [manga] and [initialChapterId]. This method will
|
||||||
* set the chapter loader, view subscriptions and trigger an initial load.
|
* set the chapter loader, view subscriptions and trigger an initial load.
|
||||||
|
@ -13,12 +13,14 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.android.synthetic.main.recent_chapters_controller.empty_view
|
import kotlinx.android.synthetic.main.recent_chapters_controller.empty_view
|
||||||
import kotlinx.android.synthetic.main.recent_chapters_controller.recycler
|
import kotlinx.android.synthetic.main.recent_chapters_controller.recycler
|
||||||
@ -68,7 +70,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
|
|||||||
*/
|
*/
|
||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
|
||||||
// Init RecyclerView and adapter
|
// Init RecyclerView and adapter
|
||||||
val layoutManager = LinearLayoutManager(view.context)
|
val layoutManager = LinearLayoutManager(view.context)
|
||||||
recycler.layoutManager = layoutManager
|
recycler.layoutManager = layoutManager
|
||||||
|
19
app/src/main/res/drawable/ic_tachi.xml
Normal file
19
app/src/main/res/drawable/ic_tachi.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="256dp"
|
||||||
|
android:height="256dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="256"
|
||||||
|
android:viewportHeight="256">
|
||||||
|
<path
|
||||||
|
android:pathData="M102.6,19.2c0.3,5.7 0.6,13.1 0.8,16.6l0.4,6.3 -42.7,-0.3c-23.4,-0.2 -43.4,-0.7 -44.3,-1.2 -1.7,-0.8 -1.8,0.6 -1.8,20.4l0,21.2 2.3,-0.6c6.6,-1.9 33.3,-2.6 108.7,-2.6 75.4,-0 102.1,0.7 108.8,2.6l2.2,0.6 0,-21.2c0,-19.8 -0.1,-21.2 -1.7,-20.4 -1,0.5 -21,1 -44.4,1.2l-42.7,0.3 0.4,-6.3c0.2,-3.5 0.5,-10.9 0.8,-16.6l0.4,-10.2 -23.8,-0 -23.8,-0 0.4,10.2z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M58.8,93.2c-10.4,3.9 -18.8,7.7 -18.8,8.3 0,0.7 1.4,4.3 3.1,8.1 8,17.7 20.6,61.5 24.1,83.6 0.6,4.3 1.6,7.8 2.2,7.8 0.6,-0 10.4,-3.2 21.9,-7.1 14.9,-5.2 20.7,-7.6 20.7,-8.7 0,-3.2 -17.8,-61 -26.2,-85 -3.8,-10.6 -5.4,-14.2 -6.7,-14.1 -0.9,-0 -10,3.2 -20.3,7.1z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M167.2,93.7c-3.3,21 -15.6,61.6 -28.8,95l-6.9,17.3 -62.7,-0 -62.8,-0 0,20 0,20 121.5,-0 121.5,-0 0,-20 0,-20 -37.1,-0c-29.3,-0 -37,-0.3 -36.6,-1.3 0.3,-0.6 2.7,-5.9 5.3,-11.7 2.5,-5.8 7.5,-18.3 11,-27.9 6.7,-18.4 21.4,-64.3 21.4,-67 0,-1.3 -4.6,-2.8 -21.2,-6.9 -11.7,-2.9 -21.8,-5.2 -22.4,-5.2 -0.6,-0 -1.6,3.5 -2.2,7.7z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
@ -61,6 +61,7 @@
|
|||||||
<string name="action_sort_down">Sort down</string>
|
<string name="action_sort_down">Sort down</string>
|
||||||
<string name="action_show_downloaded">Downloaded</string>
|
<string name="action_show_downloaded">Downloaded</string>
|
||||||
<string name="action_next_unread">Next unread</string>
|
<string name="action_next_unread">Next unread</string>
|
||||||
|
<string name="action_view_chapters">View chapters</string>
|
||||||
<string name="action_start">Start</string>
|
<string name="action_start">Start</string>
|
||||||
<string name="action_stop">Stop</string>
|
<string name="action_stop">Stop</string>
|
||||||
<string name="action_pause">Pause</string>
|
<string name="action_pause">Pause</string>
|
||||||
@ -484,7 +485,14 @@
|
|||||||
<!-- Library update service notifications -->
|
<!-- Library update service notifications -->
|
||||||
<string name="notification_update_progress">Update progress: %1$d/%2$d</string>
|
<string name="notification_update_progress">Update progress: %1$d/%2$d</string>
|
||||||
<string name="notification_new_chapters">New chapters found</string>
|
<string name="notification_new_chapters">New chapters found</string>
|
||||||
<string name="notification_new_chapters_text">For %1$d titles</string>
|
<plurals name="notification_new_chapters_text">
|
||||||
|
<item quantity="one">For %d title</item>
|
||||||
|
<item quantity="other">For %d titles</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="notification_and_n_more">
|
||||||
|
<item quantity="one">and %1$d more chapter.</item>
|
||||||
|
<item quantity="other">and %1$d more chapters.</item>
|
||||||
|
</plurals>
|
||||||
<string name="notification_cover_update_failed">Failed to update cover</string>
|
<string name="notification_cover_update_failed">Failed to update cover</string>
|
||||||
<string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
|
<string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
|
||||||
<string name="notification_not_connected_to_ac_title">Sync canceled</string>
|
<string name="notification_not_connected_to_ac_title">Sync canceled</string>
|
||||||
@ -537,7 +545,8 @@
|
|||||||
|
|
||||||
<!-- Notification channels -->
|
<!-- Notification channels -->
|
||||||
<string name="channel_common">Common</string>
|
<string name="channel_common">Common</string>
|
||||||
<string name="channel_library">Library</string>
|
<string name="channel_library">Updating Library</string>
|
||||||
<string name="channel_downloader">Downloader</string>
|
<string name="channel_downloader">Downloader</string>
|
||||||
|
<string name="channel_new_chapters">New Chapters</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user