mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 02:19:14 +01:00
Merge pull request #936 from Jays2Kings/Android-12-Features
Android 12 features
This commit is contained in:
commit
f855c10d42
@ -200,10 +200,22 @@
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".data.updater.UpdaterBroadcast"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".data.library.LibraryUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.DownloadService"
|
||||
android:exported="false" />
|
||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||
@ -29,6 +30,10 @@ object Migrations {
|
||||
*/
|
||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||
val context = preferences.context
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit {
|
||||
remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
|
||||
}
|
||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||
@ -103,7 +108,6 @@ object Migrations {
|
||||
}
|
||||
if (oldVersion < 71) {
|
||||
// Migrate DNS over HTTPS setting
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||
if (wasDohEnabled) {
|
||||
prefs.edit {
|
||||
@ -114,7 +118,6 @@ object Migrations {
|
||||
}
|
||||
if (oldVersion < 73) {
|
||||
// Reset rotation to Free after replacing Lock
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (prefs.contains("pref_rotation_type_key")) {
|
||||
prefs.edit {
|
||||
putInt("pref_rotation_type_key", 1)
|
||||
@ -128,7 +131,6 @@ object Migrations {
|
||||
}
|
||||
}
|
||||
if (oldVersion < 75) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val wasShortcutsDisabled = !prefs.getBoolean("show_manga_app_shortcuts", true)
|
||||
if (wasShortcutsDisabled) {
|
||||
prefs.edit {
|
||||
@ -149,7 +151,6 @@ object Migrations {
|
||||
}
|
||||
if (oldVersion < 77) {
|
||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||
1 -> OrientationType.FREE.flagValue
|
||||
2 -> OrientationType.PORTRAIT.flagValue
|
||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||
import eu.kanade.tachiyomi.extension.ExtensionInstallService
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||
@ -71,6 +72,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||
ACTION_CANCEL_EXTENSION_UPDATE -> cancelExtensionUpdate(context)
|
||||
ACTION_CANCEL_UPDATE_DOWNLOAD -> cancelDownloadUpdate(context)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
|
||||
// Share backup file
|
||||
@ -199,6 +201,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
Handler().post { dismissNotification(context, Notifications.ID_LIBRARY_PROGRESS) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to stop a library update
|
||||
*
|
||||
* @param context context of application
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
private fun cancelExtensionUpdate(context: Context) {
|
||||
ExtensionInstallService.stop(context)
|
||||
Handler().post { dismissNotification(context, Notifications.ID_EXTENSION_PROGRESS) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to mark as read
|
||||
*
|
||||
@ -251,6 +264,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Called to cancel library update.
|
||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||
|
||||
// Called to cancel extension update.
|
||||
private const val ACTION_CANCEL_EXTENSION_UPDATE = "$ID.$NAME.CANCEL_EXTENSION_UPDATE"
|
||||
|
||||
private const val ACTION_CANCEL_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_UPDATE_DOWNLOAD"
|
||||
|
||||
// Called to mark as read
|
||||
@ -568,6 +584,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which stops the library update
|
||||
*
|
||||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun cancelExtensionUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_EXTENSION_UPDATE
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that cancels the download for a Tachiyomi update
|
||||
*
|
||||
|
@ -20,6 +20,8 @@ object Notifications {
|
||||
const val ID_UPDATER = 1
|
||||
const val ID_DOWNLOAD_IMAGE = 2
|
||||
const val ID_INSTALL = 3
|
||||
const val CHANNEL_UPDATED = "updated_channel"
|
||||
const val ID_INSTALLED = -6
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the library updater.
|
||||
@ -48,6 +50,9 @@ object Notifications {
|
||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
|
||||
const val CHANNEL_EXT_PROGRESS = "ext_update_progress_channel"
|
||||
const val ID_EXTENSION_PROGRESS = -402
|
||||
|
||||
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
||||
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
||||
const val ID_RESTORE_PROGRESS = -501
|
||||
@ -135,5 +140,25 @@ object Notifications {
|
||||
)
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(channels)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val newChannels = listOf(
|
||||
NotificationChannel(
|
||||
CHANNEL_EXT_PROGRESS,
|
||||
context.getString(R.string.updating_extensions),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_UPDATED,
|
||||
context.getString(R.string.update_completed),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
setShowBadge(false)
|
||||
}
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(newChannels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -229,6 +229,8 @@ object PreferenceKeys {
|
||||
|
||||
const val incognitoMode = "incognito_mode"
|
||||
|
||||
const val shouldAutoUpdate = "should_auto_update"
|
||||
|
||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||
|
||||
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
|
||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.preference
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
@ -13,6 +14,7 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob
|
||||
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
|
||||
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.settings.PageLayout
|
||||
@ -120,8 +122,9 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
|
||||
|
||||
fun lightTheme() = flowPrefs.getEnum(Keys.lightTheme, Themes.DEFAULT)
|
||||
fun darkTheme() = flowPrefs.getEnum(Keys.darkTheme, Themes.DEFAULT)
|
||||
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
fun lightTheme() = flowPrefs.getEnum(Keys.lightTheme, if (isOnA12) Themes.MONET else Themes.DEFAULT)
|
||||
fun darkTheme() = flowPrefs.getEnum(Keys.darkTheme, if (isOnA12) Themes.MONET else Themes.DEFAULT)
|
||||
|
||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||
|
||||
@ -424,6 +427,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||
|
||||
fun hasPromptedBeforeUpdateAll() = flowPrefs.getBoolean("has_prompted_update_all", false)
|
||||
|
||||
fun shouldAutoUpdate() = prefs.getInt(Keys.shouldAutoUpdate, AutoUpdaterJob.ONLY_ON_UNMETERED)
|
||||
|
||||
fun filterChapterByRead() = flowPrefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
||||
|
||||
fun filterChapterByDownloaded() = flowPrefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL)
|
||||
|
@ -0,0 +1,83 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AutoUpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result = coroutineScope {
|
||||
try {
|
||||
val result = UpdateChecker.getUpdateChecker().checkForUpdate()
|
||||
if (result is UpdateResult.NewUpdate<*> && !UpdaterService.isRunning()) {
|
||||
UpdaterNotifier(context).cancel()
|
||||
UpdaterNotifier.releasePageUrl = result.release.releaseLink
|
||||
UpdaterService.start(context, result.release.downloadLink, false)
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
fun foregrounded(): Boolean {
|
||||
val appProcessInfo = ActivityManager.RunningAppProcessInfo()
|
||||
ActivityManager.getMyMemoryState(appProcessInfo)
|
||||
return appProcessInfo.importance == IMPORTANCE_FOREGROUND || appProcessInfo.importance == IMPORTANCE_VISIBLE
|
||||
}
|
||||
|
||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||
block()
|
||||
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AutoUpdateRunner"
|
||||
const val ALWAYS = 0
|
||||
const val ONLY_ON_UNMETERED = 1
|
||||
const val NEVER = 2
|
||||
|
||||
fun setupTask(context: Context) {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val restrictions = preferences.shouldAutoUpdate()
|
||||
val wifiRestriction = if (restrictions == ONLY_ON_UNMETERED) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(wifiRestriction)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<AutoUpdaterJob>()
|
||||
.addTag(TAG)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
|
||||
fun cancelTask(context: Context) {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
|
||||
class UpdaterBroadcast : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (UpdaterService.PACKAGE_INSTALLED_ACTION == intent.action) {
|
||||
val extras = intent.extras ?: return
|
||||
when (val status = extras.getInt(PackageInstaller.EXTRA_STATUS)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmIntent = extras[Intent.EXTRA_INTENT] as? Intent
|
||||
context.startActivity(confirmIntent)
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit {
|
||||
remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
|
||||
}
|
||||
val notifyOnInstall = extras.getBoolean(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, false)
|
||||
try {
|
||||
if (notifyOnInstall) {
|
||||
UpdaterNotifier(context).onInstallFinished()
|
||||
}
|
||||
} finally {
|
||||
UpdaterService.stop(context)
|
||||
}
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
|
||||
if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
context.toast(R.string.could_not_install_update)
|
||||
val uri = intent.getStringExtra(UpdaterService.EXTRA_FILE_URI) ?: return
|
||||
UpdaterNotifier(context).onInstallError(uri.toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val notifyOnInstall = prefs.getBoolean(UpdaterService.NOTIFY_ON_INSTALL_KEY, false)
|
||||
prefs.edit {
|
||||
remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
|
||||
}
|
||||
if (notifyOnInstall) {
|
||||
UpdaterNotifier(context).onInstallFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
@ -10,8 +11,10 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
@ -19,8 +22,14 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
|
||||
override suspend fun doWork(): Result = coroutineScope {
|
||||
try {
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val result = UpdateChecker.getUpdateChecker().checkForUpdate()
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
preferences.shouldAutoUpdate() != AutoUpdaterJob.NEVER
|
||||
) {
|
||||
AutoUpdaterJob.setupTask(context)
|
||||
}
|
||||
UpdaterNotifier(context).promptUpdate(
|
||||
result.release.info,
|
||||
result.release.downloadLink,
|
||||
|
@ -4,9 +4,11 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
@ -44,6 +46,7 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
fun promptUpdate(body: String, url: String, releaseUrl: String) {
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
||||
putExtra(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, true)
|
||||
}
|
||||
|
||||
val pendingIntent = NotificationReceiver.openUpdatePendingActivity(context, body, url)
|
||||
@ -56,10 +59,11 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
color = context.getResourceColor(R.attr.colorAccent)
|
||||
clearActions()
|
||||
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
// Download action
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.getString(R.string.download),
|
||||
context.getString(if (isOnA12) R.string.update else R.string.download),
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
0,
|
||||
@ -155,6 +159,31 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
notificationBuilder.show(Notifications.ID_INSTALL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download is finished.
|
||||
*
|
||||
* @param uri path location of apk.
|
||||
*/
|
||||
fun onInstallFinished() {
|
||||
with(NotificationCompat.Builder(context, Notifications.CHANNEL_UPDATED)) {
|
||||
setContentTitle(context.getString(R.string.updated_to_, BuildConfig.VERSION_NAME))
|
||||
setSmallIcon(R.drawable.ic_tachij2k_notification)
|
||||
setAutoCancel(true)
|
||||
setOngoing(false)
|
||||
setProgress(0, 0, false)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
context.packageManager.getLaunchIntentForPackage(BuildConfig.APPLICATION_ID),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
setContentIntent(pendingIntent)
|
||||
clearActions()
|
||||
addReleasePageAction()
|
||||
show(Notifications.ID_INSTALLED)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download throws a error
|
||||
*
|
||||
@ -186,6 +215,32 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
notificationBuilder.show(Notifications.ID_UPDATER)
|
||||
}
|
||||
|
||||
fun onInstallError(uri: Uri) {
|
||||
with(notificationBuilder) {
|
||||
setContentText(context.getString(R.string.could_not_install_update))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
setOnlyAlertOnce(false)
|
||||
setAutoCancel(false)
|
||||
setProgress(0, 0, false)
|
||||
color = ContextCompat.getColor(context, R.color.colorAccent)
|
||||
clearActions()
|
||||
// Retry action
|
||||
addAction(
|
||||
R.drawable.ic_refresh_24dp,
|
||||
context.getString(R.string.retry),
|
||||
NotificationHandler.installApkPendingActivity(context, uri)
|
||||
)
|
||||
// Cancel action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
|
||||
)
|
||||
addReleasePageAction()
|
||||
}
|
||||
notificationBuilder.show(Notifications.ID_UPDATER)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
NotificationReceiver.dismissNotification(context, Notifications.ID_UPDATER)
|
||||
}
|
||||
|
@ -4,9 +4,12 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@ -18,12 +21,11 @@ import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.internal.http2.ErrorCode
|
||||
@ -64,6 +66,8 @@ class UpdaterService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
|
||||
instance = this
|
||||
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
stopSelf(startId)
|
||||
@ -71,9 +75,10 @@ class UpdaterService : Service() {
|
||||
|
||||
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
|
||||
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
|
||||
val notifyOnInstall = intent.getBooleanExtra(EXTRA_NOTIFY_ON_INSTALL, false)
|
||||
|
||||
runningJob = GlobalScope.launch(handler) {
|
||||
downloadApk(title, url)
|
||||
downloadApk(title, url, notifyOnInstall)
|
||||
}
|
||||
|
||||
runningJob?.invokeOnCompletion { stopSelf(startId) }
|
||||
@ -88,6 +93,9 @@ class UpdaterService : Service() {
|
||||
|
||||
override fun onDestroy() {
|
||||
destroyJob()
|
||||
if (instance == this) {
|
||||
instance = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@ -104,7 +112,7 @@ class UpdaterService : Service() {
|
||||
*
|
||||
* @param url url location of file
|
||||
*/
|
||||
private suspend fun downloadApk(title: String, url: String) {
|
||||
private suspend fun downloadApk(title: String, url: String, notifyOnInstall: Boolean) {
|
||||
// Show notification download starting.
|
||||
notifier.onDownloadStarted(title)
|
||||
|
||||
@ -141,7 +149,11 @@ class UpdaterService : Service() {
|
||||
response.close()
|
||||
throw Exception("Unsuccessful response")
|
||||
}
|
||||
notifier.onDownloadFinished(apkFile.getUriCompat(this))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
startInstalling(apkFile, notifyOnInstall)
|
||||
} else {
|
||||
notifier.onDownloadFinished(apkFile.getUriCompat(this))
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Timber.e(error)
|
||||
if (error is CancellationException ||
|
||||
@ -154,30 +166,77 @@ class UpdaterService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInstalling(file: File, notifyOnInstall: Boolean) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
|
||||
val packageInstaller = packageManager.packageInstaller
|
||||
val data = file.inputStream()
|
||||
|
||||
val params = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
)
|
||||
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
val session = packageInstaller.openSession(sessionId)
|
||||
session.openWrite("package", 0, -1).use { packageInSession ->
|
||||
data.copyTo(packageInSession)
|
||||
}
|
||||
if (notifyOnInstall) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
prefs.edit {
|
||||
putBoolean(NOTIFY_ON_INSTALL_KEY, true)
|
||||
}
|
||||
}
|
||||
|
||||
val newIntent = Intent(this, UpdaterBroadcast::class.java)
|
||||
.setAction(PACKAGE_INSTALLED_ACTION)
|
||||
.putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
|
||||
.putExtra(EXTRA_FILE_URI, file.getUriCompat(this).toString())
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(this, -10053, newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
val statusReceiver = pendingIntent.intentSender
|
||||
session.commit(statusReceiver)
|
||||
data.close()
|
||||
} catch (error: Exception) {
|
||||
// Either install package can't be found (probably bots) or there's a security exception
|
||||
// with the download manager. Nothing we can workaround.
|
||||
toast(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val PACKAGE_INSTALLED_ACTION =
|
||||
"${BuildConfig.APPLICATION_ID}.SESSION_SELF_API_PACKAGE_INSTALLED"
|
||||
internal const val EXTRA_NOTIFY_ON_INSTALL = "${BuildConfig.APPLICATION_ID}.UpdaterService.ACTION_ON_INSTALL"
|
||||
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
|
||||
internal const val EXTRA_FILE_URI = "${BuildConfig.APPLICATION_ID}.UpdaterService.FILE_URI"
|
||||
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
|
||||
|
||||
internal const val NOTIFY_ON_INSTALL_KEY = "notify_on_install_complete"
|
||||
|
||||
private var instance: UpdaterService? = null
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
private fun isRunning(context: Context): Boolean =
|
||||
context.isServiceRunning(UpdaterService::class.java)
|
||||
fun isRunning(): Boolean = instance != null
|
||||
|
||||
/**
|
||||
* Downloads a new update and let the user install the new version from a notification.
|
||||
* @param context the application context.
|
||||
* @param url the url to the new update.
|
||||
*/
|
||||
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
||||
if (!isRunning(context)) {
|
||||
fun start(context: Context, url: String, notifyOnInstall: Boolean) {
|
||||
if (!isRunning()) {
|
||||
val title = context.getString(R.string.app_name)
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||
putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
@ -202,9 +261,10 @@ class UpdaterService : Service() {
|
||||
* @param url the url to the new update.
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
||||
internal fun downloadApkPendingService(context: Context, url: String, notifyOnInstall: Boolean = false): PendingIntent {
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||
putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
|
||||
}
|
||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
|
||||
class ExtensionInstallNotifier(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Bitmap of the app for notifications.
|
||||
*/
|
||||
private val notificationBitmap by lazy {
|
||||
BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending intent of action that cancels the library update
|
||||
*/
|
||||
private val cancelIntent by lazy {
|
||||
NotificationReceiver.cancelExtensionUpdatePendingBroadcast(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached progress notification to avoid creating a lot.
|
||||
*/
|
||||
val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_UPDATES_TO_EXTS) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setLargeIcon(notificationBitmap)
|
||||
setContentTitle(context.getString(R.string.updating_extensions))
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
setSilent(true)
|
||||
setOnlyAlertOnce(true)
|
||||
color = ContextCompat.getColor(context, R.color.colorAccent)
|
||||
addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
* @param manga the manga that's being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(progress: Int, max: Int) {
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_EXTENSION_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.updating_extensions))
|
||||
.setProgress(max, progress, progress == 0)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.ArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ExtensionInstallService(
|
||||
val extensionManager: ExtensionManager = Injekt.get(),
|
||||
) : Service() {
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var notifier: ExtensionInstallNotifier
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var serviceScope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
private val requestSemaphore = Semaphore(3)
|
||||
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
/**
|
||||
* This method needs to be implemented, but it's not used/needed.
|
||||
*/
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service receives an intent.
|
||||
*
|
||||
* @param intent the start intent from.
|
||||
* @param flags the flags of the command.
|
||||
* @param startId the start id of this command.
|
||||
* @return the start value of the command.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
if (!preferences.hasPromptedBeforeUpdateAll().get()) {
|
||||
toast(R.string.some_extensions_may_prompt)
|
||||
preferences.hasPromptedBeforeUpdateAll().set(true)
|
||||
}
|
||||
|
||||
instance = this
|
||||
|
||||
val list = intent.getParcelableArrayListExtra<ExtensionInfo>(KEY_EXTENSION)?.filter {
|
||||
(
|
||||
extensionManager.installedExtensions.find { installed ->
|
||||
installed.pkgName == it.pkgName
|
||||
}?.versionCode ?: 0
|
||||
) < it.versionCode
|
||||
}
|
||||
?: return START_NOT_STICKY
|
||||
var installed = 0
|
||||
job = serviceScope.launch {
|
||||
val results = list.map {
|
||||
async {
|
||||
requestSemaphore.withPermit {
|
||||
extensionManager.installExtension(it, serviceScope)
|
||||
.collect {
|
||||
if (it.first.isCompleted()) {
|
||||
installed++
|
||||
}
|
||||
notifier.showProgressNotification(installed, list.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results.awaitAll()
|
||||
}
|
||||
job?.invokeOnCompletion { stopSelf(startId) }
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service is created. It injects dagger dependencies and acquire
|
||||
* the wake lock.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS)
|
||||
notifier = ExtensionInstallNotifier(this)
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"ExtensionInstallService:WakeLock"
|
||||
)
|
||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||
startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service is destroyed. It cancels jobs and releases the wake lock.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
job?.cancel()
|
||||
serviceScope.cancel()
|
||||
if (instance == this) {
|
||||
instance = null
|
||||
}
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: ExtensionInstallService? = null
|
||||
|
||||
/**
|
||||
* Stops the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
instance?.serviceScope?.cancel()
|
||||
context.stopService(Intent(context, ExtensionUpdateJob::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
* Key that defines what should be updated.
|
||||
*/
|
||||
private const val KEY_EXTENSION = "extension"
|
||||
|
||||
fun jobIntent(context: Context, extensions: List<Extension.Available>): Intent {
|
||||
return Intent(context, ExtensionInstallService::class.java).apply {
|
||||
val info = extensions.map(::ExtensionInfo)
|
||||
putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Parcelable
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
@ -15,9 +16,12 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.system.launchNow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -47,6 +51,15 @@ class ExtensionManager(
|
||||
*/
|
||||
private val installer by lazy { ExtensionInstaller(context) }
|
||||
|
||||
val downloadRelay
|
||||
get() = installer.downloadsStateFlow
|
||||
|
||||
fun getExtension(downloadId: Long): String? {
|
||||
return installer.activeDownloads.entries.find { downloadId == it.value }?.key
|
||||
}
|
||||
|
||||
fun getActiveInstalls(): Int = installer.activeDownloads.size
|
||||
|
||||
/**
|
||||
* Relay used to notify the installed extensions.
|
||||
*/
|
||||
@ -232,27 +245,14 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the installation process for the given extension. It will complete
|
||||
* once the extension is installed or throws an error. The process will be canceled if
|
||||
* unsubscribed before its completion.
|
||||
* Returns a flow of the installation process for the given extension. It will complete
|
||||
* once the extension is installed or throws an error. The process will be canceled the scope
|
||||
* is canceled before its completion.
|
||||
*
|
||||
* @param extension The extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: Extension.Available): Observable<ExtensionIntallInfo> {
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the installation process for the given extension. It will complete
|
||||
* once the extension is updated or throws an error. The process will be canceled if
|
||||
* unsubscribed before its completion.
|
||||
*
|
||||
* @param extension The extension to be updated.
|
||||
*/
|
||||
fun updateExtension(extension: Extension.Installed): Observable<ExtensionIntallInfo> {
|
||||
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
||||
?: return Observable.empty()
|
||||
return installExtension(availableExt)
|
||||
suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -405,6 +405,21 @@ class ExtensionManager(
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ExtensionInfo(
|
||||
val apkName: String,
|
||||
val pkgName: String,
|
||||
val name: String,
|
||||
val versionCode: Int,
|
||||
) : Parcelable {
|
||||
constructor(extension: Extension.Available) : this(
|
||||
apkName = extension.apkName,
|
||||
pkgName = extension.pkgName,
|
||||
name = extension.name,
|
||||
versionCode = extension.versionCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtensionsChangedListener {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -17,6 +19,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -35,15 +38,21 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
}
|
||||
|
||||
if (pendingUpdates.isNotEmpty()) {
|
||||
createUpdateNotification(pendingUpdates.map { it.name })
|
||||
createUpdateNotification(pendingUpdates)
|
||||
}
|
||||
|
||||
Result.success()
|
||||
}
|
||||
|
||||
private fun createUpdateNotification(names: List<String>) {
|
||||
private fun createUpdateNotification(extensions: List<Extension.Available>) {
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
preferences.extensionUpdatesCount().set(names.size)
|
||||
preferences.extensionUpdatesCount().set(extensions.size)
|
||||
// Not doing this yet since users will get prompted while device is idle
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preferences.autoUpdateExtensions()) {
|
||||
// val intent = ExtensionInstallService.jobIntent(context, extensions)
|
||||
// context.startForegroundService(intent)
|
||||
// return
|
||||
// }
|
||||
NotificationManagerCompat.from(context).apply {
|
||||
notify(
|
||||
Notifications.ID_UPDATES_TO_EXTS,
|
||||
@ -51,11 +60,11 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
setContentTitle(
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.extension_updates_available,
|
||||
names.size,
|
||||
names.size
|
||||
extensions.size,
|
||||
extensions.size
|
||||
)
|
||||
)
|
||||
val extNames = names.joinToString(", ")
|
||||
val extNames = extensions.joinToString(", ") { it.name }
|
||||
setContentText(extNames)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
|
||||
setSmallIcon(R.drawable.ic_extension_update_24dp)
|
||||
@ -65,6 +74,16 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
context
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val intent = ExtensionInstallService.jobIntent(context, extensions)
|
||||
val pendingIntent =
|
||||
PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
addAction(
|
||||
R.drawable.ic_file_download_24dp,
|
||||
context.getString(R.string.update_all),
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
@ -30,7 +31,7 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
||||
suspend fun checkForUpdates(context: Context): List<Extension.Available> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val extensions = findExtensions()
|
||||
|
||||
@ -38,7 +39,7 @@ internal class ExtensionGithubApi {
|
||||
.filterIsInstance<LoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
||||
val extensionsWithUpdate = mutableListOf<Extension.Available>()
|
||||
val mutInstalledExtensions = installedExtensions.toMutableList()
|
||||
for (installedExt in mutInstalledExtensions) {
|
||||
val pkgName = installedExt.pkgName
|
||||
@ -46,7 +47,7 @@ internal class ExtensionGithubApi {
|
||||
|
||||
val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
||||
if (hasUpdate) {
|
||||
extensionsWithUpdate.add(installedExt)
|
||||
extensionsWithUpdate.add(availableExt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +76,7 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
fun getApkUrl(extension: ExtensionManager.ExtensionInfo): String {
|
||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
enum class InstallStep {
|
||||
Pending, Downloading, Loading, Installing, Installed, Error;
|
||||
Pending, Downloading, Loading, Installing, Installed, Error, Done;
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return this == Installed || this == Error
|
||||
return this == Installed || this == Error || this == Done
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.content.pm.PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.hippo.unifile.UniFile
|
||||
@ -35,10 +39,9 @@ class ExtensionInstallActivity : Activity() {
|
||||
val params = SessionParams(
|
||||
SessionParams.MODE_FULL_INSTALL
|
||||
)
|
||||
// TODO: Add once compiling via SDK 31
|
||||
// if (Build.VERSION.SDK_INT >= 31) {
|
||||
// params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
|
||||
// }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
|
||||
}
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
val session = packageInstaller.openSession(sessionId)
|
||||
session.openWrite("package", 0, -1).use { packageInSession ->
|
||||
@ -55,6 +58,9 @@ class ExtensionInstallActivity : Activity() {
|
||||
session.commit(statusReceiver)
|
||||
val extensionManager: ExtensionManager by injectLazy()
|
||||
extensionManager.setInstalling(downloadId, sessionId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
(getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).remove(downloadId)
|
||||
}
|
||||
data.close()
|
||||
} catch (error: Exception) {
|
||||
// Either install package can't be found (probably bots) or there's a security exception
|
||||
|
@ -5,20 +5,35 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
@ -30,7 +45,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
/**
|
||||
* The system's download manager
|
||||
*/
|
||||
private val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
private val downloadManager =
|
||||
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
/**
|
||||
* The broadcast receiver which listens to download completion events.
|
||||
@ -41,30 +57,24 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||
* returned by the download manager.
|
||||
*/
|
||||
private val activeDownloads = hashMapOf<String, Long>()
|
||||
val activeDownloads = hashMapOf<String, Long>()
|
||||
|
||||
/**
|
||||
* Relay used to notify the installation step of every download.
|
||||
* StateFlow used to notify the installation step of every download.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||
val downloadsStateFlow = MutableStateFlow(0L to ExtensionIntallInfo(InstallStep.Pending, null))
|
||||
|
||||
/** Map of download id to installer session id */
|
||||
val downloadInstallerMap = hashMapOf<Long, Int>()
|
||||
|
||||
data class DownloadSessionInfo(
|
||||
val downloadId: Long,
|
||||
val session: PackageInstaller.Session,
|
||||
val sessionId: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* Adds the given extension to the downloads queue and returns a flow containing its
|
||||
* step in the installation process.
|
||||
*
|
||||
* @param url The url of the apk.
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: Extension) = Observable.defer {
|
||||
suspend fun downloadAndInstall(url: String, extension: ExtensionManager.ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
@ -79,77 +89,120 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
||||
.setDestinationInExternalFilesDir(
|
||||
context,
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
downloadUri.lastPathSegment
|
||||
)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
downloadsRelay.filter { it.first == id }
|
||||
.map {
|
||||
val sessionId = downloadInstallerMap[it.first] ?: return@map it.second to null
|
||||
val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||
it.second to session
|
||||
scope.launch {
|
||||
flowOf(
|
||||
pollStatus(id),
|
||||
pollInstallStatus(id)
|
||||
).flattenMerge()
|
||||
.transformWhile {
|
||||
emit(it)
|
||||
!it.first.isCompleted()
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.catch { e ->
|
||||
Timber.e(e)
|
||||
emit(InstallStep.Error to null)
|
||||
}
|
||||
.onCompletion {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
.collect {
|
||||
downloadsStateFlow.emit(id to it)
|
||||
}
|
||||
}
|
||||
|
||||
return downloadsStateFlow.filter { it.first == id }.map { it.second }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.transformWhile {
|
||||
emit(it)
|
||||
!it.first.isCompleted()
|
||||
}
|
||||
.onCompletion {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
// Poll download status
|
||||
.mergeWith(pollStatus(id))
|
||||
// Poll installation status
|
||||
.mergeWith(pollInstallStatus(id))
|
||||
// Force an error if the download takes more than 3 minutes
|
||||
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error to null })
|
||||
// Stop when the application is installed or errors
|
||||
.takeUntil { it.first.isCompleted() }
|
||||
// Always notify on main thread
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Always remove the download when unsubscribed
|
||||
.doOnUnsubscribe { deleteDownload(pkgName) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that polls the given download id for its status every second, as the
|
||||
* Returns a flow that polls the given download id for its status every second, as the
|
||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||
*
|
||||
* @param id The id of the download to poll.
|
||||
*/
|
||||
private fun pollStatus(id: Long): Observable<ExtensionIntallInfo> {
|
||||
private fun pollStatus(id: Long): Flow<ExtensionIntallInfo> {
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
|
||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
||||
// Get the current download status
|
||||
.map {
|
||||
downloadManager.query(query).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
return flow {
|
||||
while (true) {
|
||||
val newDownloadState = try {
|
||||
downloadManager.query(query)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
if (newDownloadState != null) {
|
||||
emit(newDownloadState)
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
// Ignore duplicate results
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
// Stop polling when the download fails or finishes
|
||||
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
||||
// Map to our model
|
||||
.flatMap { status ->
|
||||
val step = when (status) {
|
||||
.transformWhile {
|
||||
emit(it)
|
||||
!(it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED)
|
||||
}
|
||||
.flatMapConcat { downloadState ->
|
||||
val step = when (downloadState) {
|
||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||
else -> return@flatMap Observable.empty()
|
||||
else -> return@flatMapConcat emptyFlow()
|
||||
}
|
||||
Observable.just(ExtensionIntallInfo(step, null))
|
||||
}
|
||||
.doOnError {
|
||||
Timber.e(it)
|
||||
flowOf(ExtensionIntallInfo(step, null))
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollInstallStatus(id: Long): Observable<ExtensionIntallInfo> {
|
||||
return Observable.interval(0, 500, TimeUnit.MILLISECONDS)
|
||||
.flatMap {
|
||||
val sessionId = downloadInstallerMap[id] ?: return@flatMap Observable.empty()
|
||||
val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||
Observable.just(InstallStep.Installing to session)
|
||||
/**
|
||||
* Returns a flow that polls the given installer session for its status every half second, as the
|
||||
* manager doesn't have any notification system. This will only stop once
|
||||
*
|
||||
* @param id The id of the download mapped to the session to poll.
|
||||
*/
|
||||
private fun pollInstallStatus(id: Long): Flow<ExtensionIntallInfo> {
|
||||
return flow {
|
||||
while (true) {
|
||||
val sessionId = downloadInstallerMap[id]
|
||||
if (sessionId != null) {
|
||||
val session =
|
||||
context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||
emit(InstallStep.Installing to session)
|
||||
}
|
||||
delay(500)
|
||||
}
|
||||
.doOnError {
|
||||
}
|
||||
.takeWhile { info ->
|
||||
val sessionId = downloadInstallerMap[id]
|
||||
if (sessionId != null) {
|
||||
info.second != null
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
Timber.e(it)
|
||||
}
|
||||
.onCompletion {
|
||||
emit(InstallStep.Done to null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +238,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param downloadId The id of the download.
|
||||
*/
|
||||
fun setInstalling(downloadId: Long, sessionId: Int) {
|
||||
downloadsRelay.call(downloadId to InstallStep.Installing)
|
||||
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null))
|
||||
downloadInstallerMap[downloadId] = sessionId
|
||||
}
|
||||
|
||||
@ -204,7 +257,11 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||
downloadInstallerMap.remove(downloadId)
|
||||
downloadsRelay.call(downloadId to step)
|
||||
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(step, null))
|
||||
}
|
||||
|
||||
fun softDeleteDownload(downloadId: Long) {
|
||||
downloadManager.remove(downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -267,10 +324,10 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
// Set next installation step
|
||||
if (uri != null) {
|
||||
downloadsRelay.call(id to InstallStep.Loading)
|
||||
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null))
|
||||
} else {
|
||||
Timber.e("Couldn't locate downloaded APK")
|
||||
downloadsRelay.call(id to InstallStep.Error)
|
||||
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -24,5 +24,6 @@ class ExtensionAdapter(val listener: OnButtonClickListener) :
|
||||
interface OnButtonClickListener {
|
||||
fun onButtonClick(position: Int)
|
||||
fun onCancelClick(position: Int)
|
||||
fun onUpdateAllClicked(position: Int)
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,15 @@ import eu.kanade.tachiyomi.ui.migration.SelectionHeader
|
||||
import eu.kanade.tachiyomi.ui.migration.SourceItem
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.executeOnIO
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@ -38,7 +41,7 @@ typealias ExtensionIntallInfo = Pair<InstallStep, PackageInstaller.SessionInfo?>
|
||||
class ExtensionBottomPresenter(
|
||||
private val bottomSheet: ExtensionBottomSheet,
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BaseCoroutinePresenter(), ExtensionsChangedListener {
|
||||
|
||||
private var extensions = emptyList<ExtensionItem>()
|
||||
@ -76,7 +79,10 @@ class ExtensionBottomPresenter(
|
||||
sourceItems = findSourcesWithManga(favs)
|
||||
mangaItems = HashMap(
|
||||
sourceItems.associate {
|
||||
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id)
|
||||
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(
|
||||
favs,
|
||||
it.source.id
|
||||
)
|
||||
}
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -89,6 +95,27 @@ class ExtensionBottomPresenter(
|
||||
}
|
||||
listOf(migrationJob, extensionJob).awaitAll()
|
||||
}
|
||||
presenterScope.launch {
|
||||
extensionManager.downloadRelay
|
||||
.collect {
|
||||
val extPageName = extensionManager.getExtension(it.first)
|
||||
val extension = extensions.find { item ->
|
||||
extPageName == item.extension.pkgName
|
||||
} ?: return@collect
|
||||
when (it.second.first) {
|
||||
InstallStep.Installed, InstallStep.Error -> {
|
||||
currentDownloads.remove(extension.extension.pkgName)
|
||||
}
|
||||
else -> {
|
||||
currentDownloads[extension.extension.pkgName] = it.second
|
||||
}
|
||||
}
|
||||
val item = updateInstallStep(extension.extension, it.second.first, it.second.second)
|
||||
if (item != null) {
|
||||
withUIContext { bottomSheet.downloadUpdate(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||
@ -176,7 +203,8 @@ class ExtensionBottomPresenter(
|
||||
updatesSorted.size,
|
||||
updatesSorted.size
|
||||
),
|
||||
updatesSorted.size
|
||||
updatesSorted.size,
|
||||
items.count { it.extension.pkgName in currentDownloads.keys } != updatesSorted.size
|
||||
)
|
||||
items += updatesSorted.map { extension ->
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||
@ -215,7 +243,7 @@ class ExtensionBottomPresenter(
|
||||
@Synchronized
|
||||
private fun updateInstallStep(
|
||||
extension: Extension,
|
||||
state: InstallStep,
|
||||
state: InstallStep?,
|
||||
session: PackageInstaller.SessionInfo?
|
||||
): ExtensionItem? {
|
||||
val extensions = extensions.toMutableList()
|
||||
@ -242,13 +270,21 @@ class ExtensionBottomPresenter(
|
||||
|
||||
fun installExtension(extension: Extension.Available) {
|
||||
if (isNotMIUIOptimized()) {
|
||||
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
||||
presenterScope.launch {
|
||||
extensionManager.installExtension(ExtensionManager.ExtensionInfo(extension), presenterScope)
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateExtension(extension: Extension.Installed) {
|
||||
if (isNotMIUIOptimized()) {
|
||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
||||
val availableExt =
|
||||
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return
|
||||
presenterScope.launch {
|
||||
extensionManager.installExtension(ExtensionManager.ExtensionInfo(availableExt), presenterScope)
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,17 +296,6 @@ class ExtensionBottomPresenter(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Observable<ExtensionIntallInfo>.subscribeToInstallUpdate(extension: Extension) {
|
||||
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||
.map { state -> updateInstallStep(extension, state.first, state.second) }
|
||||
.subscribe { item ->
|
||||
if (item != null) {
|
||||
bottomSheet.downloadUpdate(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallExtension(pkgName: String) {
|
||||
extensionManager.uninstallExtension(pkgName)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@ -201,6 +202,35 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
presenter.cancelExtensionInstall(extension)
|
||||
}
|
||||
|
||||
override fun onUpdateAllClicked(position: Int) {
|
||||
if (!presenter.preferences.hasPromptedBeforeUpdateAll().get()) {
|
||||
MaterialDialog(controller.activity!!)
|
||||
.title(R.string.update_all)
|
||||
.message(R.string.some_extensions_may_prompt)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
presenter.preferences.hasPromptedBeforeUpdateAll().set(true)
|
||||
updateAllExtensions(position)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
updateAllExtensions(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAllExtensions(position: Int) {
|
||||
val header = (extAdapter?.getSectionHeader(position)) as? ExtensionGroupItem ?: return
|
||||
val items = extAdapter?.getSectionItemPositions(header)
|
||||
items?.forEach {
|
||||
val extItem = (extAdapter?.getItem(it) as? ExtensionItem) ?: return
|
||||
val extension = (extAdapter?.getItem(it) as? ExtensionItem)?.extension ?: return
|
||||
if (extItem.installStep == null &&
|
||||
extension is Extension.Installed && extension.hasUpdate
|
||||
) {
|
||||
presenter.updateExtension(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||
when (binding.tabs.selectedTabPosition) {
|
||||
0 -> {
|
||||
@ -298,6 +328,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
extAdapter?.updateDataSet(extensions)
|
||||
}
|
||||
updateExtTitle()
|
||||
updateExtUpdateAllButton()
|
||||
}
|
||||
|
||||
fun canGoBack(): Boolean {
|
||||
@ -310,6 +341,20 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
|
||||
fun downloadUpdate(item: ExtensionItem) {
|
||||
extAdapter?.updateItem(item, item.installStep)
|
||||
updateExtUpdateAllButton()
|
||||
}
|
||||
|
||||
fun updateExtUpdateAllButton() {
|
||||
val updateHeader =
|
||||
extAdapter?.headerItems?.find { it is ExtensionGroupItem && it.canUpdate != null } as? ExtensionGroupItem
|
||||
?: return
|
||||
val items = extAdapter?.getSectionItemPositions(updateHeader) ?: return
|
||||
updateHeader.canUpdate = items.any {
|
||||
val extItem = (extAdapter?.getItem(it) as? ExtensionItem) ?: return
|
||||
val extension = (extAdapter?.getItem(it) as? ExtensionItem)?.extension ?: return
|
||||
extItem.installStep == null
|
||||
}
|
||||
extAdapter?.updateItem(updateHeader)
|
||||
}
|
||||
|
||||
override fun trustSignature(signatureHash: String) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
@ -13,8 +15,16 @@ class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<IFlexible<Recycl
|
||||
|
||||
private val binding = ExtensionCardHeaderBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.extButton.setOnClickListener {
|
||||
(adapter as? ExtensionAdapter)?.listener?.onUpdateAllClicked(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: ExtensionGroupItem) {
|
||||
binding.title.text = item.name
|
||||
binding.extButton.isVisible = item.canUpdate != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
binding.extButton.isEnabled = item.canUpdate == true
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.R
|
||||
* @param name The header name.
|
||||
* @param size The number of items in the group.
|
||||
*/
|
||||
data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderItem<ExtensionGroupHolder>() {
|
||||
data class ExtensionGroupItem(val name: String, val size: Int, var canUpdate: Boolean? = null) : AbstractHeaderItem<ExtensionGroupHolder>() {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
|
@ -68,6 +68,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
|
||||
@Suppress("ResourceType")
|
||||
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
|
||||
if (item.installStep == InstallStep.Done) return@with
|
||||
isEnabled = true
|
||||
isClickable = true
|
||||
isActivated = false
|
||||
@ -87,6 +88,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
InstallStep.Installing -> R.string.installing
|
||||
InstallStep.Installed -> R.string.installed
|
||||
InstallStep.Error -> R.string.retry
|
||||
else -> return@with
|
||||
}
|
||||
)
|
||||
if (installStep != InstallStep.Error) {
|
||||
|
@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.ui.main
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Dialog
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@ -55,6 +57,7 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterNotifier
|
||||
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
@ -647,6 +650,25 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
when (val controller = router.backstack.lastOrNull()?.controller) {
|
||||
is MangaDetailsController -> {
|
||||
val source = controller.presenter.source as? HttpSource ?: return
|
||||
val url = try {
|
||||
source.mangaDetailsRequest(controller.presenter.manga).url.toString()
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
outContent.webUri = Uri.parse(url)
|
||||
}
|
||||
is BrowseSourceController -> {
|
||||
val source = controller.presenter.source as? HttpSource ?: return
|
||||
outContent.webUri = Uri.parse(source.baseUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
overflowDialog?.dismiss()
|
||||
|
@ -4,6 +4,9 @@ import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
@ -124,6 +127,15 @@ class MangaHeaderHolder(
|
||||
)
|
||||
true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
backdrop.setRenderEffect(
|
||||
RenderEffect.createBlurEffect(
|
||||
10f,
|
||||
10f,
|
||||
Shader.TileMode.MIRROR
|
||||
)
|
||||
)
|
||||
}
|
||||
mangaCover.setOnClickListener { adapter.delegate.zoomImageFromThumb(coverCard) }
|
||||
trackButton.setOnClickListener { adapter.delegate.showTrackingSheet() }
|
||||
if (startExpanded) expandDesc()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -9,6 +10,7 @@ import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
@ -41,6 +43,7 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
||||
import eu.kanade.tachiyomi.data.preference.toggle
|
||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
@ -1225,6 +1228,18 @@ class ReaderActivity :
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share)))
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
val manga = presenter.manga ?: return
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
val url = try {
|
||||
source.mangaDetailsRequest(manga).url.toString()
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
outContent.webUri = Uri.parse(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the page sheet. It delegates saving the image of the given [page] on external
|
||||
* storage to the presenter.
|
||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.DelayedTrackingUpdateJob
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@ -76,6 +77,9 @@ class ReaderPresenter(
|
||||
var manga: Manga? = null
|
||||
private set
|
||||
|
||||
val source: Source?
|
||||
get() = manga?.source?.let { sourceManager.getOrStub(it) }
|
||||
|
||||
/**
|
||||
* The chapter id of the currently loaded chapter. Used to restore from process kill.
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -190,15 +191,16 @@ class AboutController : SettingsController() {
|
||||
)
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
return MaterialDialog(activity!!)
|
||||
.title(R.string.new_version_available)
|
||||
.message(text = args.getString(BODY_KEY) ?: "")
|
||||
.positiveButton(R.string.download) {
|
||||
.positiveButton(if (isOnA12) R.string.update else R.string.download) {
|
||||
val appContext = applicationContext
|
||||
if (appContext != null) {
|
||||
// Start download
|
||||
val url = args.getString(URL_KEY) ?: ""
|
||||
UpdaterService.start(appContext, url)
|
||||
UpdaterService.start(appContext, url, true)
|
||||
}
|
||||
}
|
||||
.negativeButton(R.string.ignore)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
@ -8,6 +9,7 @@ import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
||||
import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
|
||||
@ -270,6 +272,20 @@ class SettingsGeneralController : SettingsController() {
|
||||
defaultValue = ""
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isUpdaterEnabled) {
|
||||
preferenceCategory {
|
||||
titleRes = R.string.auto_updates
|
||||
|
||||
intListPreference(activity) {
|
||||
key = Keys.shouldAutoUpdate
|
||||
titleRes = R.string.auto_update_app
|
||||
entryRange = 0..2
|
||||
entriesRes = arrayOf(R.string.over_any_network, R.string.over_wifi_only, R.string.dont_auto_update)
|
||||
defaultValue = AutoUpdaterJob.ONLY_ON_UNMETERED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
@ -26,6 +27,7 @@ import eu.kanade.tachiyomi.databinding.ThemesPreferenceBinding
|
||||
import eu.kanade.tachiyomi.util.system.ThemeUtil
|
||||
import eu.kanade.tachiyomi.util.system.Themes
|
||||
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
|
||||
import eu.kanade.tachiyomi.util.system.contextCompatColor
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.isInNightMode
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@ -56,14 +58,15 @@ class ThemePreference @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
selectExtensionLight = fastAdapterLight.getSelectExtension().setThemeListener(false)
|
||||
selectExtensionDark = fastAdapterDark.getSelectExtension().setThemeListener(true)
|
||||
val enumConstants = Themes.values()
|
||||
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
itemAdapterLight.set(
|
||||
enumConstants
|
||||
.filter { !it.isDarkTheme || it.followsSystem }
|
||||
.filter { (!it.isDarkTheme || it.followsSystem) && (it.styleRes != R.style.Theme_Tachiyomi_Monet || isOnA12) }
|
||||
.map { ThemeItem(it, false) }
|
||||
)
|
||||
itemAdapterDark.set(
|
||||
enumConstants
|
||||
.filter { it.isDarkTheme || it.followsSystem }
|
||||
.filter { (it.isDarkTheme || it.followsSystem) && (it.styleRes != R.style.Theme_Tachiyomi_Monet || isOnA12) }
|
||||
.map { ThemeItem(it, true) }
|
||||
)
|
||||
isSelectable = false
|
||||
@ -207,6 +210,7 @@ class ThemePreference @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
inner class ViewHolder(view: View) : FastAdapter.ViewHolder<ThemeItem>(view) {
|
||||
|
||||
val binding = ThemeItemBinding.bind(view)
|
||||
|
||||
override fun bindView(item: ThemeItem, payloads: List<Any>) {
|
||||
binding.themeNameText.setText(
|
||||
if (item.isDarkTheme) {
|
||||
@ -228,27 +232,75 @@ class ThemePreference @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
binding.themeSelected.alpha = if (themeMatchesApp) 1f else 0.5f
|
||||
binding.checkbox.alpha = if (themeMatchesApp) 1f else 0.5f
|
||||
}
|
||||
binding.themeToolbar.setBackgroundColor(item.colors.appBar)
|
||||
binding.themeAppBarText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.appBarText)
|
||||
binding.themeHeroImage.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.primaryText)
|
||||
binding.themePrimaryText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.primaryText)
|
||||
binding.themeAccentedButton.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.colorAccent)
|
||||
binding.themeSecondaryText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.secondaryText)
|
||||
binding.themeSecondaryText2.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.secondaryText)
|
||||
if (item.theme.styleRes == R.style.Theme_Tachiyomi_Monet &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
) {
|
||||
val nightMode = item.isDarkTheme
|
||||
val appBar = context.contextCompatColor(
|
||||
if (nightMode) android.R.color.system_neutral1_900
|
||||
else android.R.color.system_neutral1_50
|
||||
)
|
||||
val appBarText = context.contextCompatColor(
|
||||
if (nightMode) android.R.color.system_accent2_10
|
||||
else android.R.color.system_accent2_800
|
||||
)
|
||||
val colorAccent = context.contextCompatColor(
|
||||
if (nightMode) android.R.color.system_accent1_300
|
||||
else android.R.color.system_accent1_500
|
||||
)
|
||||
val bottomBar = context.contextCompatColor(
|
||||
if (nightMode) android.R.color.system_neutral1_800
|
||||
else android.R.color.system_accent2_100
|
||||
)
|
||||
val colorBackground = context.contextCompatColor(
|
||||
if (nightMode) android.R.color.system_neutral1_900
|
||||
else android.R.color.system_neutral1_50
|
||||
)
|
||||
|
||||
binding.themeBottomBar.setBackgroundColor(item.colors.bottomBar)
|
||||
binding.themeItem1.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.inactiveTab)
|
||||
binding.themeItem2.imageTintList = ColorStateList.valueOf(item.colors.activeTab)
|
||||
binding.themeItem3.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.inactiveTab)
|
||||
binding.themeLayout.setBackgroundColor(item.colors.colorBackground)
|
||||
binding.themeToolbar.setBackgroundColor(appBar)
|
||||
binding.themeAppBarText.imageTintList =
|
||||
ColorStateList.valueOf(appBarText)
|
||||
binding.themeHeroImage.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.primaryText)
|
||||
binding.themePrimaryText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.primaryText)
|
||||
binding.themeAccentedButton.imageTintList =
|
||||
ColorStateList.valueOf(colorAccent)
|
||||
binding.themeSecondaryText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.secondaryText)
|
||||
binding.themeSecondaryText2.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.secondaryText)
|
||||
|
||||
binding.themeBottomBar.setBackgroundColor(bottomBar)
|
||||
binding.themeItem1.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.inactiveTab)
|
||||
binding.themeItem2.imageTintList = ColorStateList.valueOf(colorAccent)
|
||||
binding.themeItem3.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.inactiveTab)
|
||||
binding.themeLayout.setBackgroundColor(colorBackground)
|
||||
} else {
|
||||
binding.themeToolbar.setBackgroundColor(item.colors.appBar)
|
||||
binding.themeAppBarText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.appBarText)
|
||||
binding.themeHeroImage.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.primaryText)
|
||||
binding.themePrimaryText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.primaryText)
|
||||
binding.themeAccentedButton.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.colorAccent)
|
||||
binding.themeSecondaryText.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.secondaryText)
|
||||
binding.themeSecondaryText2.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.secondaryText)
|
||||
|
||||
binding.themeBottomBar.setBackgroundColor(item.colors.bottomBar)
|
||||
binding.themeItem1.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.inactiveTab)
|
||||
binding.themeItem2.imageTintList = ColorStateList.valueOf(item.colors.activeTab)
|
||||
binding.themeItem3.imageTintList =
|
||||
ColorStateList.valueOf(item.colors.inactiveTab)
|
||||
binding.themeLayout.setBackgroundColor(item.colors.colorBackground)
|
||||
}
|
||||
if (item.isDarkTheme && preferences.themeDarkAmoled().get()) {
|
||||
binding.themeLayout.setBackgroundColor(Color.BLACK)
|
||||
if (!ThemeUtil.isColoredTheme(item.theme)) {
|
||||
|
@ -11,6 +11,12 @@ import kotlin.math.roundToInt
|
||||
|
||||
@Suppress("unused")
|
||||
enum class Themes(@StyleRes val styleRes: Int, val nightMode: Int, @StringRes val nameRes: Int, @StringRes altNameRes: Int? = null) {
|
||||
MONET(
|
||||
R.style.Theme_Tachiyomi_Monet,
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
|
||||
R.string.a_brighter_you,
|
||||
R.string.a_calmer_you
|
||||
),
|
||||
DEFAULT(
|
||||
R.style.Theme_Tachiyomi,
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
|
||||
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="1.00" android:color="?colorAccent" android:state_checkable="true" android:state_checked="true" android:state_enabled="true"/>
|
||||
<item android:alpha="0.60" android:color="?colorOnSurface" android:state_checkable="true" android:state_checked="false" android:state_enabled="true"/>
|
||||
<item android:alpha="1.00" android:color="?colorAccent" android:state_enabled="true"/>
|
||||
<item android:alpha="0.38" android:color="?colorOnSurface"/>
|
||||
</selector>
|
171
app/src/main/res/drawable/anim_tachij2k_splash.xml
Normal file
171
app/src/main/res/drawable/anim_tachij2k_splash.xml
Normal file
@ -0,0 +1,171 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<group
|
||||
android:name="main_group"
|
||||
android:pivotX="128"
|
||||
android:pivotY="128"
|
||||
android:scaleX="0.5"
|
||||
android:scaleY="0.5">
|
||||
<group
|
||||
android:name="group"
|
||||
android:translateY="32">
|
||||
<path
|
||||
android:name="crown"
|
||||
android:pathData="M 159.8 21.4 C 155.9 21.4 152.7 24.6 152.7 28.5 L 152.7 28.9 C 149.2 29.2 145 30.4 141.5 34 C 141.5 34 134.2 27.2 130.1 18.4 C 132.7 17.3 134.5 14.8 134.5 11.9 C 134.5 8 131.3 4.8 127.4 4.8 C 123.5 4.8 120.3 8 120.3 11.9 C 120.3 14.9 122.1 17.4 124.8 18.5 C 120.8 27.2 113.5 34 113.5 34 C 110.3 30.8 106.6 29.5 103.4 29.1 L 103.4 28.6 C 103.4 24.7 100.2 21.5 96.3 21.5 C 92.4 21.5 89.2 24.7 89.2 28.6 C 89.2 32.5 92.4 35.7 96.3 35.7 C 97.1 35.7 97.8 35.6 98.5 35.4 C 104 46.8 105.7 59.9 105.7 59.9 L 149.4 59.9 C 149.4 59.9 151.1 46.5 156.7 35 C 157.6 35.5 158.7 35.7 159.8 35.7 C 163.7 35.7 166.9 32.5 166.9 28.6 C 166.9 24.7 163.7 21.4 159.8 21.4 Z"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0"/>
|
||||
</group>
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 231.052 55.693 C 231.059 55.696 231.065 55.699 231.071 55.702 C 231.156 55.742 231.233 55.812 231.303 55.922 C 231.945 56.931 232 61.242 232 75.2 L 232 95.3 L 232 95.3 C 232 95.3 232 95.3 232 95.3 C 232 95.3 232 95.3 232 95.3 L 232 95.3 L 232 95.3 C 232 95.3 229.5 56.3 230.4 55.8 C 230.4 55.8 230.4 55.8 230.4 55.8 C 230.591 55.711 230.759 55.654 230.907 55.662 C 230.932 55.663 230.956 55.666 230.98 55.671 C 231.005 55.676 231.029 55.684 231.052 55.693"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 165.7 106.2 C 162.6 126.1 150.9 164.7 138.4 196.4 L 131.8 212.8 L 129.1 239.7 L 157.2 236.8 C 157.2 236.8 173.3 211.9 173.3 211.6 C 173.6 211 175.9 206 178.3 200.5 C 180.7 195 185.4 183.1 188.7 174 C 195.1 156.5 209 112.9 209 110.4 C 209 109.2 204.6 107.7 188.9 103.8 C 177.8 101 168.2 98.9 167.6 98.9 C 167.2 98.9 166.3 102.2 165.7 106.2 Z"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:name="path_5"
|
||||
android:pathData="M 62.8 105.7 C 52.9 109.4 44.9 113 44.9 113.6 C 44.9 114.3 46.2 117.7 47.8 121.3 C 55.4 138.1 67.4 179.7 70.7 200.7 C 71.3 204.8 72.2 208.1 72.8 208.1 C 73.4 208.1 82.7 205.1 93.6 201.4 C 107.7 196.5 113.3 194.2 113.3 193.1 C 113.3 190.1 96.4 135.2 88.4 112.4 C 84.8 102.3 83.3 98.9 82 99 C 81.2 99 72.6 102 62.8 105.7 Z"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:name="path_6"
|
||||
android:pathData="M 12.6 212.8 L 12.6 212.8 L 12.6 250.8 L 12.6 250.8 L 12.6 213.609 L 12.6 212.8"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:pathData="M 126.6 93.2 C 198.2 93.2 223.5 93.9 229.9 95.7 L 232 96.3 L 232 76.2 C 232 57.4 231.9 56.1 230.4 56.8 C 229.5 57.3 210.5 57.7 188.2 57.9 L 149.7 58.2 L 149 34.9 L 106.1 34.9 L 105.2 58.3 L 65 57.9 C 42.8 57.7 23.8 57.2 22.9 56.8 C 21.3 56 21.2 57.4 21.2 76.2 L 21.2 96.3 L 23.4 95.7 C 29.7 93.9 55 93.2 126.6 93.2 Z M 82 100 C 81.2 100 72.6 103 62.8 106.7 C 52.9 110.4 44.9 114 44.9 114.6 C 44.9 115.3 46.2 118.7 47.8 122.3 C 55.4 139.1 67.4 180.7 70.7 201.7 C 71.3 205.8 72.2 209.1 72.8 209.1 C 73.4 209.1 82.7 206.1 93.6 202.4 C 107.7 197.5 113.3 195.2 113.3 194.1 C 113.3 191.1 96.4 136.2 88.4 113.4 C 84.8 103.3 83.3 99.9 82 100 Z"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0.25"/>
|
||||
<path
|
||||
android:name="path_4"
|
||||
android:pathData="M 172.6 213.8 C 173.1 213.1 173.3 212.6 173.3 212.6 C 173.6 212 175.9 207 178.3 201.5 C 180.7 196 185.4 184.1 188.7 175 C 195.1 157.5 209 113.9 209 111.4 C 209 110.2 204.6 108.7 188.9 104.8 C 177.8 102 168.2 99.9 167.6 99.9 C 167.2 99.9 166.3 103.2 165.7 107.2 C 162.6 127.1 150.9 165.7 138.4 197.4 L 131.8 213.8 L 12.6 213.8 L 12.6 251.8 L 243.4 251.8 L 243.4 213.8 L 172.6 213.8 Z"
|
||||
android:fillColor="@color/splashIcon"
|
||||
android:fillAlpha="0.25"/>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_1">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="425"
|
||||
android:duration="250"
|
||||
android:valueFrom="M 131.2 214.4 C 131.185 214.554 131.169 214.708 131.154 214.863 L 128.5 241.3 L 156.7 238.4 L 172.8 213.2 C 172.8 213.2 172.8 213.2 172.8 213.2 C 172.8 213.2 172.8 213.2 172.8 213.2 C 172.8 213.2 172.8 213.2 172.8 213.2 C 172.8 213.2 172.8 213.2 172.8 213.2 C 165.68 213.405 158.56 213.611 151.44 213.816 C 144.916 214.004 138.393 214.193 131.869 214.381 C 131.646 214.387 131.423 214.394 131.2 214.4 L 131.2 214.4"
|
||||
android:valueTo="M 165.7 106.2 C 162.6 126.1 150.9 164.7 138.4 196.4 L 131.8 212.8 L 129.1 239.7 L 157.2 236.8 C 157.2 236.8 173.3 211.9 173.3 211.6 C 173.6 211 175.9 206 178.3 200.5 C 180.7 195 185.4 183.1 188.7 174 C 195.1 156.5 209 112.9 209 110.4 C 209 109.2 204.6 107.7 188.9 103.8 C 177.8 101 168.2 98.9 167.6 98.9 C 167.2 98.9 166.3 102.2 165.7 106.2 L 165.7 106.2"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="450"
|
||||
android:duration="1"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_6">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="250"
|
||||
android:duration="250"
|
||||
android:valueFrom="M 12.6 212.8 L 12.6 212.8 L 12.6 250.8 L 12.6 250.8 L 12.6 213.609 L 12.6 212.8"
|
||||
android:valueTo="M 72.3 212.8 L 12.6 212.8 L 12.6 250.8 L 243.4 250.8 L 243.4 212.8 L 72.3 212.8"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"/>
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="250"
|
||||
android:duration="1"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="250"
|
||||
android:duration="250"
|
||||
android:valueFrom="M 231.052 55.693 C 231.059 55.696 231.065 55.699 231.071 55.702 C 231.156 55.742 231.233 55.812 231.303 55.922 C 231.945 56.931 232 61.242 232 75.2 L 232 95.3 L 232 95.3 C 232 95.3 232 95.3 232 95.3 C 232 95.3 232 95.3 232 95.3 L 232 95.3 L 232 95.3 C 232 95.3 229.5 56.3 230.4 55.8 C 230.4 55.8 230.4 55.8 230.4 55.8 C 230.591 55.711 230.759 55.654 230.907 55.662 C 230.932 55.663 230.956 55.666 230.98 55.671 C 231.005 55.676 231.029 55.684 231.052 55.693"
|
||||
android:valueTo="M 105.6 57.3 C 92.067 57.167 78.533 57.033 65 56.9 C 42.8 56.7 23.8 56.2 22.9 55.8 C 21.3 55 21.2 56.4 21.2 75.2 L 21.2 95.3 L 23.4 94.7 C 29.7 92.9 55 92.2 126.6 92.2 C 198.2 92.2 223.5 92.9 229.9 94.7 L 232 95.3 L 232 75.2 C 232 56.4 231.9 55.1 230.4 55.8 C 229.5 56.3 210.5 56.7 188.2 56.9 C 174.7 57 161.2 57.1 147.7 57.2 C 133.667 57.233 119.633 57.267 105.6 57.3 C 105.6 57.3 105.6 57.3 105.6 57.3"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"/>
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="250"
|
||||
android:duration="1"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_5">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="450"
|
||||
android:duration="225"
|
||||
android:valueFrom="M 62.8 105.7 C 52.9 109.4 44.9 113 44.9 113.6 C 44.9 113.6 44.9 113.6 44.9 113.6 C 44.9 113.6 44.9 113.6 44.9 113.6 C 44.9 113.6 44.9 113.6 44.9 113.6 C 44.9 113.624 44.945 113.629 45.033 113.616 C 47.46 113.265 82.363 99.262 82.03 99.003 C 82.026 99 82.016 98.999 82 99 C 82 99 82 99 82 99 C 81.2 99 72.6 102 62.8 105.7 Z"
|
||||
android:valueTo="M 62.8 105.7 C 52.9 109.4 44.9 113 44.9 113.6 C 44.9 114.3 46.2 117.7 47.8 121.3 C 55.4 138.1 67.4 179.7 70.7 200.7 C 71.3 204.8 72.2 208.1 72.8 208.1 C 73.4 208.1 82.7 205.1 93.6 201.4 C 107.7 196.5 113.3 194.2 113.3 193.1 C 113.3 190.1 96.4 135.2 88.4 112.4 C 84.8 102.3 83.3 98.9 82 99 C 81.2 99 72.6 102 62.8 105.7 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="450"
|
||||
android:duration="1"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="translateY"
|
||||
android:startOffset="550"
|
||||
android:duration="225"
|
||||
android:valueFrom="32"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/linear_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="crown">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="550"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
@ -24,6 +24,25 @@
|
||||
android:layout_marginTop="20dp"
|
||||
tools:text="Title"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ext_button"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/accent_text_btn_color_selector"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/title"
|
||||
app:layout_constraintBottom_toBottomOf="@id/title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toEndOf="@id/title"
|
||||
app:layout_constraintTop_toTopOf="@id/title"
|
||||
app:rippleColor="@color/fullRippleColor"
|
||||
android:text="@string/update_all" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
15
app/src/main/res/values-night-v31/themes.xml
Normal file
15
app/src/main/res/values-night-v31/themes.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Tachiyomi.Monet">
|
||||
<item name="colorPrimary">@android:color/system_accent2_800</item>
|
||||
<item name="colorAccent">@android:color/system_accent1_300</item>
|
||||
<item name="colorAccentText">@android:color/system_accent1_200</item>
|
||||
<item name="colorPrimaryVariant">@android:color/system_neutral1_800</item>
|
||||
<item name="colorSecondary">@android:color/system_neutral1_900</item>
|
||||
<item name="background">@android:color/system_neutral1_900</item>
|
||||
<item name="android:textColorPrimary">@android:color/system_accent2_10</item>
|
||||
<item name="android:colorBackground">@android:color/system_neutral1_900</item>
|
||||
<item name="actionBarTintColor">@android:color/system_accent2_10</item>
|
||||
<item name="colorOnAccent">@android:color/system_neutral2_900</item>
|
||||
</style>
|
||||
</resources>
|
@ -18,7 +18,7 @@
|
||||
<color name="divider">@color/md_white_1000_12</color>
|
||||
<color name="download">@color/material_green_700</color>
|
||||
<color name="holo_red">#cc4444</color>
|
||||
|
||||
<color name="splashIcon">@color/md_white_1000</color>
|
||||
|
||||
<color name="background">#1C1C1D</color>
|
||||
<color name="dialog">#212121</color>
|
||||
|
14
app/src/main/res/values-v31/themes.xml
Normal file
14
app/src/main/res/values-v31/themes.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Tachiyomi.Monet">
|
||||
<item name="colorPrimary">@android:color/system_accent2_100</item>
|
||||
<item name="colorAccent">@android:color/system_accent1_500</item>
|
||||
<item name="colorAccentText">@android:color/system_accent1_800</item>
|
||||
<item name="colorPrimaryVariant">@android:color/system_accent2_100</item>
|
||||
<item name="colorSecondary">@android:color/system_neutral1_50</item>
|
||||
<item name="background">@android:color/system_neutral1_50</item>
|
||||
<item name="android:textColorPrimary">@android:color/system_neutral1_900</item>
|
||||
<item name="android:colorBackground">@android:color/system_neutral1_50</item>
|
||||
<item name="actionBarTintColor">@android:color/system_accent2_800</item>
|
||||
</style>
|
||||
</resources>
|
@ -34,6 +34,7 @@
|
||||
|
||||
<color name="background">@color/md_grey_50</color>
|
||||
<color name="dialog">@color/md_white_1000</color>
|
||||
<color name="splashIcon">@color/md_black_1000</color>
|
||||
|
||||
<!-- Text Colors -->
|
||||
<color name="md_black_1000_87">#DE000000</color>
|
||||
|
@ -117,6 +117,8 @@
|
||||
<string name="no_new_updates_available">No new updates available</string>
|
||||
<string name="searching_for_updates">Searching for updates…</string>
|
||||
<string name="release_page">Release page</string>
|
||||
<string name="could_not_install_update">Could not install update</string>
|
||||
<string name="update_completed">Update completed</string>
|
||||
|
||||
<!-- Main Screens -->
|
||||
|
||||
@ -312,6 +314,9 @@
|
||||
<string name="app_info">App info</string>
|
||||
<string name="_must_be_enabled_first">%1$s must be enabled first</string>
|
||||
<string name="could_not_install_extension">Could not install extension</string>
|
||||
<string name="update_all">Update all</string>
|
||||
<string name="some_extensions_may_prompt">Some extensions may still prompt to be installed first.</string>
|
||||
<string name="updating_extensions">Updating extensions</string>
|
||||
<plurals name="_updates_pending">
|
||||
<item quantity="one">%d update pending</item>
|
||||
<item quantity="other">%d updates pending</item>
|
||||
@ -625,6 +630,8 @@
|
||||
<string name="midnight_dusk">Midnight Dusk</string>
|
||||
<string name="spring_blossom">Spring Blossom</string>
|
||||
<string name="strawberry_daiquiri">Strawberry Daiquiri</string>
|
||||
<string name="a_brighter_you">A Brighter You</string>
|
||||
<string name="a_calmer_you">A Calmer You</string>
|
||||
<string name="yotsuba">Yotsuba</string>
|
||||
<string name="yin">Yin</string>
|
||||
<string name="yang">Yang</string>
|
||||
@ -651,6 +658,11 @@
|
||||
<string name="starting_screen">Starting screen</string>
|
||||
<string name="back_to_start">Back to start</string>
|
||||
<string name="pressing_back_to_start">Pressing back to starting screen</string>
|
||||
<string name="auto_updates">Auto-updates</string>
|
||||
<string name="auto_update_app">Auto-update app</string>
|
||||
<string name="over_wifi_only">Over Wi-Fi only</string>
|
||||
<string name="over_any_network">Over any network</string>
|
||||
<string name="dont_auto_update">Don\'t auto-update</string>
|
||||
|
||||
<string name="app_shortcuts">App shortcuts</string>
|
||||
<string name="show_recent_sources">Show recently used sources</string>
|
||||
@ -980,5 +992,6 @@
|
||||
<string name="use_default">Use default</string>
|
||||
<string name="view_all_errors">View all errors</string>
|
||||
<string name="view_chapters">View chapters</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
</resources>
|
||||
|
@ -10,6 +10,9 @@
|
||||
<item name="android:forceDarkAllowed" tools:targetApi="29">false</item>
|
||||
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:windowSplashScreenBackground" tools:targetApi="31">@color/colorPrimary</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="31">@drawable/anim_tachij2k_splash</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="31">775</item>
|
||||
<item name="colorPrimary">@color/app_color_primary</item>
|
||||
<item name="colorPrimaryVariant">@color/colorPrimary</item>
|
||||
<item name="colorSecondary">@color/background</item>
|
||||
@ -105,6 +108,7 @@
|
||||
<item name="colorAccentText">@color/colorAccentYinyangText</item>
|
||||
<item name="colorOnAccent">@color/colorOnAccentYinyang</item>
|
||||
</style>
|
||||
<style name="Theme.Tachiyomi.Monet"/>
|
||||
|
||||
<!--===============-->
|
||||
<!-- Launch Screen -->
|
||||
|
@ -93,7 +93,7 @@ object LegacyPluginClassPath {
|
||||
}
|
||||
|
||||
object AndroidVersions {
|
||||
const val compileSdk = 30
|
||||
const val compileSdk = 31
|
||||
const val minSdk = 23
|
||||
const val targetSdk = 30
|
||||
const val versionCode = 77
|
||||
|
Loading…
Reference in New Issue
Block a user