Migrate to multiplatform string resources (#10147)

* Migrate to multiplatform string resources

* Move plurals translations into separate files

* Fix lint check on generated files
This commit is contained in:
arkon
2023-11-18 13:54:56 -05:00
committed by GitHub
parent c39ae21f4a
commit 46e734fc8e
340 changed files with 5741 additions and 6292 deletions
@@ -16,16 +16,13 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@@ -41,6 +38,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import tachiyomi.core.i18n.localize
import tachiyomi.core.preference.getAndSet
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
@@ -62,9 +60,9 @@ import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.model.SourceNotInstalledException
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@@ -114,22 +112,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
}
val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) } ?: Target.CHAPTERS
// If this is a chapter update, set the last update time to now
if (target == Target.CHAPTERS) {
libraryPreferences.lastUpdatedTimestamp().set(Date().time)
}
libraryPreferences.lastUpdatedTimestamp().set(Date().time)
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
addMangaToQueue(categoryId)
return withIOContext {
try {
when (target) {
Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers()
}
updateChapterList()
Result.success()
} catch (e: Exception) {
if (e is CancellationException) {
@@ -191,29 +181,32 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.filter {
when {
it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_always_update))
skippedUpdates.add(it.manga to context.localize(MR.strings.skipped_reason_not_always_update))
false
}
MANGA_NON_COMPLETED in restrictions && it.manga.status.toInt() == SManga.COMPLETED -> {
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_completed))
skippedUpdates.add(it.manga to context.localize(MR.strings.skipped_reason_completed))
false
}
MANGA_HAS_UNREAD in restrictions && it.unreadCount != 0L -> {
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_caught_up))
skippedUpdates.add(it.manga to context.localize(MR.strings.skipped_reason_not_caught_up))
false
}
MANGA_NON_READ in restrictions && it.totalChapters > 0L && !it.hasStarted -> {
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_started))
skippedUpdates.add(it.manga to context.localize(MR.strings.skipped_reason_not_started))
false
}
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> {
skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_in_release_period))
skippedUpdates.add(
it.manga to context.localize(MR.strings.skipped_reason_not_in_release_period),
)
false
}
else -> true
}
}
@@ -294,10 +287,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
} catch (e: Throwable) {
val errorMessage = when (e) {
is NoChaptersException -> context.getString(R.string.no_chapters_error)
is NoChaptersException -> context.localize(MR.strings.no_chapters_error)
// failedUpdates will already have the source, don't need to copy it into the message
is SourceNotInstalledException -> context.getString(
R.string.loader_not_implemented_error,
is SourceNotInstalledException -> context.localize(
MR.strings.loader_not_implemented_error,
)
else -> e.message
}
@@ -359,51 +352,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow)
}
private suspend fun updateCovers() {
val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope {
mangaToUpdate.groupBy { it.manga.source }
.values
.map { mangaInSource ->
async {
semaphore.withPermit {
mangaInSource.forEach { libraryManga ->
val manga = libraryManga.manga
ensureActive()
withUpdateNotification(
currentlyUpdatingManga,
progressCount,
manga,
) {
val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
try {
val networkManga = source.getMangaDetails(manga.toSManga())
val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true)
.copyFrom(networkManga)
try {
updateManga.await(updatedManga.toMangaUpdate())
} catch (e: Exception) {
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
}
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
}
}
}
}
.awaitAll()
}
notifier.cancelProgressNotification()
}
private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger,
@@ -440,7 +388,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (errors.isNotEmpty()) {
val file = context.createFileInCacheDir("tachiyomi_update_errors.txt")
file.bufferedWriter().use { out ->
out.write(context.getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
out.write(context.localize(MR.strings.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
// Error file format:
// ! Error
// # Source
@@ -462,14 +410,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
return File("")
}
/**
* Defines what should be updated within a service execution.
*/
enum class Target {
CHAPTERS, // Manga chapters
COVERS, // Manga covers
}
companion object {
private const val TAG = "LibraryUpdate"
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
@@ -484,11 +424,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
*/
private const val KEY_CATEGORY = "category"
/**
* Key that defines what should be updated.
*/
private const val KEY_TARGET = "target"
fun cancelAllWorks(context: Context) {
context.workManager.cancelAllWorkByTag(TAG)
}
@@ -534,7 +469,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
fun startNow(
context: Context,
category: Category? = null,
target: Target = Target.CHAPTERS,
): Boolean {
val wm = context.workManager
if (wm.isRunning(TAG)) {
@@ -544,7 +478,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val inputData = workDataOf(
KEY_CATEGORY to category?.id,
KEY_TARGET to target.name,
)
val request = OneTimeWorkRequestBuilder<LibraryUpdateJob>()
.addTag(TAG)
@@ -26,9 +26,12 @@ import eu.kanade.tachiyomi.util.system.getBitmapOrNull
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.Constants
import tachiyomi.core.i18n.localize
import tachiyomi.core.i18n.localizePlural
import tachiyomi.core.util.lang.launchUI
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.text.NumberFormat
@@ -58,12 +61,12 @@ class LibraryUpdateNotifier(private val context: Context) {
*/
val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
setContentTitle(context.getString(R.string.app_name))
setContentTitle(context.localize(MR.strings.app_name))
setSmallIcon(R.drawable.ic_refresh_24dp)
setLargeIcon(notificationBitmap)
setOngoing(true)
setOnlyAlertOnce(true)
addAction(R.drawable.ic_close_24dp, context.getString(R.string.action_cancel), cancelIntent)
addAction(R.drawable.ic_close_24dp, context.localize(MR.strings.action_cancel), cancelIntent)
}
}
@@ -77,14 +80,14 @@ class LibraryUpdateNotifier(private val context: Context) {
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
if (preferences.hideNotificationContent().get()) {
progressNotificationBuilder
.setContentTitle(context.getString(R.string.notification_check_updates))
.setContentTitle(context.localize(MR.strings.notification_check_updates))
.setContentText("($current/$total)")
} else {
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder
.setContentTitle(
context.getString(
R.string.notification_updating_progress,
context.localize(
MR.strings.notification_updating_progress,
percentFormatter.format(current.toFloat() / total),
),
)
@@ -104,8 +107,8 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.ID_LIBRARY_SIZE_WARNING,
Notifications.CHANNEL_LIBRARY_PROGRESS,
) {
setContentTitle(context.getString(R.string.label_warning))
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_size_warning)))
setContentTitle(context.localize(MR.strings.label_warning))
setStyle(NotificationCompat.BigTextStyle().bigText(context.localize(MR.strings.notification_size_warning)))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
@@ -127,8 +130,8 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.ID_LIBRARY_ERROR,
Notifications.CHANNEL_LIBRARY_ERROR,
) {
setContentTitle(context.resources.getString(R.string.notification_update_error, failed))
setContentText(context.getString(R.string.action_show_errors))
setContentTitle(context.localize(MR.strings.notification_update_error, failed))
setContentText(context.localize(MR.strings.action_show_errors))
setSmallIcon(R.drawable.ic_tachi)
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
@@ -149,8 +152,8 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.ID_LIBRARY_SKIPPED,
Notifications.CHANNEL_LIBRARY_SKIPPED,
) {
setContentTitle(context.resources.getString(R.string.notification_update_skipped, skipped))
setContentText(context.getString(R.string.learn_more))
setContentTitle(context.localize(MR.strings.notification_update_skipped, skipped))
setContentText(context.localize(MR.strings.learn_more))
setSmallIcon(R.drawable.ic_tachi)
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
}
@@ -167,13 +170,13 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.ID_NEW_CHAPTERS,
Notifications.CHANNEL_NEW_CHAPTERS,
) {
setContentTitle(context.getString(R.string.notification_new_chapters))
setContentTitle(context.localize(MR.strings.notification_new_chapters))
if (updates.size == 1 && !preferences.hideNotificationContent().get()) {
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
} else {
setContentText(
context.resources.getQuantityString(
R.plurals.notification_new_chapters_summary,
context.localizePlural(
MR.plurals.notification_new_chapters_summary,
updates.size,
updates.size,
),
@@ -243,7 +246,7 @@ class LibraryUpdateNotifier(private val context: Context) {
// Mark chapters as read action
addAction(
R.drawable.ic_glasses_24dp,
context.getString(R.string.action_mark_as_read),
context.localize(MR.strings.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
context,
manga,
@@ -254,7 +257,7 @@ class LibraryUpdateNotifier(private val context: Context) {
// View chapters action
addAction(
R.drawable.ic_book_24dp,
context.getString(R.string.action_view_chapters),
context.localize(MR.strings.action_view_chapters),
NotificationReceiver.openChapterPendingActivity(
context,
manga,
@@ -266,7 +269,7 @@ class LibraryUpdateNotifier(private val context: Context) {
if (chapters.size <= Downloader.CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
context.localize(MR.strings.action_download),
NotificationReceiver.downloadChaptersPendingBroadcast(
context,
manga,
@@ -306,8 +309,8 @@ class LibraryUpdateNotifier(private val context: Context) {
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
0 -> {
// "1 new chapter" or "5 new chapters"
context.resources.getQuantityString(
R.plurals.notification_chapters_generic,
context.localizePlural(
MR.plurals.notification_chapters_generic,
chapters.size,
chapters.size,
)
@@ -317,14 +320,14 @@ class LibraryUpdateNotifier(private val context: Context) {
val remaining = chapters.size - displayableChapterNumbers.size
if (remaining == 0) {
// "Chapter 2.5"
context.resources.getString(
R.string.notification_chapters_single,
context.localize(
MR.strings.notification_chapters_single,
displayableChapterNumbers.first(),
)
} else {
// "Chapter 2.5 and 10 more"
context.resources.getString(
R.string.notification_chapters_single_and_more,
context.localize(
MR.strings.notification_chapters_single_and_more,
displayableChapterNumbers.first(),
remaining,
)
@@ -339,16 +342,16 @@ class LibraryUpdateNotifier(private val context: Context) {
val joinedChapterNumbers = displayableChapterNumbers
.take(NOTIF_MAX_CHAPTERS)
.joinToString(", ")
context.resources.getQuantityString(
R.plurals.notification_chapters_multiple_and_more,
context.localizePlural(
MR.plurals.notification_chapters_multiple_and_more,
remaining,
joinedChapterNumbers,
remaining,
)
} else {
// "Chapters 1, 2.5, 3"
context.resources.getString(
R.string.notification_chapters_multiple,
context.localize(
MR.strings.notification_chapters_multiple,
displayableChapterNumbers.joinToString(", "),
)
}
@@ -0,0 +1,210 @@
package eu.kanade.tachiyomi.data.library
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get()
private val notifier = LibraryUpdateNotifier(context)
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
override suspend fun doWork(): Result {
try {
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
}
addMangaToQueue()
return withIOContext {
try {
updateMetadata()
Result.success()
} catch (e: Exception) {
if (e is CancellationException) {
// Assume success although cancelled
Result.success()
} else {
logcat(LogPriority.ERROR, e)
Result.failure()
}
} finally {
notifier.cancelProgressNotification()
}
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notifier = LibraryUpdateNotifier(context)
return ForegroundInfo(
Notifications.ID_LIBRARY_PROGRESS,
notifier.progressNotificationBuilder.build(),
)
}
/**
* Adds list of manga to be updated.
*/
private fun addMangaToQueue() {
mangaToUpdate = runBlocking { getLibraryManga.await() }
// Warn when excessively checking a single source
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
notifier.showQueueSizeWarningNotification()
}
}
private suspend fun updateMetadata() {
val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope {
mangaToUpdate.groupBy { it.manga.source }
.values
.map { mangaInSource ->
async {
semaphore.withPermit {
mangaInSource.forEach { libraryManga ->
val manga = libraryManga.manga
ensureActive()
withUpdateNotification(
currentlyUpdatingManga,
progressCount,
manga,
) {
val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
try {
val networkManga = source.getMangaDetails(manga.toSManga())
val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true)
.copyFrom(networkManga)
try {
updateManga.await(updatedManga.toMangaUpdate())
} catch (e: Exception) {
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
}
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
}
}
}
}
.awaitAll()
}
notifier.cancelProgressNotification()
}
private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger,
manga: Manga,
block: suspend () -> Unit,
) = coroutineScope {
ensureActive()
updatingManga.add(manga)
notifier.showProgressNotification(
updatingManga,
completed.get(),
mangaToUpdate.size,
)
block()
ensureActive()
updatingManga.remove(manga)
completed.getAndIncrement()
notifier.showProgressNotification(
updatingManga,
completed.get(),
mangaToUpdate.size,
)
}
companion object {
private const val TAG = "MetadataUpdate"
private const val WORK_NAME_MANUAL = "MetadataUpdate"
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
fun startNow(context: Context): Boolean {
val wm = context.workManager
if (wm.isRunning(TAG)) {
// Already running either as a scheduled or manual job
return false
}
val request = OneTimeWorkRequestBuilder<MetadataUpdateJob>()
.addTag(TAG)
.addTag(WORK_NAME_MANUAL)
.build()
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
return true
}
fun stop(context: Context) {
val wm = context.workManager
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG))
.addStates(listOf(WorkInfo.State.RUNNING))
.build()
wm.getWorkInfos(workQuery).get()
// Should only return one work but just in case
.forEach {
wm.cancelWorkById(it.id)
}
}
}
}