Merge branch 'master' into aniyomi-mpv

This commit is contained in:
jmir1 2022-03-28 23:25:06 +02:00
commit ae8dc156d0
27 changed files with 106 additions and 70 deletions

View file

@ -105,7 +105,7 @@ class AnimelibUpdateNotifier(private val context: Context) {
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
setContentText(context.getString(R.string.action_show_errors))
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
}
@ -145,7 +145,7 @@ class AnimelibUpdateNotifier(private val context: Context) {
}
}
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setLargeIcon(notificationBitmap)
setGroup(Notifications.GROUP_NEW_CHAPTERS)
@ -178,7 +178,7 @@ class AnimelibUpdateNotifier(private val context: Context) {
setContentText(description)
setStyle(NotificationCompat.BigTextStyle().bigText(description))
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
if (icon != null) {
setLargeIcon(icon)

View file

@ -21,7 +21,7 @@ class BackupNotifier(private val context: Context) {
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
@ -29,7 +29,7 @@ class BackupNotifier(private val context: Context) {
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setAutoCancel(false)
}

View file

@ -9,14 +9,12 @@ import eu.kanade.tachiyomi.data.database.mappers.AnimeTrackTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.AnimeTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.EpisodeTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.AnimeCategory
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
import eu.kanade.tachiyomi.data.database.models.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.queries.AnimeCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.AnimeHistoryQueries
import eu.kanade.tachiyomi.data.database.queries.AnimeQueries
@ -43,7 +41,6 @@ open class AnimeDatabaseHelper(context: Context) :
.addTypeMapping(AnimeTrack::class.java, AnimeTrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(AnimeCategory::class.java, AnimeCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.addTypeMapping(AnimeHistory::class.java, AnimeHistoryTypeMapping())
.build()

View file

@ -100,7 +100,7 @@ class AnimeDownloadCache(
val animeDir = sourceDir.files[provider.getAnimeDirName(anime)]
if (animeDir != null) {
return animeDir.files
.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
.filter { !it.endsWith(AnimeDownloader.TMP_DIR_SUFFIX) }
.size
}
}

View file

@ -109,10 +109,10 @@ class AnimeDownloadManager(
queue.add(0, download)
reorderQueue(queue)
if (isPaused()) {
if (DownloadService.isRunning(context)) {
if (AnimeDownloadService.isRunning(context)) {
downloader.start()
} else {
DownloadService.start(context)
AnimeDownloadService.start(context)
}
}
}

View file

@ -336,7 +336,7 @@ class AnimeDownloader(
download.source.fetchVideoList(download.episode).map { it.first() }
.doOnNext { video ->
if (video == null) {
throw Exception(context.getString(R.string.page_list_empty_error))
throw Exception(context.getString(R.string.video_list_empty_error))
}
download.video = video
}

View file

@ -105,7 +105,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
setContentText(context.getString(R.string.action_show_errors))
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
}
@ -145,7 +145,7 @@ class LibraryUpdateNotifier(private val context: Context) {
}
}
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setLargeIcon(notificationBitmap)
setGroup(Notifications.GROUP_NEW_CHAPTERS)
@ -178,7 +178,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentText(description)
setStyle(NotificationCompat.BigTextStyle().bigText(description))
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
if (icon != null) {
setLargeIcon(icon)

View file

@ -40,7 +40,7 @@ internal class AnimeExtensionGithubApi {
suspend fun checkForUpdates(context: Context): List<AnimeExtension.Installed> {
// Limit checks to once a day at most
if (Date().time < preferences.lastExtCheck().get() + TimeUnit.DAYS.toMillis(1)) {
if (Date().time < preferences.lastAnimeExtCheck().get() + TimeUnit.DAYS.toMillis(1)) {
return emptyList()
}

View file

@ -40,7 +40,7 @@ class AnimeExtensionInstallActivity : Activity() {
}
private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
val downloadId = intent.extras!!.getLong(AnimeExtensionInstaller.EXTRA_DOWNLOAD_ID)
val extensionManager = Injekt.get<AnimeExtensionManager>()
val newStep = when (resultCode) {
RESULT_OK -> InstallStep.Installed

View file

@ -23,7 +23,7 @@ class AnimeExtensionInstallService : Service() {
override fun onCreate() {
super.onCreate()
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setAutoCancel(false)
setOngoing(true)
setShowWhen(false)

View file

@ -23,7 +23,7 @@ class ExtensionInstallService : Service() {
override fun onCreate() {
super.onCreate()
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
setAutoCancel(false)
setOngoing(true)
setShowWhen(false)

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.anime.episode
import android.text.SpannableStringBuilder
import android.text.SpannedString
import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.color
@ -64,28 +65,57 @@ class EpisodeHolder(
descriptions.add(Date(episode.date_upload).toRelativeString(itemView.context, adapter.relativeTime, adapter.dateFormat))
}
if (!episode.seen && episode.last_second_seen > 0) {
val lastPageRead = buildSpannedString {
color(adapter.readColor) {
append(
itemView.context.getString(
R.string.episode_progress,
String.format(
"%d:%02d",
TimeUnit.MILLISECONDS.toMinutes(episode.last_second_seen),
TimeUnit.MILLISECONDS.toSeconds(episode.last_second_seen) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(episode.last_second_seen))
),
String.format(
"%d:%02d",
TimeUnit.MILLISECONDS.toMinutes(episode.total_seconds),
TimeUnit.MILLISECONDS.toSeconds(episode.total_seconds) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(episode.total_seconds))
val lastSecondSeen: SpannedString
if (episode.total_seconds > 3600000) {
lastSecondSeen = buildSpannedString {
color(adapter.readColor) {
append(
itemView.context.getString(
R.string.episode_progress,
String.format(
"%d:%02d:%02d",
TimeUnit.MILLISECONDS.toHours(episode.last_second_seen),
TimeUnit.MILLISECONDS.toMinutes(episode.last_second_seen) -
TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(episode.last_second_seen)),
TimeUnit.MILLISECONDS.toSeconds(episode.last_second_seen) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(episode.last_second_seen))
),
String.format(
"%d:%02d:%02d",
TimeUnit.MILLISECONDS.toHours(episode.total_seconds),
TimeUnit.MILLISECONDS.toMinutes(episode.total_seconds) -
TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(episode.total_seconds)),
TimeUnit.MILLISECONDS.toSeconds(episode.total_seconds) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(episode.total_seconds))
)
)
)
)
}
}
} else {
lastSecondSeen = buildSpannedString {
color(adapter.readColor) {
append(
itemView.context.getString(
R.string.episode_progress,
String.format(
"%d:%02d",
TimeUnit.MILLISECONDS.toMinutes(episode.last_second_seen),
TimeUnit.MILLISECONDS.toSeconds(episode.last_second_seen) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(episode.last_second_seen))
),
String.format(
"%d:%02d",
TimeUnit.MILLISECONDS.toMinutes(episode.total_seconds),
TimeUnit.MILLISECONDS.toSeconds(episode.total_seconds) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(episode.total_seconds))
)
)
)
}
}
}
descriptions.add(lastPageRead)
descriptions.add(lastSecondSeen)
}
if (!episode.scanlator.isNullOrBlank()) {
descriptions.add(episode.scanlator!!)

View file

@ -152,7 +152,7 @@ class AnimeExtensionDetailsController(bundle: Bundle? = null) :
}
// React to enable/disable all changes
preferences.disabledSources().asFlow()
preferences.disabledAnimeSources().asFlow()
.onEach {
val enabled = source.isEnabled()
isChecked = enabled

View file

@ -55,7 +55,7 @@ class AnimeExtensionDetailsHeaderAdapter(private val presenter: AnimeExtensionDe
if (extension.isUnofficial) {
binding.warningBanner.isVisible = true
binding.warningBanner.setText(R.string.unofficial_extension_message)
binding.warningBanner.setText(R.string.unofficial_extension_message_aniyomi)
}
}
}

View file

@ -151,7 +151,7 @@ class AnimeSourceController :
}
private fun toggleSourcePin(source: AnimeSource) {
val isPinned = source.id.toString() in preferences.pinnedSources().get()
val isPinned = source.id.toString() in preferences.pinnedAnimeSources().get()
if (isPinned) {
preferences.pinnedAnimeSources() -= source.id.toString()
} else {

View file

@ -52,7 +52,7 @@ class AnimeSourcePresenter(
sourceSubscription?.unsubscribe()
val pinnedSources = mutableListOf<AnimeSourceItem>()
val pinnedSourceIds = preferences.pinnedSources().get()
val pinnedSourceIds = preferences.pinnedAnimeSources().get()
val map = TreeMap<String, MutableList<AnimeCatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
@ -85,10 +85,10 @@ class AnimeSourcePresenter(
private fun loadLastUsedSource() {
// Immediate initial load
preferences.lastUsedSource().get().let { updateLastUsedSource(it) }
preferences.lastUsedAnimeSource().get().let { updateLastUsedSource(it) }
// Subsequent updates
preferences.lastUsedSource().asFlow()
preferences.lastUsedAnimeSource().asFlow()
.drop(1)
.onStart { delay(500) }
.distinctUntilChanged()
@ -98,7 +98,7 @@ class AnimeSourcePresenter(
private fun updateLastUsedSource(sourceId: Long) {
val source = (sourceManager.get(sourceId) as? AnimeCatalogueSource)?.let {
val isPinned = it.id.toString() in preferences.pinnedSources().get()
val isPinned = it.id.toString() in preferences.pinnedAnimeSources().get()
AnimeSourceItem(it, null, isPinned)
}
source?.let { view?.setLastUsedSource(it) }

View file

@ -210,7 +210,7 @@ open class GlobalAnimeSearchController(
*/
override fun onTitleClick(source: AnimeCatalogueSource) {
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
preferences.lastUsedAnimeSource().set(source.id)
}
router.pushController(BrowseAnimeSourceController(source, presenter.query).withFadeTransaction())
}

View file

@ -55,7 +55,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
if (extension.isUnofficial) {
binding.warningBanner.isVisible = true
binding.warningBanner.setText(R.string.unofficial_extension_message)
binding.warningBanner.setText(R.string.unofficial_extension_message_tachiyomi)
}
}
}

View file

@ -113,6 +113,7 @@ class ExternalIntents(val anime: Anime, val source: AnimeSource) {
MPV_PLAYER -> ComponentName(packageName, "$packageName.MPVActivity")
MX_PLAYER_FREE, MX_PLAYER_PRO -> ComponentName(packageName, "$packageName.ActivityScreen")
VLC_PLAYER -> ComponentName(packageName, "$packageName.gui.video.VideoPlayerActivity")
MPV_REMOTE -> ComponentName(packageName, "$packageName.MainActivity")
else -> null
}
}
@ -131,3 +132,4 @@ private const val MPV_PLAYER = "is.xyz.mpv"
private const val MX_PLAYER_FREE = "com.mxtech.videoplayer.ad"
private const val MX_PLAYER_PRO = "com.mxtech.videoplayer.pro"
private const val VLC_PLAYER = "org.videolan.vlc"
private const val MPV_REMOTE = "com.husudosu.mpvremote"

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.AnimeExtensionUpdateJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.defaultValue
@ -31,6 +32,7 @@ class SettingsBrowseController : SettingsController() {
onChange { newValue ->
val checked = newValue as Boolean
ExtensionUpdateJob.setupTask(activity!!, checked)
AnimeExtensionUpdateJob.setupTask(activity!!, checked)
true
}
}

View file

@ -91,6 +91,7 @@ class SettingsPlayerController : SettingsController() {
"com.mxtech.videoplayer.ad" -> true
"com.mxtech.videoplayer.pro" -> true
"org.videolan.vlc" -> true
"com.husudosu.mpvremote" -> true
else -> false
}
}

View file

@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.util.system.toast
class CrashLogUtil(private val context: Context) {
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_CRASH_LOGS) {
setSmallIcon(R.drawable.ic_tachi)
setSmallIcon(R.drawable.ic_ani)
}
fun dumpLogs() {

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="512"
android:viewportHeight="512"
android:width="256dp"
android:height="256dp">
<path
android:pathData="M239.5 0L273.5 0L274.5 1L292.5 2L325.5 9L346.5 16Q401.6 37.4 439 76.5Q475.6 112.9 496 165.5L503 186.5L510 219.5L511 237.5L512 238.5L512 271.5L511 272.5L511 283.5L510 284.5L509 297.5L505 317.5L491 358.5Q466.8 411.8 425.5 448Q391.1 479.1 343.5 497L321.5 504L297.5 509L291.5 509L283.5 511L272.5 511L271.5 512L240.5 512L239.5 511L220.5 510L194.5 505L155.5 492Q100.1 467.4 63 424.5Q33.6 391.4 16 346.5L8 321.5L2 291.5L1 272.5L0 271.5L0 239.5L1 238.5L1 227.5L2 226.5L3 213.5L8 190.5Q14.7 165.7 25 144.5Q48.9 96.4 87.5 63Q116.9 36.9 155.5 20L198.5 6L227.5 1L238.5 1L239.5 0ZM253 64L252 65L237 65L236 66L218 68L182 79L149 97L130 112Q97 140 79 182L68 218L66 236L65 237L65 252L64 253Q67 260 65 272L66 273L68 293L78 327Q92 361 115 386Q142 415 181 432L215 443L232 445L233 446L240 446L241 447L255 447L256 448L257 447L271 447L272 446L293 444L327 434Q361 420 386 397Q415 370 432 332L443 298L445 281L446 280L446 272L447 271L447 258L448 257L447 255L447 237L445 230L444 218L435 188Q421 152 397 127Q370 96 330 79L314 73L295 68L283 67L276 65L258 65L253 64Z"
android:fillColor="#FFFFFF"
android:strokeWidth="1" />
<path
android:pathData="M206.5 159L213.5 160L222.5 165L342.5 240L350 246.5Q353 250.5 353 257.5Q351.5 266 345.5 270L217.5 350Q213.5 353 206.5 353Q198 351.5 194 345.5L191 337.5L191 174.5Q192.7 166.2 198.5 162L206.5 159Z"
android:fillColor="#FFFFFF"
android:strokeWidth="1" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:fillColor="@android:color/white"
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.4v21.2l2.3,-0.6C23.9,79.7 50.6,79 126,79s102.1,0.7 108.8,2.6l2.2,0.6V61c0,-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.2h-47.6l0.4,10.2zM58.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,-85C82,89.6 80.4,86 79.1,86.1c-0.9,0 -10,3.2 -20.3,7.1z" />
<path
android:fillColor="@android:color/white"
android:pathData="M167.2,93.7c-3.3,21 -15.6,61.6 -28.8,95l-6.9,17.3H6v40h243v-40h-37.1c-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" />
</vector>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="72dp"
android:height="72dp"
android:drawable="@drawable/ic_tachi"
android:width="108dp"
android:height="108dp"
android:drawable="@drawable/ic_ani"
android:gravity="center" />
</layer-list>

View file

@ -9,10 +9,10 @@
android:padding="32dp">
<ImageView
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_tachi"
app:srcCompat="@drawable/ic_ani"
app:tint="?attr/colorOnSurface"
tools:ignore="ContentDescription" />

View file

@ -299,9 +299,10 @@
<string name="ext_uninstall">Uninstall</string>
<string name="ext_app_info">App info</string>
<string name="untrusted_extension">Untrusted extension</string>
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Tachiyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Aniyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
<string name="obsolete_extension_message">This extension is no longer available.</string>
<string name="unofficial_extension_message">This extension is not from the official Tachiyomi extensions list.</string>
<string name="unofficial_extension_message_tachiyomi">This extension is not from the official Tachiyomi extensions list.</string>
<string name="unofficial_extension_message_aniyomi">This extension is not from the official Aniyomi extensions list.</string>
<string name="extension_api_error">Failed to get extensions list</string>
<string name="ext_version_info">Version: %1$s</string>
<string name="ext_language_info">Language: %1$s</string>
@ -814,8 +815,9 @@
<!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi</string>
<string name="download_insufficient_space">Couldn\'t download due to low storage space</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Aniyomi</string>
<string name="video_list_empty_error">No video found</string>
<!-- Library update service notifications -->
<string name="notification_check_updates">Checking for new chapters</string>
@ -898,7 +900,7 @@
<string name="information_empty_category_dialog">You don\'t have any categories yet.</string>
<string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string>
<!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for Tachiyomi</string>
<string name="information_webview_required">WebView is required for Aniyomi</string>
<!-- Do not translate "WebView" -->
<string name="information_webview_outdated">Please update the WebView app for better compatibility</string>
<string name="chapter_settings_updated">Updated default chapter settings</string>