App language change (#1370)

* Add back option to change app langauge

Courtesy of AndroidX/Appcompat
On Android 13, it uses the native methods (at least for now) Also adding locales config file to only list supported languages in android app info's setting

A bit about this for A12 and under, an app restart is needed right now when switching from LTR to RTL or vice versa, not sure if later versions of androidx app compat will change this

* stop using preference context for strings (when possible)

which fixes some of the language issues on a12 and under

* Fixes to timespanfromnow method

now respect set language

* Dont show langauge selector on android 6

it doesn't work so rip

* Filter out unsupported locales in language selector

* appcompat -> 1.6.0-beta01

* Set notifications to use app language

* Add beta tag to language

* Update ExtensionHolder.kt
This commit is contained in:
Jays2Kings 2022-08-17 14:57:47 -04:00 committed by GitHub
parent 0cf6393ad6
commit 30b4b589e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 347 additions and 124 deletions

View file

@ -126,10 +126,10 @@ dependencies {
implementation("tachiyomi.sourceapi:source-api:1.1") implementation("tachiyomi.sourceapi:source-api:1.1")
// Android X libraries // Android X libraries
implementation("androidx.appcompat:appcompat:1.6.0-alpha03") implementation("androidx.appcompat:appcompat:1.6.0-beta01")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("com.google.android.material:material:1.7.0-alpha02") implementation("com.google.android.material:material:1.7.0-alpha02")
implementation("androidx.webkit:webkit:1.4.0") implementation("androidx.webkit:webkit:1.5.0-rc01")
implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.preference:preference:1.2.0") implementation("androidx.preference:preference:1.2.0")
implementation("androidx.annotation:annotation:1.4.0") implementation("androidx.annotation:annotation:1.4.0")

View file

@ -36,6 +36,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:theme="@style/Theme.Tachiyomi" android:theme="@style/Theme.Tachiyomi"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
@ -239,6 +240,15 @@
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false"/> android:exported="false"/>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<provider <provider
android:name="rikka.shizuku.ShizukuProvider" android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku" android:authorities="${applicationId}.shizuku"

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.source.SourcePresenter import eu.kanade.tachiyomi.ui.source.SourcePresenter
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -90,10 +91,11 @@ open class App : Application(), DefaultLifecycleObserver {
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
if (enabled) { if (enabled) {
disableIncognitoReceiver.register() disableIncognitoReceiver.register()
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) { val nContext = localeContext
val incogText = getString(R.string.incognito_mode) val notification = nContext.notification(Notifications.CHANNEL_INCOGNITO_MODE) {
val incogText = nContext.getString(R.string.incognito_mode)
setContentTitle(incogText) setContentTitle(incogText)
setContentText(getString(R.string.turn_off_, incogText)) setContentText(nContext.getString(R.string.turn_off_, incogText))
setSmallIcon(R.drawable.ic_incognito_24dp) setSmallIcon(R.drawable.ic_incognito_24dp)
setOngoing(true) setOngoing(true)

View file

@ -16,6 +16,7 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications 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.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -27,7 +28,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val notifier = BackupNotifier(context) val notifier = BackupNotifier(context.localeContext)
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) } val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
?: preferences.backupsDirectory().get().toUri() ?: preferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL) val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.localeContext
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -61,7 +62,7 @@ class BackupRestoreService : Service() {
fun stop(context: Context) { fun stop(context: Context) {
context.stopService(Intent(context, BackupRestoreService::class.java)) context.stopService(Intent(context, BackupRestoreService::class.java))
BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled)) BackupNotifier(context.localeContext).showRestoreError(context.getString(R.string.restoring_backup_canceled))
} }
} }
@ -78,7 +79,7 @@ class BackupRestoreService : Service() {
super.onCreate() super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = BackupNotifier(this) notifier = BackupNotifier(this.localeContext)
wakeLock = acquireWakeLock() wakeLock = acquireWakeLock()
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build()) startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications 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.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -69,6 +70,7 @@ internal class DownloadNotifier(private val context: Context) {
} }
fun setPlaceholder(download: Download?) { fun setPlaceholder(download: Download?) {
val context = context.localeContext
with(notification) { with(notification) {
// Check if first call. // Check if first call.
if (!isDownloading) { if (!isDownloading) {
@ -140,7 +142,7 @@ internal class DownloadNotifier(private val context: Context) {
} }
val downloadingProgressText = val downloadingProgressText =
context.getString(R.string.downloading_progress) context.localeContext.getString(R.string.downloading_progress)
.format(download.downloadedImages, download.pages!!.size) .format(download.downloadedImages, download.pages!!.size)
if (preferences.hideNotificationContent()) { if (preferences.hideNotificationContent()) {
@ -166,6 +168,7 @@ internal class DownloadNotifier(private val context: Context) {
* Show notification when download is paused. * Show notification when download is paused.
*/ */
fun onDownloadPaused() { fun onDownloadPaused() {
val context = context.localeContext
with(notification) { with(notification) {
setContentTitle(context.getString(R.string.paused)) setContentTitle(context.getString(R.string.paused))
setContentText(context.getString(R.string.download_paused)) setContentText(context.getString(R.string.download_paused))
@ -203,6 +206,7 @@ internal class DownloadNotifier(private val context: Context) {
* @param reason the text to show. * @param reason the text to show.
*/ */
fun onWarning(reason: String) { fun onWarning(reason: String) {
val context = context.localeContext
with(notification) { with(notification) {
setContentTitle(context.getString(R.string.downloads)) setContentTitle(context.getString(R.string.downloads))
setContentText(reason) setContentText(reason)
@ -223,6 +227,7 @@ internal class DownloadNotifier(private val context: Context) {
* Called when the downloader has too many downloads from one source. * Called when the downloader has too many downloads from one source.
*/ */
fun massDownloadWarning() { fun massDownloadWarning() {
val context = context.localeContext
val notification = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) { val notification = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
setContentTitle(context.getString(R.string.warning)) setContentTitle(context.getString(R.string.warning))
setSmallIcon(R.drawable.ic_warning_white_24dp) setSmallIcon(R.drawable.ic_warning_white_24dp)
@ -260,6 +265,7 @@ internal class DownloadNotifier(private val context: Context) {
customIntent: Intent? = null, customIntent: Intent? = null,
) { ) {
// Create notification // Create notification
val context = context.localeContext
with(notification) { with(notification) {
setContentTitle( setContentTitle(
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_error), mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_error),

View file

@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -241,7 +242,7 @@ class DownloadService : Service() {
private fun getPlaceholderNotification(): Notification { private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER) return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
.setContentTitle(getString(R.string.downloading)) .setContentTitle(localeContext.getString(R.string.downloading))
.build() .build()
} }
} }

View file

@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.localeContext
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -170,7 +171,7 @@ class LibraryUpdateService(
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notifier = LibraryUpdateNotifier(this) notifier = LibraryUpdateNotifier(this.localeContext)
wakeLock = acquireWakeLock(timeout = TimeUnit.MINUTES.toMillis(30)) wakeLock = acquireWakeLock(timeout = TimeUnit.MINUTES.toMillis(30))
startForeground(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build()) startForeground(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build())
} }
@ -380,7 +381,7 @@ class LibraryUpdateService(
val errorFile = writeErrorFile(failedUpdates).getUriCompat(this) val errorFile = writeErrorFile(failedUpdates).getUriCompat(this)
notifier.showUpdateErrorNotification(failedUpdates.map { it.key.title }, errorFile) notifier.showUpdateErrorNotification(failedUpdates.map { it.key.title }, errorFile)
} }
mangaShortcutManager.updateShortcuts() mangaShortcutManager.updateShortcuts(this)
failedUpdates.clear() failedUpdates.clear()
notifier.cancelProgressNotification() notifier.cancelProgressNotification()
if (runExtensionUpdatesAfter && !DownloadService.isRunning(this)) { if (runExtensionUpdatesAfter && !DownloadService.isRunning(this)) {

View file

@ -237,6 +237,8 @@ class PreferencesHelper(val context: Context) {
else -> SimpleDateFormat(format, Locale.getDefault()) else -> SimpleDateFormat(format, Locale.getDefault())
} }
fun appLanguage() = flowPrefs.getString("app_language", "")
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString()) fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true) fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)

View file

@ -8,6 +8,7 @@ import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
class AppUpdateBroadcast : BroadcastReceiver() { class AppUpdateBroadcast : BroadcastReceiver() {
@ -27,7 +28,7 @@ class AppUpdateBroadcast : BroadcastReceiver() {
val notifyOnInstall = extras.getBoolean(AppUpdateService.EXTRA_NOTIFY_ON_INSTALL, false) val notifyOnInstall = extras.getBoolean(AppUpdateService.EXTRA_NOTIFY_ON_INSTALL, false)
try { try {
if (notifyOnInstall) { if (notifyOnInstall) {
AppUpdateNotifier(context).onInstallFinished() AppUpdateNotifier(context.localeContext).onInstallFinished()
} }
} finally { } finally {
AppUpdateService.stop(context) AppUpdateService.stop(context)
@ -37,7 +38,7 @@ class AppUpdateBroadcast : BroadcastReceiver() {
if (status != PackageInstaller.STATUS_FAILURE_ABORTED) { if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
context.toast(R.string.could_not_install_update) context.toast(R.string.could_not_install_update)
val uri = intent.getStringExtra(AppUpdateService.EXTRA_FILE_URI) ?: return val uri = intent.getStringExtra(AppUpdateService.EXTRA_FILE_URI) ?: return
AppUpdateNotifier(context).onInstallError(uri.toUri()) AppUpdateNotifier(context.localeContext).onInstallError(uri.toUri())
} }
} }
} }
@ -48,7 +49,7 @@ class AppUpdateBroadcast : BroadcastReceiver() {
remove(AppUpdateService.NOTIFY_ON_INSTALL_KEY) remove(AppUpdateService.NOTIFY_ON_INSTALL_KEY)
} }
if (notifyOnInstall) { if (notifyOnInstall) {
AppUpdateNotifier(context).onInstallFinished() AppUpdateNotifier(context.localeContext).onInstallFinished()
} }
} }
} }

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withIOContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date import java.util.Date
@ -63,7 +64,7 @@ class AppUpdateChecker {
) { ) {
AutoAppUpdaterJob.setupTask(context) AutoAppUpdaterJob.setupTask(context)
} }
AppUpdateNotifier(context).promptUpdate(result.release) AppUpdateNotifier(context.localeContext).promptUpdate(result.release)
} }
result result

View file

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@ -52,7 +53,7 @@ class AppUpdateService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notifier = AppUpdateNotifier(this) notifier = AppUpdateNotifier(this.localeContext)
startForeground(Notifications.ID_UPDATER, notifier.onDownloadStarted(getString(R.string.app_name)).build()) startForeground(Notifications.ID_UPDATER, notifier.onDownloadStarted(getString(R.string.app_name)).build())

View file

@ -11,6 +11,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.localeContext
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -32,7 +33,7 @@ class AutoAppUpdaterJob(private val context: Context, workerParams: WorkerParame
} }
val result = AppUpdateChecker().checkForUpdate(context, true, doExtrasAfterNewUpdate = false) val result = AppUpdateChecker().checkForUpdate(context, true, doExtrasAfterNewUpdate = false)
if (result is AppUpdateResult.NewUpdate && !AppUpdateService.isRunning()) { if (result is AppUpdateResult.NewUpdate && !AppUpdateService.isRunning()) {
AppUpdateNotifier(context).cancel() AppUpdateNotifier(context.localeContext).cancel()
AppUpdateNotifier.releasePageUrl = result.release.releaseLink AppUpdateNotifier.releasePageUrl = result.release.releaseLink
AppUpdateService.start(context, result.release.downloadLink, false) AppUpdateService.start(context, result.release.downloadLink, false)
} }

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.localeContext
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -140,7 +141,7 @@ class ExtensionInstallService(
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS) notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS)
notifier = ExtensionInstallNotifier(this) notifier = ExtensionInstallNotifier(this.localeContext)
wakeLock = acquireWakeLock(timeout = TimeUnit.MINUTES.toMillis(30)) wakeLock = acquireWakeLock(timeout = TimeUnit.MINUTES.toMillis(30))
startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build()) startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build())
} }

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku
@ -110,6 +111,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
notify( notify(
Notifications.ID_UPDATES_TO_EXTS, Notifications.ID_UPDATES_TO_EXTS,
context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) { context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) {
val context = context.localeContext
setContentTitle( setContentTitle(
context.resources.getQuantityString( context.resources.getQuantityString(
R.plurals.extension_updates_available, R.plurals.extension_updates_available,

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.SearchActivity import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.getThemeWithExtras import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref import eu.kanade.tachiyomi.util.system.setThemeByPref
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -20,6 +21,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
private var updatedTheme: Resources.Theme? = null private var updatedTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setLocaleByAppCompat()
updatedTheme = null updatedTheme = null
setThemeByPref(preferences) setThemeByPref(preferences)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.getThemeWithExtras import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref import eu.kanade.tachiyomi.util.system.setThemeByPref
import nucleus.view.NucleusAppCompatActivity import nucleus.view.NucleusAppCompatActivity
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -18,6 +19,7 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
private var updatedTheme: Resources.Theme? = null private var updatedTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setLocaleByAppCompat()
updatedTheme = null updatedTheme = null
setThemeByPref(preferences) setThemeByPref(preferences)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -5,6 +5,7 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.getThemeWithExtras import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref import eu.kanade.tachiyomi.util.system.setThemeByPref
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -14,6 +15,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
private var updatedTheme: Resources.Theme? = null private var updatedTheme: Resources.Theme? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setLocaleByAppCompat()
updatedTheme = null updatedTheme = null
setThemeByPref(preferences) setThemeByPref(preferences)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.category
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
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -20,11 +19,8 @@ import uy.kohesive.injekt.api.get
class CategoryPresenter( class CategoryPresenter(
private val controller: CategoryController, private val controller: CategoryController,
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
preferences: PreferencesHelper = Injekt.get(),
) { ) {
private val context = preferences.context
private var scope = CoroutineScope(Job() + Dispatchers.Default) private var scope = CoroutineScope(Job() + Dispatchers.Default)
/** /**
@ -51,7 +47,8 @@ class CategoryPresenter(
} }
private fun newCategory(): Category { private fun newCategory(): Category {
val default = Category.create(context.getString(R.string.create_new_category)) val default =
Category.create(controller.view?.context?.getString(R.string.create_new_category) ?: "")
default.order = CREATE_CATEGORY_ORDER default.order = CREATE_CATEGORY_ORDER
default.id = Int.MIN_VALUE default.id = Int.MIN_VALUE
return default return default

View file

@ -49,20 +49,15 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
InstalledExtensionsOrder.RecentlyUpdated -> { InstalledExtensionsOrder.RecentlyUpdated -> {
extensionUpdateDate(extension.pkgName)?.let { extensionUpdateDate(extension.pkgName)?.let {
binding.date.isVisible = true binding.date.isVisible = true
binding.date.text = itemView.context.getString( binding.date.text = itemView.context.timeSpanFromNow(R.string.updated_, it)
R.string.updated_,
it.timeSpanFromNow,
)
infoText.add("") infoText.add("")
} }
} }
InstalledExtensionsOrder.RecentlyInstalled -> { InstalledExtensionsOrder.RecentlyInstalled -> {
extensionInstallDate(extension.pkgName)?.let { extensionInstallDate(extension.pkgName)?.let {
binding.date.isVisible = true binding.date.isVisible = true
binding.date.text = itemView.context.getString( binding.date.text =
R.string.installed_, itemView.context.timeSpanFromNow(R.string.installed_, it)
it.timeSpanFromNow,
)
infoText.add("") infoText.add("")
} }
} }

View file

@ -176,7 +176,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
override fun onCreateBubbleText(position: Int): String { override fun onCreateBubbleText(position: Int): String {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom) val context = recyclerView.context
if (position == itemCount - 1) return context.getString(R.string.bottom)
return when (val item: IFlexible<*>? = getItem(position)) { return when (val item: IFlexible<*>? = getItem(position)) {
is LibraryHeaderItem -> { is LibraryHeaderItem -> {
vibrateOnCategoryChange(item.category.name) vibrateOnCategoryChange(item.category.name)
@ -188,7 +189,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
LibrarySort.DragAndDrop -> { LibrarySort.DragAndDrop -> {
if (item.header.category.isDynamic) { if (item.header.category.isDynamic) {
val category = db.getCategoriesForManga(item.manga).executeAsBlocking().firstOrNull()?.name val category = db.getCategoriesForManga(item.manga).executeAsBlocking().firstOrNull()?.name
category ?: recyclerView.context.getString(R.string.default_value) category ?: context.getString(R.string.default_value)
} else { } else {
val title = item.manga.title val title = item.manga.title
if (preferences.removeArticles().get()) title.removeArticles().chop(15) if (preferences.removeArticles().get()) title.removeArticles().chop(15)
@ -200,10 +201,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
val history = db.getChapters(id).executeAsBlocking() val history = db.getChapters(id).executeAsBlocking()
val last = history.maxOfOrNull { it.date_fetch } val last = history.maxOfOrNull { it.date_fetch }
if (last != null && last > 100) { if (last != null && last > 100) {
recyclerView.context.getString( context.timeSpanFromNow(R.string.fetched_, last)
R.string.fetched_,
last.timeSpanFromNow(preferences.context),
)
} else { } else {
"N/A" "N/A"
} }
@ -213,18 +211,15 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
val history = db.getHistoryByMangaId(id).executeAsBlocking() val history = db.getHistoryByMangaId(id).executeAsBlocking()
val last = history.maxOfOrNull { it.last_read } val last = history.maxOfOrNull { it.last_read }
if (last != null && last > 100) { if (last != null && last > 100) {
recyclerView.context.getString( context.timeSpanFromNow(R.string.read_, last)
R.string.read_,
last.timeSpanFromNow(preferences.context),
)
} else { } else {
"N/A" "N/A"
} }
} }
LibrarySort.Unread -> { LibrarySort.Unread -> {
val unread = item.manga.unread val unread = item.manga.unread
if (unread > 0) recyclerView.context.getString(R.string._unread, unread) if (unread > 0) context.getString(R.string._unread, unread)
else recyclerView.context.getString(R.string.read) else context.getString(R.string.read)
} }
LibrarySort.TotalChapters -> { LibrarySort.TotalChapters -> {
val total = item.manga.totalChapters val total = item.manga.totalChapters
@ -240,10 +235,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
LibrarySort.LatestChapter -> { LibrarySort.LatestChapter -> {
val lastUpdate = item.manga.last_update val lastUpdate = item.manga.last_update
if (lastUpdate > 0) { if (lastUpdate > 0) {
recyclerView.context.getString( context.timeSpanFromNow(R.string.updated_, lastUpdate)
R.string.updated_,
lastUpdate.timeSpanFromNow(preferences.context),
)
} else { } else {
"N/A" "N/A"
} }
@ -251,7 +243,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
LibrarySort.DateAdded -> { LibrarySort.DateAdded -> {
val added = item.manga.date_added val added = item.manga.date_added
if (added > 0) { if (added > 0) {
recyclerView.context.getString(R.string.added_, added.timeSpanFromNow(preferences.context)) context.timeSpanFromNow(R.string.added_, added)
} else { } else {
"N/A" "N/A"
} }

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
@ -29,9 +30,9 @@ import uy.kohesive.injekt.injectLazy
class LibraryItem( class LibraryItem(
val manga: LibraryManga, val manga: LibraryManga,
header: LibraryHeaderItem, header: LibraryHeaderItem,
private val context: Context?,
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
) : ) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1 var downloadCount = -1
var unreadType = 2 var unreadType = 2
@ -172,7 +173,8 @@ class LibraryItem(
private fun containsGenre(tag: String, genres: List<String>?): Boolean { private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true if (tag.trim().isEmpty()) return true
val seriesType by lazy { manga.seriesType(preferences.context, sourceManager) } context ?: return false
val seriesType by lazy { manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) { return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-") val realTag = tag.substringAfter("-")
genres?.find { genres?.find {

View file

@ -66,6 +66,8 @@ class LibraryPresenter(
) : BaseCoroutinePresenter<LibraryController>() { ) : BaseCoroutinePresenter<LibraryController>() {
private val context = preferences.context private val context = preferences.context
private val viewContext
get() = controller?.view?.context
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } } private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
@ -198,6 +200,7 @@ class LibraryPresenter(
LibraryItem( LibraryItem(
LibraryManga.createBlank(id), LibraryManga.createBlank(id),
LibraryHeaderItem({ getCategory(id) }, id), LibraryHeaderItem({ getCategory(id) }, id),
viewContext,
), ),
) )
} }
@ -581,7 +584,7 @@ class LibraryPresenter(
else headerItems[it.category] else headerItems[it.category]
) ?: return@mapNotNull null ) ?: return@mapNotNull null
categorySet.add(it.category) categorySet.add(it.category)
LibraryItem(it, headerItem) LibraryItem(it, headerItem, viewContext)
}.toMutableList() }.toMutableList()
val categoriesHidden = if (forceShowAllCategories) { val categoriesHidden = if (forceShowAllCategories) {
@ -599,7 +602,7 @@ class LibraryPresenter(
) { ) {
val headerItem = headerItems[catId] val headerItem = headerItems[catId]
if (headerItem != null) items.add( if (headerItem != null) items.add(
LibraryItem(LibraryManga.createBlank(catId), headerItem), LibraryItem(LibraryManga.createBlank(catId), headerItem, viewContext),
) )
} else if (catId in categoriesHidden && showAll && categories.size > 1) { } else if (catId in categoriesHidden && showAll && categories.size > 1) {
val mangaToRemove = items.filter { it.manga.category == catId } val mangaToRemove = items.filter { it.manga.category == catId }
@ -611,7 +614,14 @@ class LibraryPresenter(
items.removeAll(mangaToRemove) items.removeAll(mangaToRemove)
val headerItem = headerItems[catId] val headerItem = headerItems[catId]
if (headerItem != null) items.add( if (headerItem != null) items.add(
LibraryItem(LibraryManga.createHide(catId, mergedTitle, mangaToRemove.size), headerItem), LibraryItem(
LibraryManga.createHide(
catId,
mergedTitle,
mangaToRemove.size,
),
headerItem, viewContext,
),
) )
} }
} }
@ -672,7 +682,7 @@ class LibraryPresenter(
} ?: listOf(unknown) } ?: listOf(unknown)
} }
tags.map { tags.map {
LibraryItem(manga, makeOrGetHeader(it)) LibraryItem(manga, makeOrGetHeader(it), viewContext)
} }
} }
BY_TRACK_STATUS -> { BY_TRACK_STATUS -> {
@ -688,9 +698,9 @@ class LibraryPresenter(
service.getStatus(track.status) service.getStatus(track.status)
} }
} else { } else {
context.getString(R.string.not_tracked) controller?.view?.context?.getString(R.string.not_tracked) ?: ""
} }
listOf(LibraryItem(manga, makeOrGetHeader(status))) listOf(LibraryItem(manga, makeOrGetHeader(status), viewContext))
} }
BY_SOURCE -> { BY_SOURCE -> {
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
@ -698,12 +708,13 @@ class LibraryPresenter(
LibraryItem( LibraryItem(
manga, manga,
makeOrGetHeader("${source.name}$sourceSplitter${source.id}"), makeOrGetHeader("${source.name}$sourceSplitter${source.id}"),
viewContext,
), ),
) )
} }
BY_AUTHOR -> { BY_AUTHOR -> {
if (manga.artist.isNullOrBlank() && manga.author.isNullOrBlank()) { if (manga.artist.isNullOrBlank() && manga.author.isNullOrBlank()) {
listOf(LibraryItem(manga, makeOrGetHeader(unknown))) listOf(LibraryItem(manga, makeOrGetHeader(unknown), viewContext))
} else { } else {
listOfNotNull( listOfNotNull(
manga.author.takeUnless { it.isNullOrBlank() }, manga.author.takeUnless { it.isNullOrBlank() },
@ -714,11 +725,11 @@ class LibraryPresenter(
author.ifBlank { null } author.ifBlank { null }
} }
}.flatten().distinct().map { }.flatten().distinct().map {
LibraryItem(manga, makeOrGetHeader(it, true)) LibraryItem(manga, makeOrGetHeader(it, true), viewContext)
} }
} }
} }
else -> listOf(LibraryItem(manga, makeOrGetHeader(mapStatus(manga.status)))) else -> listOf(LibraryItem(manga, makeOrGetHeader(mapStatus(manga.status)), viewContext))
} }
}.flatten().toMutableList() }.flatten().toMutableList()
@ -761,7 +772,11 @@ class LibraryPresenter(
sectionedLibraryItems[catId] = mangaToRemove sectionedLibraryItems[catId] = mangaToRemove
items.removeAll { it.header.catId == catId } items.removeAll { it.header.catId == catId }
if (headerItem != null) items.add( if (headerItem != null) items.add(
LibraryItem(LibraryManga.createHide(catId, mergedTitle, mangaToRemove.size), headerItem), LibraryItem(
LibraryManga.createHide(catId, mergedTitle, mangaToRemove.size),
headerItem,
viewContext,
),
) )
} }
} }

View file

@ -747,7 +747,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
} }
fun saveExtras() { fun saveExtras() {
mangaShortcutManager.updateShortcuts() mangaShortcutManager.updateShortcuts(this)
MangaCoverMetadata.savePrefs() MangaCoverMetadata.savePrefs()
} }

View file

@ -374,7 +374,7 @@ class MangaDetailsPresenter(
.map { it.toModel() }, .map { it.toModel() },
) )
} }
mangaShortcutManager.updateShortcuts() controller?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) }
} }
if (newChapters.second.isNotEmpty()) { if (newChapters.second.isNotEmpty()) {
val removedChaptersId = newChapters.second.map { it.id } val removedChaptersId = newChapters.second.map { it.id }
@ -447,7 +447,7 @@ class MangaDetailsPresenter(
) e.message?.split(": ")?.drop(1) ) e.message?.split(": ")?.drop(1)
?.joinToString(": ") ?.joinToString(": ")
else e.message else e.message
) ?: preferences.context.getString(R.string.unknown_error) ) ?: controller?.view?.context?.getString(R.string.unknown_error) ?: ""
} }
/** /**
@ -636,7 +636,8 @@ class MangaDetailsPresenter(
filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_BOOKMARKED) R.string.bookmarked else null) filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_BOOKMARKED) R.string.bookmarked else null)
filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_NOT_BOOKMARKED) R.string.not_bookmarked else null) filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_NOT_BOOKMARKED) R.string.not_bookmarked else null)
filtersId.add(if (manga.filtered_scanlators?.isNotEmpty() == true) R.string.scanlators else null) filtersId.add(if (manga.filtered_scanlators?.isNotEmpty() == true) R.string.scanlators else null)
return filtersId.filterNotNull().joinToString(", ") { preferences.context.getString(it) } return filtersId.filterNotNull()
.joinToString(", ") { controller?.view?.context?.getString(it) ?: "" }
} }
fun setScanlatorFilter(filteredScanlators: Set<String>) { fun setScanlatorFilter(filteredScanlators: Set<String>) {

View file

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.ui.setting.titleRes
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toTimestampString import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.openInBrowser import eu.kanade.tachiyomi.util.view.openInBrowser
@ -97,7 +98,7 @@ class AboutController : SettingsController() {
onClick { onClick {
activity?.let { activity?.let {
val deviceInfo = CrashLogUtil(it).getDebugInfo() val deviceInfo = CrashLogUtil(it.localeContext).getDebugInfo()
val clipboard = it.getSystemService<ClipboardManager>()!! val clipboard = it.getSystemService<ClipboardManager>()!!
val appInfo = it.getString(R.string.app_info) val appInfo = it.getString(R.string.app_info)
clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo)) clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo))

View file

@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.toInt import eu.kanade.tachiyomi.util.system.toInt
import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -343,14 +344,16 @@ class ReaderPresenter(
return delegatedSource.pageNumber(url)?.minus(1) return delegatedSource.pageNumber(url)?.minus(1)
} }
@Suppress("DEPRECATION")
suspend fun loadChapterURL(url: Uri) { suspend fun loadChapterURL(url: Uri) {
val host = url.host ?: return val host = url.host ?: return
val context = view ?: preferences.context
val delegatedSource = sourceManager.getDelegatedSource(host) ?: error( val delegatedSource = sourceManager.getDelegatedSource(host) ?: error(
preferences.context.getString(R.string.source_not_installed), context.getString(R.string.source_not_installed),
) )
val chapterUrl = delegatedSource.chapterUrl(url) val chapterUrl = delegatedSource.chapterUrl(url)
val sourceId = delegatedSource.delegate?.id ?: error( val sourceId = delegatedSource.delegate?.id ?: error(
preferences.context.getString(R.string.source_not_installed), context.getString(R.string.source_not_installed),
) )
if (chapterUrl != null) { if (chapterUrl != null) {
val dbChapter = db.getChapters(chapterUrl).executeOnIO().find { val dbChapter = db.getChapters(chapterUrl).executeOnIO().find {
@ -395,18 +398,18 @@ class ReaderPresenter(
delegatedSource.delegate!!, delegatedSource.delegate!!,
).first ).first
chapterId = newChapters.find { it.url == chapter.url }?.id chapterId = newChapters.find { it.url == chapter.url }?.id
?: error(preferences.context.getString(R.string.chapter_not_found)) ?: error(context.getString(R.string.chapter_not_found))
} else { } else {
chapter.date_fetch = Date().time chapter.date_fetch = Date().time
chapterId = db.insertChapter(chapter).executeOnIO().insertedId() ?: error( chapterId = db.insertChapter(chapter).executeOnIO().insertedId() ?: error(
preferences.context.getString(R.string.unknown_error), context.getString(R.string.unknown_error),
) )
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
init(manga, chapterId) init(manga, chapterId)
} }
} }
} else error(preferences.context.getString(R.string.unknown_error)) } else error(context.getString(R.string.unknown_error))
} }
/** /**
@ -838,7 +841,7 @@ class ReaderPresenter(
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val notifier = SaveImageNotifier(context) val notifier = SaveImageNotifier(context.localeContext)
notifier.onClear() notifier.onClear()
// Pictures directory. // Pictures directory.
@ -873,7 +876,7 @@ class ReaderPresenter(
val manga = manga ?: return@launch val manga = manga ?: return@launch
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val notifier = SaveImageNotifier(context) val notifier = SaveImageNotifier(context.localeContext)
notifier.onClear() notifier.onClear()
// Pictures directory. // Pictures directory.

View file

@ -118,58 +118,36 @@ class RecentMangaHolder(
} }
val notValidNum = item.mch.chapter.chapter_number <= 0 val notValidNum = item.mch.chapter.chapter_number <= 0
binding.body.isVisible = !isSmallUpdates binding.body.isVisible = !isSmallUpdates
val context = itemView.context
binding.body.text = when { binding.body.text = when {
item.mch.chapter.id == null -> binding.body.context.getString( item.mch.chapter.id == null -> context.timeSpanFromNow(R.string.added_, item.mch.manga.date_added)
R.string.added_,
item.mch.manga.date_added.timeSpanFromNow(itemView.context),
)
isSmallUpdates -> "" isSmallUpdates -> ""
item.mch.history.id == null -> { item.mch.history.id == null -> {
if (adapter.viewType == RecentsPresenter.VIEW_TYPE_ONLY_UPDATES) { if (adapter.viewType == RecentsPresenter.VIEW_TYPE_ONLY_UPDATES) {
if (adapter.sortByFetched) { if (adapter.sortByFetched) {
binding.body.context.getString( context.timeSpanFromNow(R.string.fetched_, item.chapter.date_fetch)
R.string.fetched_,
item.chapter.date_fetch.timeSpanFromNow(itemView.context),
)
} else { } else {
binding.body.context.getString( context.timeSpanFromNow(R.string.updated_, item.chapter.date_upload)
R.string.updated_,
item.chapter.date_upload.timeSpanFromNow(itemView.context),
)
} }
} else { } else {
binding.body.context.getString( context.timeSpanFromNow(R.string.fetched_, item.chapter.date_fetch) + "\n" +
R.string.fetched_, context.timeSpanFromNow(R.string.updated_, item.chapter.date_upload)
item.chapter.date_fetch.timeSpanFromNow(itemView.context),
) + "\n" + binding.body.context.getString(
R.string.updated_,
item.chapter.date_upload.timeSpanFromNow(itemView.context),
)
} }
} }
item.chapter.id != item.mch.chapter.id -> item.chapter.id != item.mch.chapter.id -> context.timeSpanFromNow(R.string.read_, item.mch.history.last_read) +
binding.body.context.getString( "\n" + binding.body.context.getString(
R.string.read_, if (notValidNum) R.string.last_read_ else R.string.last_read_chapter_,
item.mch.history.last_read.timeSpanFromNow, if (notValidNum) item.mch.chapter.name else adapter.decimalFormat.format(item.mch.chapter.chapter_number),
) + "\n" + binding.body.context.getString(
if (notValidNum) R.string.last_read_ else R.string.last_read_chapter_,
if (notValidNum) item.mch.chapter.name else adapter.decimalFormat.format(item.mch.chapter.chapter_number),
)
item.chapter.pages_left > 0 && !item.chapter.read ->
binding.body.context.getString(
R.string.read_,
item.mch.history.last_read.timeSpanFromNow(itemView.context),
) + "\n" + itemView.resources.getQuantityString(
R.plurals.pages_left,
item.chapter.pages_left,
item.chapter.pages_left,
)
else -> binding.body.context.getString(
R.string.read_,
item.mch.history.last_read.timeSpanFromNow(itemView.context),
) )
item.chapter.pages_left > 0 && !item.chapter.read -> context.timeSpanFromNow(R.string.read_, item.mch.history.last_read) +
"\n" + itemView.resources.getQuantityString(
R.plurals.pages_left,
item.chapter.pages_left,
item.chapter.pages_left,
)
else -> context.timeSpanFromNow(R.string.read_, item.mch.history.last_read)
} }
if ((itemView.context as? Activity)?.isDestroyed != true) { if ((context as? Activity)?.isDestroyed != true) {
binding.coverThumbnail.loadManga(item.mch.manga) binding.coverThumbnail.loadManga(item.mch.manga)
} }
if (!item.mch.manga.isLocal()) { if (!item.mch.manga.isLocal()) {

View file

@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.util.system.disableItems
import eu.kanade.tachiyomi.util.system.isPackageInstalled import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -90,7 +91,7 @@ class SettingsAdvancedController : SettingsController() {
summaryRes = R.string.saves_error_logs summaryRes = R.string.saves_error_logs
onClick { onClick {
CrashLogUtil(context).dumpLogs() CrashLogUtil(context.localeContext).dumpLogs()
} }
} }

View file

@ -1,14 +1,22 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.content.Intent import android.content.Intent
import android.content.res.XmlResourceParser
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.BuildCompat
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.systemLangContext
import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsGeneralController : SettingsController() { class SettingsGeneralController : SettingsController() {
@ -18,6 +26,8 @@ class SettingsGeneralController : SettingsController() {
var lastThemeXLight: Int? = null var lastThemeXLight: Int? = null
var lastThemeXDark: Int? = null var lastThemeXDark: Int? = null
var themePreference: ThemePreference? = null var themePreference: ThemePreference? = null
@BuildCompat.PrereleaseSdkCheck
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.general titleRes = R.string.general
@ -131,6 +141,83 @@ class SettingsGeneralController : SettingsController() {
} }
defaultValue = "" defaultValue = ""
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
listPreference(activity) {
key = "language"
isPersistent = false
title = context.getString(R.string.language).let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
it.addBetaTag(context)
} else {
it
}
}
dialogTitleRes = R.string.language
val locales = mutableListOf<String>()
val availLocales = Locale.getAvailableLocales()
resources?.getXml(R.xml.locales_config).use { parser ->
parser ?: return@use
while (parser.next() != XmlResourceParser.END_DOCUMENT) {
if (parser.eventType == XmlResourceParser.START_TAG && parser.name == "locale") {
val locale = parser.getAttributeValue(
"http://schemas.android.com/apk/res/android",
"name",
) ?: continue
if (availLocales.contains(Locale.forLanguageTag(locale))) {
locales.add(locale)
}
}
}
}
val localesMap = locales.associateBy { Locale.forLanguageTag(it) }
.toSortedMap { locale1, locale2 ->
val l1 = locale1.getDisplayName(locale1)
.replaceFirstChar { it.uppercase(locale1) }
val l2 = locale2.getDisplayName(locale2)
.replaceFirstChar { it.uppercase(locale2) }
l1.compareToCaseInsensitiveNaturalOrder(l2)
}
val localArray = localesMap.keys.filterNotNull().toTypedArray()
val localeList = LocaleListCompat.create(*localArray)
val sysDef = context.systemLangContext.getString(R.string.system_default)
entries = listOf(sysDef) + localesMap.keys.map { locale ->
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
}
entryValues = listOf("") + localesMap.values
defaultValue = ""
val locale = AppCompatDelegate.getApplicationLocales()
.getFirstMatch(locales.toTypedArray())
if (locale != null) {
tempValue = localArray.indexOf(
if (locales.contains(locale.toLanguageTag())) {
locale
} else {
localeList.getFirstMatch(arrayOf(locale.toLanguageTag()))
},
) + 1
tempEntry =
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
}
onChange {
val value = it as String
val appLocale: LocaleListCompat = if (value.isBlank()) {
preferences.appLanguage().delete()
LocaleListCompat.getEmptyLocaleList()
} else {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
preferences.appLanguage().set(value)
}
LocaleListCompat.forLanguageTags(value)
}
AppCompatDelegate.setApplicationLocales(appLocale)
true
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
infoPreference(R.string.language_requires_app_restart)
}
}
} }
} }

View file

@ -106,7 +106,7 @@ class SettingsSearchController :
binding.recycler.adapter = adapter binding.recycler.adapter = adapter
// load all search results // load all search results
SettingsSearchHelper.initPreferenceSearchResultCollection(presenter.preferences.context) SettingsSearchHelper.initPreferenceSearchResultCollection(view.context)
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import kotlinx.coroutines.GlobalScope
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -35,15 +34,14 @@ class MangaShortcutManager(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
) { ) {
val context: Context = preferences.context fun updateShortcuts(context: Context) {
fun updateShortcuts() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
if (!preferences.showSeriesInShortcuts() && !preferences.showSourcesInShortcuts()) { if (!preferences.showSeriesInShortcuts() && !preferences.showSourcesInShortcuts()) {
val shortcutManager = context.getSystemService(ShortcutManager::class.java) val shortcutManager = context.getSystemService(ShortcutManager::class.java)
shortcutManager.removeAllDynamicShortcuts() shortcutManager.removeAllDynamicShortcuts()
return return
} }
GlobalScope.launchIO { launchIO {
val shortcutManager = context.getSystemService(ShortcutManager::class.java) val shortcutManager = context.getSystemService(ShortcutManager::class.java)
val recentManga = if (preferences.showSeriesInShortcuts()) { val recentManga = if (preferences.showSeriesInShortcuts()) {

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.app.ActivityManager import android.app.ActivityManager
import android.app.LocaleManager
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -44,6 +45,7 @@ import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.util.Locale
import kotlin.math.max import kotlin.math.max
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720 private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
@ -483,3 +485,41 @@ fun Context.getApplicationIcon(pkgName: String): Drawable? {
null null
} }
} }
/** Context used for notifications as Appcompat app lang does not support notifications */
val Context.localeContext: Context
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return this
val pref = Injekt.get<PreferencesHelper>()
val prefsLang = if (pref.appLanguage().isSet()) {
Locale.forLanguageTag(pref.appLanguage().get())
} else null
val configuration = Configuration(resources.configuration)
configuration.setLocale(
prefsLang
?: AppCompatDelegate.getApplicationLocales()[0]
?: Locale.getDefault(),
)
return createConfigurationContext(configuration)
}
fun setLocaleByAppCompat() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
AppCompatDelegate.getApplicationLocales().get(0)?.let { Locale.setDefault(it) }
}
}
val Context.systemLangContext: Context
get() {
val configuration = Configuration(resources.configuration)
val systemLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSystemService<LocaleManager>()?.systemLocales?.get(0)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Resources.getSystem().configuration.locales.get(0)
} else {
return this
} ?: Locale.getDefault()
configuration.setLocale(systemLocale)
return createConfigurationContext(configuration)
}

View file

@ -18,6 +18,8 @@ fun Long.timeSpanFromNow(context: Context): String {
} }
} }
fun Context.timeSpanFromNow(res: Int, time: Long) = getString(res, time.timeSpanFromNow(this))
/** /**
* Convert local time millisecond value to Calendar instance in UTC * Convert local time millisecond value to Calendar instance in UTC
* *

View file

@ -20,6 +20,8 @@ open class ListMatPreference @JvmOverloads constructor(
get() = emptyArray() get() = emptyArray()
set(value) { entries = value.map { context.getString(it) } } set(value) { entries = value.map { context.getString(it) } }
private var defValue: String = "" private var defValue: String = ""
var tempEntry: String? = null
var tempValue: Int? = null
var entries: List<String> = emptyList() var entries: List<String> = emptyList()
override fun onSetInitialValue(defaultValue: Any?) { override fun onSetInitialValue(defaultValue: Any?) {
@ -27,10 +29,19 @@ open class ListMatPreference @JvmOverloads constructor(
defValue = defaultValue as? String ?: defValue defValue = defaultValue as? String ?: defValue
} }
private val indexOfPref: Int
get() = tempValue ?: entryValues.indexOf(
if (isPersistent) {
sharedPreferences?.getString(key, defValue)
} else {
tempEntry
} ?: defValue,
)
override var customSummaryProvider: SummaryProvider<MatPreference>? = SummaryProvider<MatPreference> { override var customSummaryProvider: SummaryProvider<MatPreference>? = SummaryProvider<MatPreference> {
val index = entryValues.indexOf(sharedPreferences?.getString(key, defValue)) val index = indexOfPref
if (entries.isEmpty() || index == -1) "" if (entries.isEmpty() || index == -1) ""
else entries[index] else tempEntry ?: entries.getOrNull(index) ?: ""
} }
override fun dialog(): MaterialAlertDialogBuilder { override fun dialog(): MaterialAlertDialogBuilder {
@ -41,10 +52,16 @@ open class ListMatPreference @JvmOverloads constructor(
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
open fun MaterialAlertDialogBuilder.setListItems() { open fun MaterialAlertDialogBuilder.setListItems() {
val default = entryValues.indexOf(sharedPreferences?.getString(key, defValue) ?: defValue) val default = indexOfPref
setSingleChoiceItems(entries.toTypedArray(), default) { dialog, pos -> setSingleChoiceItems(entries.toTypedArray(), default) { dialog, pos ->
val value = entryValues[pos] val value = entryValues[pos]
sharedPreferences?.edit { putString(key, value) } if (isPersistent) {
sharedPreferences?.edit { putString(key, value) }
} else {
tempValue = pos
tempEntry = entries.getOrNull(pos)
notifyChanged()
}
this@ListMatPreference.summary = this@ListMatPreference.summary this@ListMatPreference.summary = this@ListMatPreference.summary
callChangeListener(value) callChangeListener(value)
dialog.dismiss() dialog.dismiss()

View file

@ -739,6 +739,7 @@
<string name="over_wifi_only">Over Wi-Fi only</string> <string name="over_wifi_only">Over Wi-Fi only</string>
<string name="over_any_network">Over any network</string> <string name="over_any_network">Over any network</string>
<string name="dont_auto_update">Don\'t auto-update</string> <string name="dont_auto_update">Don\'t auto-update</string>
<string name="language_requires_app_restart">Some languages may require an app relaunch to display correctly</string>
<string name="app_shortcuts">App shortcuts</string> <string name="app_shortcuts">App shortcuts</string>
<string name="show_recent_sources">Show recently used sources</string> <string name="show_recent_sources">Show recently used sources</string>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar"/>
<locale android:name="bg"/>
<locale android:name="bn"/>
<locale android:name="ca"/>
<locale android:name="ceb"/>
<locale android:name="cs"/>
<locale android:name="cv"/>
<locale android:name="de"/>
<locale android:name="el"/>
<locale android:name="en"/>
<locale android:name="eo"/>
<locale android:name="es"/>
<locale android:name="eu"/>
<locale android:name="fa"/>
<locale android:name="fi"/>
<locale android:name="fil"/>
<locale android:name="fr"/>
<locale android:name="gl"/>
<locale android:name="hi"/>
<locale android:name="hr"/>
<locale android:name="hu"/>
<locale android:name="in"/>
<locale android:name="it"/>
<locale android:name="ja"/>
<locale android:name="ka"/>
<locale android:name="km"/>
<locale android:name="ko"/>
<locale android:name="lv"/>
<locale android:name="mn"/>
<locale android:name="ms"/>
<locale android:name="my"/>
<locale android:name="nb-NO"/>
<locale android:name="nl"/>
<locale android:name="nn"/>
<locale android:name="or"/>
<locale android:name="pl"/>
<locale android:name="pt"/>
<locale android:name="pt-BR"/>
<locale android:name="ro"/>
<locale android:name="ru"/>
<locale android:name="sc"/>
<locale android:name="sk"/>
<locale android:name="sr"/>
<locale android:name="sv"/>
<locale android:name="te"/>
<locale android:name="th"/>
<locale android:name="ti"/>
<locale android:name="tl"/>
<locale android:name="tr"/>
<locale android:name="uk"/>
<locale android:name="vi"/>
<locale android:name="zh-CN"/>
<locale android:name="zh-TW"/>
</locale-config>