Migrate to Metro DI (#3608)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-17 00:47:48 +06:00
committed by GitHub
parent 1e5b1dc5e7
commit b2015d1ef3
314 changed files with 2416 additions and 2010 deletions
+66 -33
View File
@@ -22,21 +22,21 @@ import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import dev.mihon.injekt.patchInjekt
import eu.kanade.domain.DomainModule
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.createGraphFactory
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.ImageDecoder
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.di.AppModule
import eu.kanade.tachiyomi.di.PreferenceModule
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
@@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isDebugBuildType
import eu.kanade.tachiyomi.util.system.notify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
@@ -51,34 +52,68 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.app.di.AppGraph
import mihon.app.di.injekt.MetroInteropModule
import mihon.core.metro.GraphProvider
import mihon.core.migration.Migration
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import mihon.telemetry.TelemetryConfig
import org.conscrypt.Conscrypt
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.addSingleton
import java.security.Security
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory, GraphProvider<AppGraph> {
private val basePreferences: BasePreferences by injectLazy()
private val privacyPreferences: PrivacyPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
override val graph: AppGraph by lazy {
createGraphFactory<AppGraph.Factory>().create(context = this, isDebugBuild = isDebugBuildType)
}
@Inject private lateinit var preferenceStore: PreferenceStore
@Inject private lateinit var basePreferences: BasePreferences
@Inject private lateinit var privacyPreferences: PrivacyPreferences
@Inject private lateinit var networkPreferences: NetworkPreferences
@Inject private lateinit var uiPreferences: UiPreferences
@Inject private lateinit var coverCache: CoverCache
@Inject private lateinit var networkHelper: NetworkHelper
@Inject private lateinit var sourceManager: SourceManager
@Inject private lateinit var widgetManager: WidgetManager
@Inject private lateinit var injektMetroInteropModule: MetroInteropModule
@Inject private lateinit var migrations: Set<Migration>
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@SuppressLint("LaunchActivityFromNotification")
override fun onCreate() {
super<Application>.onCreate()
patchInjekt()
// Must run before the graph is built, since injecting dependencies initializes WebView and the
// suffix can't be set once a provider exists in the process. Secondary processes die otherwise.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
graph.inject(this)
setupInjekt()
TelemetryConfig.init(applicationContext)
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
@@ -88,16 +123,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
// Avoid potential crashes
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
Injekt.importModule(PreferenceModule(this))
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@@ -143,10 +168,10 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
.onEach(TelemetryConfig::setCrashlyticsEnabled)
.launchIn(scope)
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode.get())
setAppCompatDelegateThemeMode(uiPreferences.themeMode.get())
// Updates widget update
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
widgetManager.init(scope)
if (!LogcatLogger.isInstalled) {
val minLogPriority = when {
@@ -161,14 +186,22 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
initializeMigrator()
}
private fun setupInjekt() {
patchInjekt()
Injekt.addSingleton<Application>(this)
Injekt.addSingleton<Context>(this)
Injekt.importModule(injektMetroInteropModule)
}
private fun initializeMigrator() {
val preferenceStore = Injekt.get<PreferenceStore>()
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
logcat {
"Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE} with ${migrations.size} migration(s)"
}
Migrator.initialize(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
migrations = migrations.toList(),
onMigrationComplete = {
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
@@ -178,7 +211,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun newImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(this).apply {
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
val callFactoryLazy = lazy { networkHelper.client }
components {
// NetworkFetcher.Factory
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
@@ -186,10 +219,10 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
add(ImageDecoder.Factory())
// Fetcher.Factory
add(BufferedSourceFetcher.Factory())
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, coverCache, sourceManager))
add(MangaCoverFetcher.MangaFactory(callFactoryLazy, coverCache, sourceManager))
// Keyer
add(MangaCoverKeyer())
add(MangaCoverKeyer(coverCache))
add(MangaKeyer())
}
@@ -211,11 +244,11 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
}
override fun onStart(owner: LifecycleOwner) {
SecureActivityDelegate.onApplicationStart()
SecureActivityDelegate.onApplicationStart(this)
}
override fun onStop(owner: LifecycleOwner) {
SecureActivityDelegate.onApplicationStopped()
SecureActivityDelegate.onApplicationStopped(this)
}
override fun getPackageName(): String {
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.Backup
import kotlinx.serialization.SerializationException
import kotlinx.serialization.protobuf.ProtoBuf
@@ -10,13 +11,12 @@ import okio.gzip
import okio.source
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
@Inject
class BackupDecoder(
private val context: Context,
private val parser: ProtoBuf = Injekt.get(),
private val parser: ProtoBuf,
) {
/**
* Decode a potentially-gzipped backup.
@@ -1,17 +1,15 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.track.TrackerManager
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class BackupFileValidator(
private val context: Context,
private val sourceManager: SourceManager = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
private val sourceManager: SourceManager,
private val trackerManager: TrackerManager,
private val backupDecoder: BackupDecoder,
) {
/**
@@ -21,7 +19,7 @@ class BackupFileValidator(
*/
fun validate(uri: Uri): Results {
val backup = try {
BackupDecoder(context).decode(uri)
backupDecoder.decode(uri)
} catch (e: Exception) {
throw IllegalStateException(e)
}
@@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import com.hippo.unifile.UniFile
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@@ -16,14 +17,14 @@ import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.io.File
import kotlin.time.Duration.Companion.milliseconds
class BackupNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
@Inject
class BackupNotifier(
private val context: Context,
private val preferences: SecurityPreferences,
) {
private val progressNotificationBuilder = context.notificationBuilder(
Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS,
) {
@@ -16,6 +16,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.hippo.unifile.UniFile
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -24,22 +25,33 @@ import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import mihon.app.di.AppGraph
import mihon.app.di.appGraph
import mihon.core.metro.metroGraph
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.storage.service.StorageManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = BackupNotifier(context)
private val graph: AppGraph = context.metroGraph()
@Inject
private lateinit var backupCreatorFactory: BackupCreator.Factory
@Inject
private lateinit var storageManager: StorageManager
@Inject private lateinit var notifier: BackupNotifier
override suspend fun doWork(): Result {
graph.inject(this)
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry()
if (isAutoBackup && BackupRestoreJob.isRunning(context.workManager)) return Result.retry()
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: getAutomaticBackupLocation()
@@ -51,7 +63,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
?: BackupOptions()
return try {
val location = BackupCreator(context, isAutoBackup).backup(uri, options)
val location = backupCreatorFactory.create(isAutoBackup = isAutoBackup).backup(uri, options)
if (!isAutoBackup) {
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
}
@@ -78,7 +90,6 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
}
private fun getAutomaticBackupLocation(): Uri? {
val storageManager = Injekt.get<StorageManager>()
return storageManager.getAutomaticBackupsDirectory()?.uri
}
@@ -88,7 +99,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
}
fun setupTask(context: Context, prefInterval: Int? = null) {
val backupPreferences = Injekt.get<BackupPreferences>()
val backupPreferences = context.appGraph.backupPreferences
val interval = prefInterval ?: backupPreferences.backupInterval.get()
if (interval > 0) {
val constraints = Constraints(
@@ -3,6 +3,9 @@ package eu.kanade.tachiyomi.data.backup.create
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
@@ -29,29 +32,31 @@ import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.time.Clock
@AssistedInject
class BackupCreator(
@Assisted private val isAutoBackup: Boolean,
private val context: Context,
private val isAutoBackup: Boolean,
private val parser: ProtoBuf = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(),
private val backupPreferences: BackupPreferences = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(),
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
private val parser: ProtoBuf,
private val getFavorites: GetFavorites,
private val backupPreferences: BackupPreferences,
private val mangaRepository: MangaRepository,
private val categoriesBackupCreator: CategoriesBackupCreator,
private val mangaBackupCreator: MangaBackupCreator,
private val preferenceBackupCreator: PreferenceBackupCreator,
private val extensionStoresBackupCreator: ExtensionStoresBackupCreator,
private val sourcesBackupCreator: SourcesBackupCreator,
private val backupFileValidator: BackupFileValidator,
) {
@AssistedFactory
fun interface Factory {
fun create(isAutoBackup: Boolean): BackupCreator
}
suspend fun backup(uri: Uri, options: BackupOptions): String {
var file: UniFile? = null
@@ -105,7 +110,7 @@ class BackupCreator(
val fileUri = file.uri
// Make sure it's a valid backup file
BackupFileValidator(context).validate(fileUri)
backupFileValidator.validate(fileUri)
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp.set(Clock.System.now().toEpochMilliseconds())
@@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.data.backup.create.creators
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class CategoriesBackupCreator(
private val getCategories: GetCategories = Injekt.get(),
private val getCategories: GetCategories,
) {
suspend operator fun invoke(): List<BackupCategory> {
@@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.data.backup.create.creators
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
import eu.kanade.tachiyomi.data.backup.models.backupExtensionStoreMapper
import mihon.domain.extension.interactor.GetExtensionStores
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class ExtensionStoresBackupCreator(
private val getExtensionStores: GetExtensionStores = Injekt.get(),
private val getExtensionStores: GetExtensionStores,
) {
suspend operator fun invoke(): List<BackupExtensionStore> {
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup.create.creators
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
@@ -14,13 +15,12 @@ import tachiyomi.data.MemoColumnAdapter
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class MangaBackupCreator(
private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getHistory: GetHistory = Injekt.get(),
private val database: Database,
private val getCategories: GetCategories,
private val getHistory: GetHistory,
) {
suspend operator fun invoke(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.backup.create.creators
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
@@ -14,12 +15,11 @@ import eu.kanade.tachiyomi.source.sourcePreferences
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class PreferenceBackupCreator(
private val sourceManager: SourceManager = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(),
private val sourceManager: SourceManager,
private val preferenceStore: PreferenceStore,
) {
fun createApp(includePrivatePreferences: Boolean): List<BackupPreference> {
@@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.data.backup.create.creators
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class SourcesBackupCreator(
private val sourceManager: SourceManager = Injekt.get(),
private val sourceManager: SourceManager,
) {
operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
@@ -9,16 +9,19 @@ import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException
import logcat.LogPriority
import mihon.app.di.AppGraph
import mihon.core.metro.metroGraph
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
@@ -26,9 +29,15 @@ import tachiyomi.i18n.MR
class BackupRestoreJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = BackupNotifier(context)
private val graph: AppGraph = context.metroGraph()
@Inject private lateinit var backupRestorerFactory: BackupRestorer.Factory
@Inject private lateinit var notifier: BackupNotifier
override suspend fun doWork(): Result {
graph.inject(this)
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { RestoreOptions.fromBooleanArray(it) }
@@ -41,7 +50,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
setForegroundSafely()
return try {
BackupRestorer(context, notifier, isSync).restore(uri, options)
backupRestorerFactory.create(notifier = notifier, isSync = isSync).restore(uri, options)
Result.success()
} catch (e: Exception) {
if (e is CancellationException) {
@@ -70,12 +79,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
}
companion object {
fun isRunning(context: Context): Boolean {
return context.workManager.isRunning(TAG)
fun isRunning(workManager: WorkManager): Boolean {
return workManager.isRunning(TAG)
}
fun start(
context: Context,
workManager: WorkManager,
uri: Uri,
options: RestoreOptions,
sync: Boolean = false,
@@ -89,11 +98,11 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
.addTag(TAG)
.setInputData(inputData)
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
}
fun stop(context: Context) {
context.workManager.cancelUniqueWork(TAG)
fun stop(workManager: WorkManager) {
workManager.cancelUniqueWork(TAG)
}
}
}
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.backup.restore
import android.content.Context
import android.net.Uri
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import eu.kanade.tachiyomi.data.backup.BackupDecoder
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
@@ -24,8 +27,6 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.Database
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
@@ -36,18 +37,25 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
@OptIn(ExperimentalAtomicApi::class)
@AssistedInject
class BackupRestorer(
@Assisted private val notifier: BackupNotifier,
@Assisted private val isSync: Boolean,
private val context: Context,
private val notifier: BackupNotifier,
private val isSync: Boolean,
private val database: Database = Injekt.get(),
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(),
private val mangaRestorer: MangaRestorer = MangaRestorer(),
private val database: Database,
private val downloadCache: DownloadCache,
private val categoriesRestorer: CategoriesRestorer,
private val preferenceRestorer: PreferenceRestorer,
private val extensionStoreRestorer: ExtensionStoreRestorer,
private val mangaRestorer: MangaRestorer,
private val backupDecoder: BackupDecoder,
) {
@AssistedFactory
fun interface Factory {
fun create(notifier: BackupNotifier, isSync: Boolean): BackupRestorer
}
private var restoreAmount = 0
private val restoreProgress = AtomicInt(0)
private val errors = CopyOnWriteArrayList<Pair<Date, String>>()
@@ -65,7 +73,7 @@ class BackupRestorer(
// Invalidate download cache to ensure UI reflects any restored downloads
if (options.libraryEntries) {
try {
Injekt.get<DownloadCache>().invalidateCache()
downloadCache.invalidateCache()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to invalidate download cache after restore" }
}
@@ -85,7 +93,7 @@ class BackupRestorer(
}
private suspend fun restoreFromFile(uri: Uri, options: RestoreOptions) {
val backup = BackupDecoder(context).decode(uri)
val backup = backupDecoder.decode(uri)
// Store source mapping for error messages
val backupMaps = backup.backupSources
@@ -1,16 +1,16 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import tachiyomi.data.Database
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class CategoriesRestorer(
private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val database: Database,
private val getCategories: GetCategories,
private val libraryPreferences: LibraryPreferences,
) {
suspend operator fun invoke(backupCategories: List<BackupCategory>) {
@@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
import tachiyomi.data.Database
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class ExtensionStoreRestorer(
private val database: Database = Injekt.get(),
private val database: Database,
) {
suspend operator fun invoke(
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import dev.zacsweers.metro.Inject
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
@@ -23,21 +24,20 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import kotlin.math.max
import kotlin.time.Clock
@Inject
class MangaRestorer(
private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
fetchInterval: FetchInterval = Injekt.get(),
private val database: Database,
private val getCategories: GetCategories,
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val updateManga: UpdateManga,
private val getTracks: GetTracks,
private val insertTrack: InsertTrack,
fetchInterval: FetchInterval,
) {
private val timeZone = TimeZone.currentSystemDefault()
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers
import android.content.Context
import android.util.Log
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
@@ -21,13 +22,12 @@ import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
class PreferenceRestorer(
private val context: Context,
private val getCategories: GetCategories = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(),
private val getCategories: GetCategories,
private val preferenceStore: PreferenceStore,
) {
suspend fun restoreApp(
preferences: List<BackupPreference>,
@@ -45,7 +45,7 @@ class PreferenceRestorer(
suspend fun restoreSource(preferences: List<BackupSourcePreferences>) {
preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
val sourcePrefs = AndroidPreferenceStore(sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs)
}
}
@@ -3,6 +3,9 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import com.jakewharton.disklrucache.DiskLruCache
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
@@ -24,6 +27,8 @@ import java.io.IOException
*
* @param context the application context.
*/
@Inject
@SingleIn(AppScope::class)
class ChapterCache(
private val context: Context,
private val json: Json,
@@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.domain.manga.model.Manga
import java.io.File
@@ -15,6 +18,8 @@ import java.io.InputStream
* @param context the application context.
* @constructor creates an instance of the cover cache.
*/
@Inject
@SingleIn(AppScope::class)
class CoverCache(private val context: Context) {
companion object {
@@ -31,7 +31,6 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
@@ -299,11 +298,10 @@ class MangaCoverFetcher(
class MangaFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val coverCache: CoverCache,
private val sourceManager: SourceManager,
) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.thumbnailUrl,
@@ -321,11 +319,10 @@ class MangaCoverFetcher(
class MangaCoverFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val coverCache: CoverCache,
private val sourceManager: SourceManager,
) : Fetcher.Factory<MangaCover> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.url,
@@ -5,8 +5,6 @@ import coil3.request.Options
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import tachiyomi.domain.manga.model.MangaCover
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import tachiyomi.domain.manga.model.Manga as DomainManga
class MangaKeyer : Keyer<DomainManga> {
@@ -20,7 +18,7 @@ class MangaKeyer : Keyer<DomainManga> {
}
class MangaCoverKeyer(
private val coverCache: CoverCache = Injekt.get(),
private val coverCache: CoverCache,
) : Keyer<MangaCover> {
override fun key(data: MangaCover, options: Options): String {
return if (coverCache.getCustomCoverFile(data.mangaId).exists()) {
@@ -1,9 +1,11 @@
package eu.kanade.tachiyomi.data.download
import android.app.Application
import android.content.Context
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.CancellationException
@@ -61,12 +63,14 @@ import kotlin.time.Duration.Companion.seconds
* defined in [renewInterval] as we don't have any control over the filesystem and the user can
* delete the folders at any time without the app noticing.
*/
@Inject
@SingleIn(AppScope::class)
class DownloadCache(
private val context: Context,
private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(),
private val provider: DownloadProvider,
private val sourceManager: SourceManager,
private val extensionManager: ExtensionManager,
private val storageManager: StorageManager,
) {
private val scope = CoroutineScope(Dispatchers.IO)
@@ -492,7 +496,7 @@ private object UniFileAsStringSerializer : KSerializer<UniFile?> {
override fun deserialize(decoder: Decoder): UniFile? {
return if (decoder.decodeNotNullMark()) {
UniFile.fromUri(Injekt.get<Application>(), decoder.decodeString().toUri())
UniFile.fromUri(Injekt.get<Context>(), decoder.decodeString().toUri())
} else {
decoder.decodeNull()
}
@@ -11,6 +11,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.NetworkState
@@ -24,9 +25,9 @@ import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import mihon.app.di.AppGraph
import mihon.core.metro.metroGraph
import tachiyomi.domain.download.service.DownloadPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This worker is used to manage the downloader. The system can decide to stop the worker, in
@@ -34,8 +35,15 @@ import uy.kohesive.injekt.api.get
*/
class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
private val downloadManager: DownloadManager = Injekt.get()
private val downloadPreferences: DownloadPreferences = Injekt.get()
private val graph: AppGraph = context.metroGraph()
@Inject private lateinit var downloadManager: DownloadManager
@Inject private lateinit var downloadPreferences: DownloadPreferences
init {
graph.inject(this)
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = applicationContext.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
@@ -1,9 +1,13 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.drop
@@ -20,41 +24,37 @@ import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This class is used to manage chapter downloads in the application. It must be instantiated once
* and retrieved through dependency injection. You can use this class to queue new chapters or query
* downloaded chapters.
*/
@Inject
@SingleIn(AppScope::class)
class DownloadManager(
private val context: Context,
private val provider: DownloadProvider = Injekt.get(),
private val cache: DownloadCache = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val provider: DownloadProvider,
private val cache: DownloadCache,
private val getCategories: GetCategories,
private val getManga: GetManga,
private val getChapter: GetChapter,
private val sourceManager: SourceManager,
private val downloadPreferences: DownloadPreferences,
private val downloader: Downloader,
private val pendingDeleter: DownloadPendingDeleter,
) {
/**
* Downloader whose only task is to download chapters.
*/
private val downloader = Downloader(context, provider, cache)
val isRunning: Boolean
get() = downloader.isRunning
/**
* Queue to delay the deletion of a list of chapters until triggered.
*/
private val pendingDeleter = DownloadPendingDeleter(context)
val queueState
get() = downloader.queueState
@@ -107,7 +107,7 @@ class DownloadManager(
fun startDownloadNow(chapterId: Long) {
val existingDownload = getQueuedDownloadOrNull(chapterId)
// If not in queue try to start a new download
val toAdd = existingDownload ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
val toAdd = existingDownload ?: runBlocking { downloadFromChapterId(chapterId) } ?: return
queueState.value.toMutableList().apply {
existingDownload?.let { remove(it) }
add(0, toAdd)
@@ -116,6 +116,14 @@ class DownloadManager(
startDownloads()
}
private suspend fun downloadFromChapterId(chapterId: Long): Download? {
val chapter = getChapter.await(chapterId) ?: return null
val manga = getManga.await(chapter.mangaId) ?: return null
val source = sourceManager.get(manga.source) as? HttpSource ?: return null
return Download(source, manga, chapter)
}
/**
* Reorders the download queue.
*
@@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.Context
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.download.model.Download
@@ -16,7 +17,6 @@ import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.util.regex.Pattern
/**
@@ -24,9 +24,11 @@ import java.util.regex.Pattern
*
* @param context context of application
*/
internal class DownloadNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
@Inject
class DownloadNotifier(
private val context: Context,
private val preferences: SecurityPreferences,
) {
private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
@@ -2,21 +2,24 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.content.edit
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Class used to keep a list of chapters for future deletion.
*
* @param context the application context.
*/
@Inject
@SingleIn(AppScope::class)
class DownloadPendingDeleter(
context: Context,
private val json: Json = Injekt.get(),
private val json: Json,
) {
/**
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.Hash.md5
import eu.kanade.tachiyomi.util.storage.DiskUtil
@@ -14,8 +17,6 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
/**
@@ -24,10 +25,12 @@ import java.io.IOException
*
* @param context the application context.
*/
@Inject
@SingleIn(AppScope::class)
class DownloadProvider(
private val context: Context,
private val storageManager: StorageManager = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val storageManager: StorageManager,
private val libraryPreferences: LibraryPreferences,
) {
private val downloadsDir: UniFile?
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.content.edit
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.runBlocking
@@ -11,18 +14,18 @@ import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This class is used to persist active downloads across application restarts.
*/
@Inject
@SingleIn(AppScope::class)
class DownloadStore(
context: Context,
private val sourceManager: SourceManager = Injekt.get(),
private val json: Json = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val getChapter: GetChapter = Injekt.get(),
private val sourceManager: SourceManager,
private val json: Json,
private val getManga: GetManga,
private val getChapter: GetChapter,
) {
/**
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.model.getComicInfo
import eu.kanade.tachiyomi.data.cache.ChapterCache
@@ -57,8 +60,6 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Locale
import kotlin.time.Duration.Companion.seconds
@@ -68,34 +69,27 @@ import kotlin.time.Duration.Companion.seconds
*
* Its queue contains the list of chapters to download.
*/
@Inject
@SingleIn(AppScope::class)
class Downloader(
private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache,
private val sourceManager: SourceManager = Injekt.get(),
private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val xml: XML = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val sourceManager: SourceManager,
private val chapterCache: ChapterCache,
private val downloadPreferences: DownloadPreferences,
private val xml: XML,
private val getCategories: GetCategories,
private val getTracks: GetTracks,
private val store: DownloadStore,
private val notifier: DownloadNotifier,
) {
/**
* Store for persisting downloads across restarts.
*/
private val store = DownloadStore(context)
/**
* Queue where active downloads are kept.
*/
private val _queueState = MutableStateFlow<List<Download>>(emptyList())
val queueState = _queueState.asStateFlow()
/**
* Notifier for the downloader state and progress.
*/
private val notifier by lazy { DownloadNotifier(context) }
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var downloaderJob: Job? = null
@@ -10,13 +10,8 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.milliseconds
data class Download(
@@ -71,19 +66,4 @@ data class Download(
DOWNLOADED(3),
ERROR(4),
}
companion object {
suspend fun fromChapterId(
chapterId: Long,
getChapter: GetChapter = Injekt.get(),
getManga: GetManga = Injekt.get(),
sourceManager: SourceManager = Injekt.get(),
): Download? {
val chapter = getChapter.await(chapterId) ?: return null
val manga = getManga.await(chapter.mangaId) ?: return null
val source = sourceManager.get(manga.source) as? HttpSource ?: return null
return Download(source, manga, chapter)
}
}
}
@@ -15,9 +15,11 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SManga
@@ -38,6 +40,9 @@ import kotlinx.coroutines.sync.withPermit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import mihon.app.di.AppGraph
import mihon.app.di.appGraph
import mihon.core.metro.metroGraph
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource
@@ -63,8 +68,6 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.SourceNotInstalledException
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
@@ -78,24 +81,34 @@ import kotlin.time.Clock
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get()
private val downloadManager: DownloadManager = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private val getManga: GetManga = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get()
private val graph: AppGraph = context.metroGraph()
private val notifier = LibraryUpdateNotifier(context)
@Inject private lateinit var sourceManager: SourceManager
@Inject private lateinit var libraryPreferences: LibraryPreferences
@Inject private lateinit var downloadManager: DownloadManager
@Inject private lateinit var getLibraryManga: GetLibraryManga
@Inject private lateinit var getManga: GetManga
@Inject private lateinit var fetchInterval: FetchInterval
@Inject private lateinit var filterChaptersForDownload: FilterChaptersForDownload
@Inject private lateinit var updateMangaFromRemote: UpdateMangaFromRemote
@Inject private lateinit var notifier: LibraryUpdateNotifier
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
override suspend fun doWork(): Result {
graph.inject(this)
if (tags.contains(WORK_NAME_AUTO)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val preferences = Injekt.get<LibraryPreferences>()
val restrictions = preferences.autoUpdateDeviceRestrictions.get()
val restrictions = libraryPreferences.autoUpdateDeviceRestrictions.get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.retry()
}
@@ -133,7 +146,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notifier = LibraryUpdateNotifier(context)
return ForegroundInfo(
Notifications.ID_LIBRARY_PROGRESS,
notifier.progressNotificationBuilder.build(),
@@ -420,7 +432,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
context: Context,
prefInterval: Int? = null,
) {
val preferences = Injekt.get<LibraryPreferences>()
val preferences = context.appGraph.libraryPreferences
val interval = prefInterval ?: preferences.autoUpdateInterval.get()
if (interval > 0) {
val restrictions = preferences.autoUpdateDeviceRestrictions.get()
@@ -469,11 +481,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
fun startNow(
context: Context,
workManager: WorkManager,
category: Category? = null,
): Boolean {
val wm = context.workManager
if (wm.isRunning(TAG)) {
if (workManager.isRunning(TAG)) {
// Already running either as a scheduled or manual job
return false
}
@@ -486,20 +497,20 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.addTag(WORK_NAME_MANUAL)
.setInputData(inputData)
.build()
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
workManager.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
return true
}
fun stop(context: Context) {
val wm = context.workManager
val workManager = context.workManager
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG))
.addStates(listOf(WorkInfo.State.RUNNING))
.build()
wm.getWorkInfos(workQuery).get()
workManager.getWorkInfos(workQuery).get()
// Should only return one work but just in case
.forEach {
wm.cancelWorkById(it.id)
workManager.cancelWorkById(it.id)
// Re-enqueue cancelled scheduled work
if (it.tags.contains(WORK_NAME_AUTO)) {
@@ -14,6 +14,7 @@ import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.transform.CircleCropTransformation
import dev.zacsweers.metro.Inject
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
@@ -37,15 +38,14 @@ import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.math.RoundingMode
import java.text.NumberFormat
@Inject
class LibraryUpdateNotifier(
private val context: Context,
private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val securityPreferences: SecurityPreferences,
private val sourceManager: SourceManager,
) {
private val percentFormatter = NumberFormat.getPercentInstance().apply {
@@ -7,13 +7,12 @@ import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkQuery
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -22,6 +21,8 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import mihon.app.di.AppGraph
import mihon.core.metro.metroGraph
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
@@ -29,8 +30,6 @@ import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
@@ -40,15 +39,21 @@ import kotlin.concurrent.atomics.fetchAndIncrement
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get()
private val graph: AppGraph = context.metroGraph()
private val notifier = LibraryUpdateNotifier(context)
@Inject private lateinit var sourceManager: SourceManager
@Inject private lateinit var getLibraryManga: GetLibraryManga
@Inject private lateinit var updateMangaFromRemote: UpdateMangaFromRemote
@Inject private lateinit var notifier: LibraryUpdateNotifier
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
override suspend fun doWork(): Result {
graph.inject(this)
setForegroundSafely()
addMangaToQueue()
@@ -72,7 +77,6 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notifier = LibraryUpdateNotifier(context)
return ForegroundInfo(
Notifications.ID_LIBRARY_PROGRESS,
notifier.progressNotificationBuilder.build(),
@@ -168,9 +172,8 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
fun startNow(context: Context): Boolean {
val wm = context.workManager
if (wm.isRunning(TAG)) {
fun startNow(workManager: WorkManager): Boolean {
if (workManager.isRunning(TAG)) {
// Already running either as a scheduled or manual job
return false
}
@@ -178,21 +181,9 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
.addTag(TAG)
.addTag(WORK_NAME_MANUAL)
.build()
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
workManager.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
return true
}
fun stop(context: Context) {
val wm = context.workManager
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG))
.addStates(listOf(WorkInfo.State.RUNNING))
.build()
wm.getWorkInfos(workQuery).get()
// Should only return one work but just in case
.forEach {
wm.cancelWorkById(it.id)
}
}
}
}
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
@@ -16,7 +17,9 @@ import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.runBlocking
import mihon.app.di.appGraph
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapter
@@ -28,9 +31,6 @@ import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
@@ -40,12 +40,21 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
*/
class NotificationReceiver : BroadcastReceiver() {
private val getManga: GetManga by injectLazy()
private val getChapter: GetChapter by injectLazy()
private val updateChapter: UpdateChapter by injectLazy()
private val downloadManager: DownloadManager by injectLazy()
@Inject private lateinit var getManga: GetManga
@Inject private lateinit var getChapter: GetChapter
@Inject private lateinit var updateChapter: UpdateChapter
@Inject private lateinit var downloadManager: DownloadManager
@Inject private lateinit var downloadPreferences: DownloadPreferences
@Inject private lateinit var sourceManager: SourceManager
override fun onReceive(context: Context, intent: Intent) {
context.appGraph.inject(this)
when (intent.action) {
// Dismiss notification
ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
@@ -161,7 +170,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param context context of application
*/
private fun cancelRestore(context: Context) {
BackupRestoreJob.stop(context)
BackupRestoreJob.stop(context.workManager)
}
/**
@@ -180,9 +189,6 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga
*/
private fun markAsRead(chapterUrls: Array<String>, mangaId: Long) {
val downloadPreferences: DownloadPreferences = Injekt.get()
val sourceManager: SourceManager = Injekt.get()
launchIO {
val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
.map {
@@ -11,6 +11,9 @@ import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.content.contentValuesOf
import androidx.core.net.toUri
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.storage.getUriCompat
@@ -26,6 +29,8 @@ import java.io.File
import java.io.InputStream
import kotlin.time.Clock
@Inject
@SingleIn(AppScope::class)
class ImageSaver(
val context: Context,
) {
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track
import android.app.Application
import android.content.Context
import androidx.annotation.CallSuper
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.model.toDomainTrack
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import logcat.LogPriority
import mihon.app.di.appGraph
import okhttp3.OkHttpClient
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
@@ -18,7 +19,6 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack
abstract class BaseTracker(
@@ -26,10 +26,14 @@ abstract class BaseTracker(
override val name: String,
) : Tracker {
val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy()
private val addTracks: AddTracks by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
protected val appGraph get() = Injekt.get<Context>().appGraph
val trackPreferences: TrackPreferences by lazy { appGraph.trackPreferences }
val networkService: NetworkHelper by lazy { appGraph.networkHelper }
private val context: Context by lazy { appGraph.context }
private val addTracks: AddTracks by lazy { appGraph.addTracks }
private val insertTrack: InsertTrack by lazy { appGraph.insertTrack }
override val client: OkHttpClient
get() = networkService.client
@@ -83,7 +87,9 @@ abstract class BaseTracker(
try {
addTracks.bind(this, item, mangaId)
} catch (e: Throwable) {
withUIContext { Injekt.get<Application>().toast(e.message) }
withUIContext {
context.toast(e.message)
}
}
}
@@ -139,7 +145,9 @@ abstract class BaseTracker(
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" }
withUIContext { Injekt.get<Application>().toast(e.message) }
withUIContext {
context.toast(e.message)
}
}
}
}
@@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.hikka.Hikka
@@ -13,6 +16,8 @@ import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
import kotlinx.coroutines.flow.combine
@Inject
@SingleIn(AppScope::class)
class TrackerManager {
companion object {
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack
class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
@@ -32,7 +31,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
private const val SEARCH_ID_PREFIX = "id:"
}
private val json: Json by injectLazy()
private val json: Json by lazy { appGraph.json }
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
@@ -42,7 +41,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override val supportsPrivateTracking: Boolean = true
private val scorePreference = trackPreferences.anilistScoreType
private val scorePreference by lazy { trackPreferences.anilistScoreType }
init {
// If the preference is an int from APIv1, logout user to force using APIv2
@@ -135,7 +134,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
else -> "😊"
}
else -> track.toApiScore()
else -> track.toApiScore(appGraph.trackPreferences)
}
}
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack
fun Track.toApiStatus() = when (status) {
@@ -15,9 +14,7 @@ fun Track.toApiStatus() = when (status) {
else -> throw NotImplementedError("Unknown status: $status")
}
private val preferences: TrackPreferences by injectLazy()
fun DomainTrack.toApiScore(): String = when (preferences.anilistScoreType.get()) {
fun DomainTrack.toApiScore(preferences: TrackPreferences): String = when (preferences.anilistScoreType.get()) {
// 10 point
"POINT_10" -> (score.toInt() / 10).toString()
// 100 point
@@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.sourcePreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest
import tachiyomi.domain.track.model.Track as DomainTrack
@@ -29,7 +28,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
private val interceptor by lazy { KavitaInterceptor(this) }
val api by lazy { KavitaApi(client, interceptor) }
private val sourceManager: SourceManager by injectLazy()
private val sourceManager: SourceManager by lazy { appGraph.sourceManager }
override fun getLogo(): Int = R.drawable.brand_kavita
@@ -21,10 +21,11 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
const val COMPLETED = 3L
}
override val client: OkHttpClient =
override val client: OkHttpClient by lazy {
networkService.client.newBuilder()
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build()
}
val api by lazy { KomgaApi(id, client) }
@@ -13,7 +13,7 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
val api by lazy { SuwayomiApi(id) }
val api by lazy { SuwayomiApi(id, appGraph.sourceManager) }
override fun getLogo() = R.drawable.brand_suwayomi
@@ -23,11 +23,12 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest
class SuwayomiApi(private val trackId: Long) {
class SuwayomiApi(
private val trackId: Long,
private val sourceManager: SourceManager,
) {
private val json: Json by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
private val configurableSource: ConfigurableSource by lazy { (sourceManager.get(sourceId) as ConfigurableSource) }
private val client: OkHttpClient by lazy { source.client }
@@ -1,15 +1,16 @@
package eu.kanade.tachiyomi.data.updater
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.isFossBuildType
import eu.kanade.tachiyomi.util.system.isNightlyBuildType
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.release.interactor.GetApplicationRelease
import uy.kohesive.injekt.injectLazy
class AppUpdateChecker {
private val getApplicationRelease: GetApplicationRelease by injectLazy()
@Inject
class AppUpdateChecker(
private val getApplicationRelease: GetApplicationRelease,
) {
suspend fun checkForUpdate(forceCheck: Boolean = false): GetApplicationRelease.Result {
// Disable app update checks for older Android versions that we're going to drop support for
@@ -1,146 +0,0 @@
package eu.kanade.tachiyomi.di
import android.app.Application
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import app.cash.sqldelight.db.SqlDriver
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteDatabaseType
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteDriver
import com.eygraber.sqldelight.androidx.driver.FileProvider
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.AndroidSourceManager
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.storage.AndroidStorageFolderProvider
import tachiyomi.data.Chapters
import tachiyomi.data.Database
import tachiyomi.data.DateColumnAdapter
import tachiyomi.data.History
import tachiyomi.data.Mangas
import tachiyomi.data.MemoColumnAdapter
import tachiyomi.data.StringListColumnAdapter
import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.source.local.image.LocalCoverManager
import tachiyomi.source.local.io.LocalSourceFileSystem
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
import java.lang.ref.WeakReference
private val lock = Any()
class AppModule(val app: Application) : InjektModule {
private var sqlDriverRef: WeakReference<SqlDriver>? = null
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingleton<Context>(app)
addSingletonFactory<SqlDriver> {
synchronized(lock) {
sqlDriverRef?.get()?.let { return@synchronized it }
AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.FileProvider(app, "tachiyomi.db"),
schema = Database.Schema,
configuration = AndroidxSqliteConfiguration(
isForeignKeyConstraintsEnabled = true,
),
)
.also { sqlDriverRef = WeakReference(it) }
}
}
addSingletonFactory {
Database(
driver = get(),
historyAdapter = History.Adapter(
last_readAdapter = DateColumnAdapter,
),
mangasAdapter = Mangas.Adapter(
genreAdapter = StringListColumnAdapter,
update_strategyAdapter = UpdateStrategyColumnAdapter,
memoAdapter = MemoColumnAdapter,
),
chaptersAdapter = Chapters.Adapter(
memoAdapter = MemoColumnAdapter,
),
)
}
addSingletonFactory {
Json {
ignoreUnknownKeys = true
explicitNulls = false
}
}
addSingletonFactory<XML> {
XML.v1 {
policy {
ignoreUnknownChildren()
autoPolymorphic = true
}
xmlDeclMode = XmlDeclMode.Charset
xmlVersion = XmlVersion.XML10
setIndent(2)
}
}
addSingletonFactory<ProtoBuf> {
ProtoBuf
}
addSingletonFactory { ChapterCache(app, get()) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { JavaScriptEngine(app) }
addSingletonFactory<SourceManager> { AndroidSourceManager(app, get(), get()) }
addSingletonFactory { ExtensionManager(app) }
addSingletonFactory { DownloadProvider(app) }
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { DownloadCache(app) }
addSingletonFactory { TrackerManager() }
addSingletonFactory { DelayedTrackingStore(app) }
addSingletonFactory { ImageSaver(app) }
addSingletonFactory { AndroidStorageFolderProvider(app) }
addSingletonFactory { LocalSourceFileSystem(get()) }
addSingletonFactory { LocalCoverManager(app, get()) }
addSingletonFactory { StorageManager(app, get()) }
// Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute {
get<NetworkHelper>()
get<SourceManager>()
get<Database>()
get<DownloadManager>()
}
}
}
@@ -1,82 +0,0 @@
package eu.kanade.tachiyomi.di
import android.app.Application
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.isDebugBuildType
import tachiyomi.core.common.preference.AndroidPreferenceStore
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.storage.AndroidStorageFolderProvider
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.domain.upcoming.service.UpcomingPreferences
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class PreferenceModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<PreferenceStore> {
AndroidPreferenceStore(app)
}
addSingletonFactory {
NetworkPreferences(
preferenceStore = get(),
verboseLoggingDefault = isDebugBuildType,
)
}
addSingletonFactory {
SourcePreferences(get())
}
addSingletonFactory {
SecurityPreferences(get())
}
addSingletonFactory {
PrivacyPreferences(get())
}
addSingletonFactory {
LibraryPreferences(get())
}
addSingletonFactory {
UpcomingPreferences(get())
}
addSingletonFactory {
UpdatesPreferences(get())
}
addSingletonFactory {
ReaderPreferences(get())
}
addSingletonFactory {
TrackPreferences(get())
}
addSingletonFactory {
DownloadPreferences(get())
}
addSingletonFactory {
BackupPreferences(get())
}
addSingletonFactory {
StoragePreferences(
folderProvider = get<AndroidStorageFolderProvider>(),
preferenceStore = get(),
)
}
addSingletonFactory {
UiPreferences(get())
}
addSingletonFactory {
BasePreferences(app, get())
}
}
}
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.api.ExtensionApi
@@ -30,8 +33,6 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
/**
@@ -41,10 +42,15 @@ import java.util.Locale
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
* loaded.
*/
@Inject
@SingleIn(AppScope::class)
class ExtensionManager(
private val context: Context,
private val preferences: SourcePreferences = Injekt.get(),
private val trustExtension: TrustExtension = Injekt.get(),
private val preferences: SourcePreferences,
private val trustExtension: TrustExtension,
private val api: ExtensionApi,
private val installer: ExtensionInstaller,
private val extensionUpdateNotifier: ExtensionUpdateNotifier,
) {
val scope = CoroutineScope(SupervisorJob())
@@ -52,16 +58,6 @@ class ExtensionManager(
private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
/**
* API where all the available extensions can be found.
*/
private val api = ExtensionApi()
/**
* The installer which installs, updates and uninstalls the extensions.
*/
private val installer by lazy { ExtensionInstaller(context) }
private val iconMap = mutableMapOf<String, Drawable>()
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
@@ -373,7 +369,7 @@ class ExtensionManager(
val pendingUpdateCount = installedExtensionMapFlow.value.values.count { it.hasUpdate }
preferences.extensionUpdatesCount.set(pendingUpdateCount)
if (pendingUpdateCount == 0) {
ExtensionUpdateNotifier(context).dismiss()
extensionUpdateNotifier.dismiss()
}
}
@@ -1,19 +1,23 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import mihon.domain.extension.interactor.UpdateExtensionStores
import mihon.domain.extension.repository.ExtensionStoreRepository
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
internal class ExtensionApi {
private val repository: ExtensionStoreRepository by injectLazy()
private val updateExtensionStores: UpdateExtensionStores by injectLazy()
@Inject
@SingleIn(AppScope::class)
class ExtensionApi(
private val repository: ExtensionStoreRepository,
private val updateExtensionStores: UpdateExtensionStores,
private val extensionUpdateNotifier: ExtensionUpdateNotifier,
) {
suspend fun findExtensions(): List<Extension.Available> {
return withIOContext { repository.fetchExtensions() }
@@ -41,7 +45,7 @@ internal class ExtensionApi {
}
if (extensionsWithUpdate.isNotEmpty()) {
ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
extensionUpdateNotifier.promptUpdates(extensionsWithUpdate.map { it.name })
}
}
}
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.extension.api
import android.content.Context
import androidx.core.app.NotificationCompat
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@@ -10,12 +13,12 @@ import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Inject
@SingleIn(AppScope::class)
class ExtensionUpdateNotifier(
private val context: Context,
private val securityPreferences: SecurityPreferences = Injekt.get(),
private val securityPreferences: SecurityPreferences,
) {
fun promptUpdates(names: List<String>) {
context.notify(
@@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.extension.util
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import dev.zacsweers.metro.Inject
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import mihon.app.di.appGraph
import kotlin.time.Duration.Companion.seconds
/**
@@ -17,6 +17,8 @@ import kotlin.time.Duration.Companion.seconds
*/
class ExtensionInstallActivity : Activity() {
@Inject private lateinit var extensionManager: ExtensionManager
// MIUI package installer bug workaround
private var ignoreUntil = 0L
private var ignoreResult = false
@@ -24,6 +26,7 @@ class ExtensionInstallActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appGraph.inject(this)
@Suppress("DEPRECATION")
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
@@ -71,7 +74,6 @@ class ExtensionInstallActivity : Activity() {
private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
val extensionManager = Injekt.get<ExtensionManager>()
val newStep = when (resultCode) {
RESULT_OK -> InstallStep.Installed
RESULT_CANCELED -> InstallStep.Idle
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import dev.zacsweers.metro.Inject
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.model.Extension
@@ -23,8 +24,6 @@ import logcat.LogPriority
import okhttp3.OkHttpClient
import okhttp3.Request
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
/**
@@ -32,16 +31,19 @@ import java.io.File
*
* @param context The application context.
*/
internal class ExtensionInstaller(
@Inject
class ExtensionInstaller(
private val context: Context,
basePreferences: BasePreferences,
networkHelper: NetworkHelper,
) {
private val scope = CoroutineScope(Dispatchers.IO)
private val activeJobs = mutableMapOf<String, Job>()
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller
private val extensionInstaller = basePreferences.extensionInstaller
private val httpClient: OkHttpClient = Injekt.get<NetworkHelper>().client
private val httpClient: OkHttpClient = networkHelper.client
/**
* Adds the given extension to the downloads queue and returns an observable containing its
@@ -7,7 +7,6 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.Source
@@ -20,8 +19,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import mihon.app.di.appGraph
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.File
/**
@@ -40,12 +39,6 @@ import java.io.File
*/
internal object ExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
private val trustExtension: TrustExtension by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource.get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
@@ -228,6 +221,9 @@ internal object ExtensionLoader {
* @param extensionInfo The extension to load.
*/
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val trustExtension: TrustExtension = context.appGraph.trustExtension
val loadNsfwSource: Boolean = context.appGraph.sourcePreferences.showNsfwSource.get()
val pkgManager = context.packageManager
val pkgInfo = extensionInfo.packageInfo
val appInfo = pkgInfo.applicationInfo!!
@@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.online.HttpSource
@@ -20,22 +23,21 @@ import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.repository.StubSourceRepository
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.source.local.LocalSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class AndroidSourceManager(
private val context: Context,
private val extensionManager: ExtensionManager,
private val sourceRepository: StubSourceRepository,
private val localSource: LocalSource,
private val downloadManager: Lazy<DownloadManager>,
) : SourceManager {
private val _isInitialized = MutableStateFlow(false)
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val downloadManager: DownloadManager by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.IO)
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>())
@@ -51,13 +53,7 @@ class AndroidSourceManager(
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>(
mapOf(
LocalSource.ID to LocalSource(
context,
Injekt.get(),
Injekt.get(),
),
),
mapOf(LocalSource.ID to localSource),
)
extensions.forEach { extension ->
extension.sources.forEach {
@@ -106,7 +102,7 @@ class AndroidSourceManager(
if (dbSource == source) return@launch
sourceRepository.upsertStubSource(source.id, source.lang, source.name)
if (dbSource != null) {
downloadManager.renameSource(dbSource, source)
downloadManager.value.renameSource(dbSource, source)
}
}
}
@@ -1,13 +1,14 @@
package eu.kanade.tachiyomi.source
import eu.kanade.domain.source.service.SourcePreferences
import android.content.Context
import mihon.app.di.appGraph
import tachiyomi.domain.source.model.StubSource
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
fun Source.getNameForMangaInfo(): String {
val preferences = Injekt.get<SourcePreferences>()
val preferences = Injekt.get<Context>().appGraph.sourcePreferences
val enabledLanguages = preferences.enabledLanguages.get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl
import eu.kanade.tachiyomi.util.system.prepareTabletUiContext
import mihon.app.di.appGraph
open class BaseActivity :
AppCompatActivity(),
@@ -15,7 +16,8 @@ open class BaseActivity :
ThemingDelegate by ThemingDelegateImpl() {
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(newBase.prepareTabletUiContext())
val uiPreferences = newBase.appGraph.uiPreferences
super.attachBaseContext(newBase.prepareTabletUiContext(uiPreferences))
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -1,12 +1,14 @@
package eu.kanade.tachiyomi.ui.base.delegate
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import dev.zacsweers.metro.Inject
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.security.UnlockActivity
@@ -16,9 +18,9 @@ import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import mihon.app.di.appGraph
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
interface SecureActivityDelegate {
fun registerSecureActivity(activity: AppCompatActivity)
@@ -31,8 +33,8 @@ interface SecureActivityDelegate {
*/
var requireUnlock = true
fun onApplicationStopped() {
val preferences = Injekt.get<SecurityPreferences>()
fun onApplicationStopped(context: Context) {
val preferences = context.appGraph.securityPreferences
if (!preferences.useAuthenticator.get()) return
if (!AuthenticatorUtil.isAuthenticating) {
@@ -48,8 +50,8 @@ interface SecureActivityDelegate {
/**
* Checks if unlock is needed when app comes foreground.
*/
fun onApplicationStart() {
val preferences = Injekt.get<SecurityPreferences>()
fun onApplicationStart(context: Context) {
val preferences = context.appGraph.securityPreferences
if (!preferences.useAuthenticator.get()) return
val lastClosedPref = preferences.lastAppClosed
@@ -76,11 +78,13 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
private lateinit var activity: AppCompatActivity
private val preferences: BasePreferences by injectLazy()
private val securityPreferences: SecurityPreferences by injectLazy()
@Inject private lateinit var preferences: BasePreferences
@Inject private lateinit var securityPreferences: SecurityPreferences
override fun registerSecureActivity(activity: AppCompatActivity) {
this.activity = activity
Injekt.get<Context>().appGraph.inject(this)
activity.lifecycle.addObserver(this)
}
@@ -1,11 +1,9 @@
package eu.kanade.tachiyomi.ui.base.delegate
import android.app.Activity
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.tachiyomi.R
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import mihon.app.di.appGraph
interface ThemingDelegate {
fun applyAppTheme(activity: Activity)
@@ -22,7 +20,7 @@ interface ThemingDelegate {
class ThemingDelegateImpl : ThemingDelegate {
override fun applyAppTheme(activity: Activity) {
val uiPreferences = Injekt.get<UiPreferences>()
val uiPreferences = activity.appGraph.uiPreferences
ThemingDelegate.getThemeResIds(uiPreferences.appTheme.get(), uiPreferences.themeDarkAmoled.get())
.forEach(activity::setTheme)
}
@@ -9,10 +9,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
@@ -58,7 +58,7 @@ data object BrowseTab : Tab {
val context = LocalContext.current
// Hoisted for extensions tab's search bar
val extensionsViewModel = viewModel<ExtensionsViewModel>()
val extensionsViewModel = metroViewModel<ExtensionsViewModel>()
val extensionsSearchQuery by extensionsViewModel.searchQuery.collectAsStateWithLifecycle()
val tabs = listOf(
@@ -8,6 +8,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.browse.ExtensionFilterScreen
import eu.kanade.presentation.util.Screen
import kotlinx.coroutines.flow.collectLatest
@@ -21,7 +22,7 @@ class ExtensionFilterScreen : Screen() {
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<ExtensionFilterViewModel>()
val viewModel = metroViewModel<ExtensionFilterViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
if (state is ExtensionFilterState.Loading) {
@@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.service.SourcePreferences
@@ -17,14 +22,15 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class ExtensionFilterViewModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
private val preferences: SourcePreferences,
private val getExtensionLanguages: GetExtensionLanguages,
private val toggleLanguage: ToggleLanguage,
) : ViewModel() {
private val _events: Channel<ExtensionFilterEvent> = Channel()
@@ -1,10 +1,15 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.icerock.moko.resources.StringResource
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.source.service.SourcePreferences
@@ -33,21 +38,21 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class ExtensionsViewModel(
private val preferences: SourcePreferences = Injekt.get(),
basePreferences: BasePreferences = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(),
private val preferences: SourcePreferences,
basePreferences: BasePreferences,
private val extensionManager: ExtensionManager,
private val getExtensions: GetExtensionsByType,
private val context: Context,
) : ViewModel() {
private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
private val context = Injekt.get<Application>()
// Public so BrowseTab's search bar can observe it without subscribing to the whole state.
val searchQuery: StateFlow<String?>
field = MutableStateFlow(null)
@@ -117,6 +122,7 @@ class ExtensionsViewModel(
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State())
init {
viewModelScope.launchIO { findAvailableExtensions() }
}
@@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.presentation.browse.ExtensionDetailsScreen
import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR
@@ -20,12 +20,10 @@ data class ExtensionDetailsScreen(
@Composable
override fun Content() {
val viewModel = viewModel<ExtensionDetailsViewModel>(
factory = ExtensionDetailsViewModel.Factory,
extras = CreationExtras {
set(ExtensionDetailsViewModel.PKG_NAME_KEY, pkgName)
},
)
val viewModel =
assistedMetroViewModel<ExtensionDetailsViewModel, ExtensionDetailsViewModel.Factory> {
create(pkgName = pkgName)
}
val state by viewModel.state.collectAsStateWithLifecycle()
val navigator = LocalNavigator.currentOrThrow
@@ -1,13 +1,16 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleIncognito
@@ -32,32 +35,25 @@ import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@AssistedInject
class ExtensionDetailsViewModel(
pkgName: String,
@Assisted pkgName: String,
private val context: Context,
private val network: NetworkHelper = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensionSources: GetExtensionSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleIncognito: ToggleIncognito = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
private val network: NetworkHelper,
private val extensionManager: ExtensionManager,
private val getExtensionSources: GetExtensionSources,
private val toggleSource: ToggleSource,
private val toggleIncognito: ToggleIncognito,
private val preferences: SourcePreferences,
) : ViewModel() {
companion object {
val PKG_NAME_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
ExtensionDetailsViewModel(
pkgName = get(PKG_NAME_KEY)!!,
context = Injekt.get<Application>(),
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(pkgName: String): ExtensionDetailsViewModel
}
val state: StateFlow<State> = extensionManager.installedExtensionsFlow
@@ -38,11 +38,9 @@ import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.sourcePreferences
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
import tachiyomi.domain.source.service.SourceManager
import mihon.app.di.appGraph
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SourcePreferencesScreen(val sourceId: Long) : Screen() {
@@ -59,7 +57,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
Scaffold(
topBar = {
AppBar(
title = Injekt.get<SourceManager>().getOrStub(sourceId).toString(),
title = context.appGraph.sourceManager.getOrStub(sourceId).toString(),
navigateUp = navigator::pop,
scrollBehavior = it,
)
@@ -132,7 +130,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
private fun populateScreen(): PreferenceScreen {
val sourceId = requireArguments().getLong(SOURCE_ID)
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
val appGraph = requireContext().appGraph
val source = appGraph.sourceManager.getOrStub(sourceId)
val basePreferences = appGraph.basePreferences
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
if (source is ConfigurableSource) {
@@ -152,7 +152,7 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
val setListener = pref.getOnBindEditTextListener()
pref.setOnBindEditTextListener {
setListener?.onBindEditText(it)
it.setIncognito(lifecycleScope)
it.setIncognito(basePreferences, lifecycleScope)
}
}
}
@@ -19,10 +19,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.manga.components.BaseMangaListItem
import eu.kanade.presentation.util.Screen
@@ -48,12 +48,8 @@ data class MigrateMangaScreen(
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<MigrateMangaViewModel>(
factory = MigrateMangaViewModel.Factory,
extras = CreationExtras {
set(MigrateMangaViewModel.SOURCE_ID_KEY, sourceId)
},
)
val viewModel =
assistedMetroViewModel<MigrateMangaViewModel, MigrateMangaViewModel.Factory> { create(sourceId = sourceId) }
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -3,9 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
@@ -27,26 +31,20 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@AssistedInject
class MigrateMangaViewModel(
private val sourceId: Long,
private val sourceManager: SourceManager = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(),
@Assisted private val sourceId: Long,
private val sourceManager: SourceManager,
private val getFavorites: GetFavorites,
) : ViewModel() {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateMangaViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(sourceId: Long): MigrateMangaViewModel
}
private val _events: Channel<MigrationMangaEvent> = Channel()
@@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
@@ -20,12 +19,8 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<MigrateSearchViewModel>(
factory = MigrateSearchViewModel.Factory,
extras = CreationExtras {
set(MigrateSearchViewModel.MANGA_ID_KEY, mangaId)
},
)
val viewModel =
assistedMetroViewModel<MigrateSearchViewModel, MigrateSearchViewModel.Factory> { create(mangaId = mangaId) }
val state by viewModel.state.collectAsState()
MigrateSearchScreen(
@@ -1,36 +1,45 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@AssistedInject
class MigrateSearchViewModel(
val mangaId: Long,
getManga: GetManga = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : SearchViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateSearchViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
@Assisted val mangaId: Long,
sourcePreferences: SourcePreferences,
extensionManager: ExtensionManager,
networkToLocalManga: NetworkToLocalManga,
getManga: GetManga,
preferences: SourcePreferences,
private val sourceManager: SourceManager,
) : SearchViewModel(
sourcePreferences = sourcePreferences,
sourceManager = sourceManager,
extensionManager = extensionManager,
networkToLocalManga = networkToLocalManga,
getManga = getManga,
preferences = preferences,
) {
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(mangaId: Long): MigrateSearchViewModel
}
private val migrationSources by lazy { sourcePreferences.migrationSources.get() }
@@ -17,10 +17,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.core.util.ifSourcesLoaded
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.components.SearchToolbar
@@ -60,13 +59,10 @@ data class MigrateSourceSearchScreen(
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val viewModel = viewModel<BrowseSourceViewModel>(
factory = BrowseSourceViewModel.Factory,
extras = CreationExtras {
set(BrowseSourceViewModel.SOURCE_ID_KEY, sourceId)
set(BrowseSourceViewModel.LISTING_QUERY_KEY, query)
},
)
val viewModel =
assistedMetroViewModel<BrowseSourceViewModel, BrowseSourceViewModel.Factory> {
create(sourceId = sourceId, listingQuery = query)
}
val state by viewModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
@@ -10,6 +10,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
@@ -21,7 +22,7 @@ import tachiyomi.presentation.core.i18n.stringResource
fun Screen.migrateSourceTab(): TabContent {
val uriHandler = LocalUriHandler.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<MigrateSourceViewModel>()
val viewModel = metroViewModel<MigrateSourceViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
return TabContent(
@@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences
@@ -20,13 +25,14 @@ import logcat.LogPriority
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class MigrateSourceViewModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val preferences: SourcePreferences,
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount,
) : ViewModel() {
private val _channel = Channel<Event>(Int.MAX_VALUE)
@@ -8,6 +8,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.browse.SourcesFilterScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast
@@ -19,7 +20,7 @@ class SourcesFilterScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<SourcesFilterViewModel>()
val viewModel = metroViewModel<SourcesFilterViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
if (state is SourcesFilterViewModel.State.Loading) {
@@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
@@ -14,16 +19,17 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.SortedMap
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class SourcesFilterViewModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
private val preferences: SourcePreferences,
private val getLanguagesWithSources: GetLanguagesWithSources,
private val toggleSource: ToggleSource,
private val toggleLanguage: ToggleLanguage,
) : ViewModel() {
val state: StateFlow<State> = combine(
@@ -11,6 +11,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.browse.SourceOptionsDialog
import eu.kanade.presentation.browse.SourcesScreen
import eu.kanade.presentation.components.AppBar
@@ -25,7 +26,7 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun Screen.sourcesTab(): TabContent {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<SourcesViewModel>()
val viewModel = metroViewModel<SourcesViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
return TabContent(
@@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -24,15 +29,16 @@ import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Pin
import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.TreeMap
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class SourcesViewModel(
private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
private val getEnabledSources: GetEnabledSources,
private val toggleSource: ToggleSource,
private val toggleSourcePin: ToggleSourcePin,
) : ViewModel() {
private val _events = Channel<Event>(Int.MAX_VALUE)
@@ -32,10 +32,9 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.core.util.ifSourcesLoaded
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.MissingSourceScreen
@@ -82,13 +81,10 @@ data class BrowseSourceScreen(
return
}
val viewModel = viewModel<BrowseSourceViewModel>(
factory = BrowseSourceViewModel.Factory,
extras = CreationExtras {
set(BrowseSourceViewModel.SOURCE_ID_KEY, sourceId)
set(BrowseSourceViewModel.LISTING_QUERY_KEY, listingQuery)
},
)
val viewModel =
assistedMetroViewModel<BrowseSourceViewModel, BrowseSourceViewModel.Factory> {
create(sourceId = sourceId, listingQuery = listingQuery)
}
val state by viewModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow
@@ -8,14 +8,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.core.preference.asState
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetIncognitoState
@@ -49,44 +53,36 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Clock
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
@AssistedInject
class BrowseSourceViewModel(
private val sourceId: Long,
listingQuery: String?,
sourceManager: SourceManager = Injekt.get(),
sourcePreferences: SourcePreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val getRemoteManga: GetRemoteManga = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
getIncognitoState: GetIncognitoState = Injekt.get(),
@Assisted private val sourceId: Long,
@Assisted listingQuery: String?,
sourceManager: SourceManager,
sourcePreferences: SourcePreferences,
private val libraryPreferences: LibraryPreferences,
private val coverCache: CoverCache,
private val getRemoteManga: GetRemoteManga,
private val getDuplicateLibraryManga: GetDuplicateLibraryManga,
private val getCategories: GetCategories,
private val setMangaCategories: SetMangaCategories,
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags,
private val getManga: GetManga,
private val updateManga: UpdateManga,
private val addTracks: AddTracks,
getIncognitoState: GetIncognitoState,
) : ViewModel() {
val state: StateFlow<BrowseSourceViewModel.State>
field = MutableStateFlow<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery)))
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val LISTING_QUERY_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
BrowseSourceViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
listingQuery = get(LISTING_QUERY_KEY),
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(sourceId: Long, listingQuery: String?): BrowseSourceViewModel
}
var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope)
@@ -7,10 +7,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.core.util.ifSourcesLoaded
import eu.kanade.presentation.browse.GlobalSearchScreen
import eu.kanade.presentation.util.Screen
@@ -32,13 +31,10 @@ class GlobalSearchScreen(
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<GlobalSearchViewModel>(
factory = GlobalSearchViewModel.Factory,
extras = CreationExtras {
set(GlobalSearchViewModel.INITIAL_QUERY_KEY, searchQuery)
set(GlobalSearchViewModel.INITIAL_EXTENSION_FILTER_KEY, extensionFilter)
},
)
val viewModel =
assistedMetroViewModel<GlobalSearchViewModel, GlobalSearchViewModel.Factory> {
create(initialQuery = searchQuery, initialExtensionFilter = extensionFilter)
}
val state by viewModel.state.collectAsState()
var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1)
@@ -1,27 +1,44 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.source.service.SourceManager
@AssistedInject
class GlobalSearchViewModel(
initialQuery: String,
initialExtensionFilter: String?,
) : SearchViewModel(State(searchQuery = initialQuery)) {
@Assisted initialQuery: String,
@Assisted initialExtensionFilter: String?,
sourcePreferences: SourcePreferences,
sourceManager: SourceManager,
extensionManager: ExtensionManager,
networkToLocalManga: NetworkToLocalManga,
getManga: GetManga,
preferences: SourcePreferences,
) : SearchViewModel(
initialState = State(searchQuery = initialQuery),
sourcePreferences = sourcePreferences,
sourceManager = sourceManager,
extensionManager = extensionManager,
networkToLocalManga = networkToLocalManga,
getManga = getManga,
preferences = preferences,
) {
companion object {
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val INITIAL_EXTENSION_FILTER_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
GlobalSearchViewModel(
initialQuery = get(INITIAL_QUERY_KEY)!!,
initialExtensionFilter = get(INITIAL_EXTENSION_FILTER_KEY),
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(initialQuery: String, initialExtensionFilter: String?): GlobalSearchViewModel
}
init {
@@ -27,18 +27,16 @@ import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.Executors
abstract class SearchViewModel(
initialState: State = State(),
sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
sourcePreferences: SourcePreferences,
private val sourceManager: SourceManager,
private val extensionManager: ExtensionManager,
private val networkToLocalManga: NetworkToLocalManga,
private val getManga: GetManga,
private val preferences: SourcePreferences,
) : ViewModel() {
val state: StateFlow<State>
@@ -9,6 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.category.CategoryScreen
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
@@ -24,7 +25,7 @@ class CategoryScreen : Screen() {
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<CategoryViewModel>()
val viewModel = metroViewModel<CategoryViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -4,6 +4,11 @@ import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.icerock.moko.resources.StringResource
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -21,16 +26,17 @@ import tachiyomi.domain.category.interactor.RenameCategory
import tachiyomi.domain.category.interactor.ReorderCategory
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class CategoryViewModel(
private val getCategories: GetCategories = Injekt.get(),
private val createCategoryWithName: CreateCategoryWithName = Injekt.get(),
private val deleteCategory: DeleteCategory = Injekt.get(),
private val reorderCategory: ReorderCategory = Injekt.get(),
private val renameCategory: RenameCategory = Injekt.get(),
private val getCategories: GetCategories,
private val createCategoryWithName: CreateCategoryWithName,
private val deleteCategory: DeleteCategory,
private val reorderCategory: ReorderCategory,
private val renameCategory: RenameCategory,
) : ViewModel() {
private val _events: Channel<CategoryEvent> = Channel()
@@ -6,10 +6,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
@@ -29,12 +28,7 @@ class DeepLinkScreen(
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<DeepLinkViewModel>(
factory = DeepLinkViewModel.Factory,
extras = CreationExtras {
set(DeepLinkViewModel.QUERY_KEY, query)
},
)
val viewModel = assistedMetroViewModel<DeepLinkViewModel, DeepLinkViewModel.Factory> { create(query = query) }
val state by viewModel.state.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
@@ -3,9 +3,13 @@ package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.ResolvableSource
@@ -21,30 +25,24 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@AssistedInject
class DeepLinkViewModel(
query: String,
private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
@Assisted query: String,
private val sourceManager: SourceManager,
private val networkToLocalManga: NetworkToLocalManga,
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId,
private val updateMangaFromRemote: UpdateMangaFromRemote,
) : ViewModel() {
val state: StateFlow<DeepLinkViewModel.State>
field = MutableStateFlow<DeepLinkViewModel.State>(State.Loading)
companion object {
val QUERY_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
DeepLinkViewModel(
query = get(QUERY_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(query: String): DeepLinkViewModel
}
init {
@@ -45,6 +45,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu
@@ -65,7 +66,7 @@ object DownloadQueueScreen : Screen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val viewModel = viewModel<DownloadQueueViewModel>()
val viewModel = metroViewModel<DownloadQueueViewModel>()
val downloadList by viewModel.state.collectAsStateWithLifecycle()
val downloadCount by remember {
derivedStateOf { downloadList.sumOf { it.subItems.size } }
@@ -3,6 +3,10 @@ package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
@@ -20,13 +24,14 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class)
class DownloadQueueViewModel(
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadManager: DownloadManager,
) : ViewModel() {
val state: StateFlow<List<DownloadHeaderItem>> = downloadManager.queueState
@@ -16,6 +16,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.history.HistoryScreen
import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
@@ -62,7 +63,7 @@ data object HistoryTab : Tab {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val viewModel = viewModel<HistoryViewModel>()
val viewModel = metroViewModel<HistoryViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
HistoryScreen(
@@ -4,6 +4,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.track.interactor.AddTracks
@@ -46,25 +51,27 @@ import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class HistoryViewModel(
private val addTracks: AddTracks = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getHistory: GetHistory = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val removeHistory: RemoveHistory = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
private val sourceManager: SourceManager = Injekt.get(),
private val addTracks: AddTracks,
private val getCategories: GetCategories,
private val getDuplicateLibraryManga: GetDuplicateLibraryManga,
private val getHistory: GetHistory,
private val getManga: GetManga,
private val getNextChapters: GetNextChapters,
private val libraryPreferences: LibraryPreferences,
private val removeHistory: RemoveHistory,
private val setMangaCategories: SetMangaCategories,
private val updateManga: UpdateManga,
private val sourceManager: SourceManager,
) : ViewModel() {
val snackbarHostState: SnackbarHostState = SnackbarHostState()
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _events.receiveAsFlow()
@@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
@@ -33,7 +34,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabNavigator
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.Screen
import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.ui.browse.BrowseTab
@@ -48,16 +48,14 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import mihon.app.di.appGraph
import soup.compose.material.motion.animation.materialFadeThroughIn
import soup.compose.material.motion.animation.materialFadeThroughOut
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.NavigationBar
import tachiyomi.presentation.core.components.material.NavigationRail
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object HomeScreen : Screen() {
@@ -235,12 +233,13 @@ object HomeScreen : Screen() {
@Composable
private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) {
val context = LocalContext.current
BadgedBox(
badge = {
when {
tab is UpdatesTab -> {
val count by produceState(initialValue = 0) {
val pref = Injekt.get<LibraryPreferences>()
val pref = context.appGraph.libraryPreferences
combine(
pref.newShowUpdatesCount.changes(),
pref.newUpdatesCount.changes(),
@@ -263,7 +262,7 @@ object HomeScreen : Screen() {
}
BrowseTab::class.isInstance(tab) -> {
val count by produceState(initialValue = 0) {
Injekt.get<SourcePreferences>().extensionUpdatesCount.changes()
context.appGraph.sourcePreferences.extensionUpdatesCount.changes()
.collectLatest { value = it }
}
if (count > 0) {
@@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.ui.library
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.flow.SharingStarted
@@ -16,16 +20,17 @@ import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class)
class LibrarySettingsViewModel(
val preferences: BasePreferences = Injekt.get(),
val libraryPreferences: LibraryPreferences = Injekt.get(),
private val setDisplayMode: SetDisplayMode = Injekt.get(),
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
trackerManager: TrackerManager = Injekt.get(),
val preferences: BasePreferences,
val libraryPreferences: LibraryPreferences,
private val setDisplayMode: SetDisplayMode,
private val setSortModeForCategory: SetSortModeForCategory,
trackerManager: TrackerManager,
) : ViewModel() {
val trackersFlow = trackerManager.loggedInTrackersFlow()
@@ -27,6 +27,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.library.DeleteLibraryMangaDialog
import eu.kanade.presentation.library.LibrarySettingsDialog
@@ -43,6 +44,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
@@ -86,14 +88,14 @@ data object LibraryTab : Tab {
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
val viewModel = viewModel<LibraryViewModel>()
val settingsViewModel = viewModel<LibrarySettingsViewModel>()
val viewModel = metroViewModel<LibraryViewModel>()
val settingsViewModel = metroViewModel<LibrarySettingsViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val onClickRefresh: (Category?) -> Boolean = { category ->
val started = LibraryUpdateJob.startNow(context, category)
val started = LibraryUpdateJob.startNow(context.workManager, category)
scope.launch {
val msgRes = when {
!started -> MR.strings.update_already_running
@@ -6,6 +6,11 @@ import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.fastFilterNot
@@ -65,28 +70,29 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracksPerManga
import tachiyomi.domain.track.model.Track
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class, binding = binding<ViewModel>())
class LibraryViewModel(
private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val preferences: BasePreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadCache: DownloadCache = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
private val getLibraryManga: GetLibraryManga,
private val getCategories: GetCategories,
private val getTracksPerManga: GetTracksPerManga,
private val getNextChapters: GetNextChapters,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId,
private val setReadStatus: SetReadStatus,
private val updateManga: UpdateManga,
private val setMangaCategories: SetMangaCategories,
private val preferences: BasePreferences,
private val libraryPreferences: LibraryPreferences,
private val coverCache: CoverCache,
private val sourceManager: SourceManager,
private val downloadManager: DownloadManager,
private val downloadCache: DownloadCache,
private val trackerManager: TrackerManager,
) : ViewModel() {
private val searchQuery = MutableStateFlow<String?>(null)
@@ -67,6 +67,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metro.Inject
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.presentation.components.AdaptiveSheet
@@ -81,7 +82,6 @@ import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
@@ -106,6 +106,9 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.app.di.AppGraph
import mihon.app.di.appGraph
import mihon.core.metro.metroGraph
import mihon.core.migration.Migrator
import mihon.feature.support.SupportUsScreen
import tachiyomi.core.common.Constants
@@ -118,7 +121,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.injectLazy
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Instant
@@ -126,13 +128,19 @@ import kotlin.time.times
class MainActivity : BaseActivity() {
private val libraryPreferences: LibraryPreferences by injectLazy()
private val preferences: BasePreferences by injectLazy()
private val graph: AppGraph by lazy { metroGraph() }
private val downloadCache: DownloadCache by injectLazy()
private val chapterCache: ChapterCache by injectLazy()
@Inject private lateinit var libraryPreferences: LibraryPreferences
private val getIncognitoState: GetIncognitoState by injectLazy()
@Inject private lateinit var preferences: BasePreferences
@Inject private lateinit var downloadCache: DownloadCache
@Inject private lateinit var chapterCache: ChapterCache
@Inject private lateinit var getIncognitoState: GetIncognitoState
@Inject private lateinit var extensionApi: ExtensionApi
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
@@ -144,6 +152,7 @@ class MainActivity : BaseActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
graph.inject(this)
val isLaunch = savedInstanceState == null
// Prevent splash screen showing up on configuration changes
@@ -311,7 +320,7 @@ class MainActivity : BaseActivity() {
LaunchedEffect(Unit) {
if (updaterEnabled) {
try {
val result = AppUpdateChecker().checkForUpdate()
val result = context.appGraph.updateChecker.checkForUpdate()
if (result is GetApplicationRelease.Result.NewUpdate) {
val updateScreen = NewUpdateScreen(
versionName = result.release.version,
@@ -330,7 +339,7 @@ class MainActivity : BaseActivity() {
// Extensions updates
LaunchedEffect(Unit) {
try {
ExtensionApi().checkForUpdates(context)
extensionApi.checkForUpdates(context)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
@@ -5,13 +5,17 @@ import android.net.Uri
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.size.Size
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.saver.Image
@@ -36,30 +40,25 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import tachiyomi.source.local.image.LocalCoverManager
import kotlin.time.Duration.Companion.seconds
@AssistedInject
class MangaCoverViewModel(
private val mangaId: Long,
private val getManga: GetManga = Injekt.get(),
private val imageSaver: ImageSaver = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
@Assisted private val mangaId: Long,
private val getManga: GetManga,
private val imageSaver: ImageSaver,
private val coverCache: CoverCache,
private val updateManga: UpdateManga,
private val coverManager: LocalCoverManager,
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : ViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MangaCoverViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(mangaId: Long): MangaCoverViewModel
}
val state: StateFlow<Manga?> = getManga.subscribe(mangaId)
@@ -140,7 +139,7 @@ class MangaCoverViewModel(
viewModelScope.launchIO {
context.contentResolver.openInputStream(data)?.use {
try {
manga.editCover(Injekt.get(), it, updateManga, coverCache)
manga.editCover(coverManager, it, updateManga, coverCache)
notifyCoverUpdated(context)
} catch (e: Exception) {
notifyFailedCoverUpdate(context, e)
@@ -17,11 +17,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.core.util.ifSourcesLoaded
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga
@@ -83,13 +82,10 @@ class MangaScreen(
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
val viewModel = viewModel<MangaViewModel>(
factory = MangaViewModel.Factory,
extras = CreationExtras {
set(MangaViewModel.MANGA_ID_KEY, mangaId)
set(MangaViewModel.IS_FROM_SOURCE_KEY, fromSource)
},
)
val viewModel =
assistedMetroViewModel<MangaViewModel, MangaViewModel.Factory> {
create(mangaId = mangaId, isFromSource = fromSource)
}
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -243,28 +239,26 @@ class MangaScreen(
)
}
MangaViewModel.Dialog.FullCover -> {
val sm = viewModel<MangaCoverViewModel>(
factory = MangaCoverViewModel.Factory,
extras = CreationExtras {
set(MangaCoverViewModel.MANGA_ID_KEY, successState.manga.id)
},
)
val manga by sm.state.collectAsStateWithLifecycle()
val vm =
assistedMetroViewModel<MangaCoverViewModel, MangaCoverViewModel.Factory> {
create(mangaId = mangaId)
}
val manga by vm.state.collectAsStateWithLifecycle()
if (manga != null) {
val getContent = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
if (it == null) return@rememberLauncherForActivityResult
sm.editCover(context, it)
vm.editCover(context, it)
}
MangaCoverDialog(
manga = manga!!,
snackbarHostState = sm.snackbarHostState,
snackbarHostState = vm.snackbarHostState,
isCustomCover = remember(manga) { manga!!.hasCustomCover() },
onShareClick = { sm.shareCover(context) },
onSaveClick = { sm.saveCover(context) },
onShareClick = { vm.shareCover(context) },
onSaveClick = { vm.saveCover(context) },
onEditClick = {
when (it) {
EditCoverAction.EDIT -> getContent.launch("image/*")
EditCoverAction.DELETE -> sm.deleteCustomCover(context)
EditCoverAction.DELETE -> vm.deleteCustomCover(context)
}
},
onDismissRequest = onDismissRequest,
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.manga
import android.app.Application
import android.content.Context
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
@@ -10,9 +9,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.util.fastAny
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
@@ -31,6 +34,7 @@ import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
@@ -85,60 +89,54 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.i18n.MR
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.floor
@AssistedInject
class MangaViewModel(
@Assisted private val mangaId: Long,
@Assisted private val isFromSource: Boolean,
private val context: Context,
private val mangaId: Long,
private val isFromSource: Boolean,
private val libraryPreferences: LibraryPreferences = Injekt.get(),
trackPreferences: TrackPreferences = Injekt.get(),
readerPreferences: ReaderPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
private val trackChapter: TrackChapter = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadCache: DownloadCache = Injekt.get(),
private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getAvailableScanlators: GetAvailableScanlators = Injekt.get(),
private val getExcludedScanlators: GetExcludedScanlators = Injekt.get(),
private val setExcludedScanlators: SetExcludedScanlators = Injekt.get(),
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
private val libraryPreferences: LibraryPreferences,
trackPreferences: TrackPreferences,
readerPreferences: ReaderPreferences,
private val trackerManager: TrackerManager,
private val trackChapter: TrackChapter,
private val downloadManager: DownloadManager,
private val downloadCache: DownloadCache,
private val getMangaAndChapters: GetMangaWithChapters,
private val getDuplicateLibraryManga: GetDuplicateLibraryManga,
private val getAvailableScanlators: GetAvailableScanlators,
private val getExcludedScanlators: GetExcludedScanlators,
private val setExcludedScanlators: SetExcludedScanlators,
private val setMangaChapterFlags: SetMangaChapterFlags,
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags,
private val setReadStatus: SetReadStatus,
private val updateChapter: UpdateChapter,
private val updateManga: UpdateManga,
private val getCategories: GetCategories,
private val getTracks: GetTracks,
private val addTracks: AddTracks,
private val setMangaCategories: SetMangaCategories,
private val mangaRepository: MangaRepository,
private val filterChaptersForDownload: FilterChaptersForDownload,
private val updateMangaFromRemote: UpdateMangaFromRemote,
private val sourceManager: SourceManager,
private val refreshTracks: RefreshTracks,
private val coverCache: CoverCache,
) : ViewModel() {
val state: StateFlow<State>
field = MutableStateFlow<State>(State.Loading)
val state: StateFlow<MangaViewModel.State>
field = MutableStateFlow<MangaViewModel.State>(State.Loading)
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val IS_FROM_SOURCE_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
MangaViewModel(
context = Injekt.get<Application>(),
mangaId = get(MANGA_ID_KEY)!!,
isFromSource = get(IS_FROM_SOURCE_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(mangaId: Long, isFromSource: Boolean): MangaViewModel
}
val snackbarHostState: SnackbarHostState = SnackbarHostState()
private val successState: State.Success?
get() = state.value as? State.Success
@@ -236,7 +234,7 @@ class MangaViewModel(
state.update {
State.Success(
manga = manga,
source = Injekt.get<SourceManager>().getOrStub(manga.source),
source = sourceManager.getOrStub(manga.source),
isFromSource = isFromSource,
chapters = chapters,
availableScanlators = getAvailableScanlators.await(mangaId),
@@ -348,7 +346,7 @@ class MangaViewModel(
// Remove from library
if (updateManga.awaitUpdateFavorite(manga.id, false)) {
// Remove covers and update last modified in db
if (manga.removeCovers() != manga) {
if (manga.removeCovers(coverCache) != manga) {
updateManga.awaitUpdateCoverLastModified(manga.id)
}
withUIContext { onRemoved() }
@@ -777,9 +775,7 @@ class MangaViewModel(
}
}
private suspend fun refreshTrackers(
refreshTracks: RefreshTracks = Injekt.get(),
) {
private suspend fun refreshTrackers() {
refreshTracks.await(mangaId)
.filter { it.first != null }
.forEach { (track, e) ->
@@ -6,12 +6,16 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.presentation.manga.MangaNotesScreen
import eu.kanade.presentation.util.Screen
import kotlinx.coroutines.flow.MutableStateFlow
@@ -20,8 +24,6 @@ import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaNotesScreen(
private val manga: Manga,
@@ -30,12 +32,7 @@ class MangaNotesScreen(
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_KEY, manga)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> { create(manga = manga) }
val state by viewModel.state.collectAsState()
MangaNotesScreen(
@@ -45,24 +42,20 @@ class MangaNotesScreen(
)
}
@AssistedInject
class Model(
private val manga: Manga,
private val updateMangaNotes: UpdateMangaNotes = Injekt.get(),
@Assisted private val manga: Manga,
private val updateMangaNotes: UpdateMangaNotes,
) : ViewModel() {
val state: StateFlow<State>
field = MutableStateFlow<State>(State(manga, manga.notes))
companion object {
val MANGA_KEY = CreationExtras.Key<Manga>()
val Factory = viewModelFactory {
initializer {
Model(
manga = get(MANGA_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(manga: Manga): Model
}
fun updateNotes(content: String) {
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.app.Application
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -32,14 +31,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.icerock.moko.resources.StringResource
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.ui.UiPreferences
@@ -52,7 +55,6 @@ import eu.kanade.presentation.track.TrackerSearch
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
@@ -70,6 +72,7 @@ import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import logcat.LogPriority
import mihon.app.di.appGraph
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext
@@ -85,8 +88,6 @@ import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Clock
import kotlin.time.Instant
@@ -100,15 +101,9 @@ data class TrackInfoDialogHomeScreen(
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_ID_KEY, mangaId)
set(Model.SOURCE_ID_KEY, sourceId)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> { create(mangaId = mangaId, sourceId = sourceId) }
val dateFormat = remember { UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat.get()) }
val dateFormat = remember { UiPreferences.dateFormat(context.appGraph.uiPreferences.dateFormat.get()) }
val state by viewModel.state.collectAsState()
TrackInfoDialogHome(
@@ -202,27 +197,26 @@ data class TrackInfoDialogHomeScreen(
}
}
@AssistedInject
class Model(
private val mangaId: Long,
private val sourceId: Long,
private val getTracks: GetTracks = Injekt.get(),
@Assisted private val mangaId: Long,
@Assisted private val sourceId: Long,
private val context: Context,
private val getTracks: GetTracks,
private val getManga: GetManga,
private val trackerManager: TrackerManager,
private val sourceManager: SourceManager,
private val refreshTracks: RefreshTracks,
) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State())
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY)!!,
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(mangaId: Long, sourceId: Long): Model
}
init {
@@ -242,20 +236,19 @@ data class TrackInfoDialogHomeScreen(
fun registerEnhancedTracking(item: TrackItem) {
item.tracker as EnhancedTracker
viewModelScope.launchNonCancellable {
val manga = Injekt.get<GetManga>().await(mangaId) ?: return@launchNonCancellable
val manga = getManga.await(mangaId) ?: return@launchNonCancellable
try {
val matchResult = item.tracker.match(manga) ?: throw Exception()
item.tracker.register(matchResult, mangaId)
} catch (_: Exception) {
withUIContext { Injekt.get<Application>().toast(MR.strings.error_no_match) }
withUIContext {
context.toast(MR.strings.error_no_match)
}
}
}
}
private suspend fun refreshTrackers() {
val refreshTracks = Injekt.get<RefreshTracks>()
val context = Injekt.get<Application>()
refreshTracks.await(mangaId)
.filter { it.first != null }
.forEach { (track, e) ->
@@ -281,8 +274,8 @@ data class TrackInfoDialogHomeScreen(
}
private fun List<Track>.mapToTrackItem(): List<TrackItem> {
val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers()
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
val loggedInTrackers = trackerManager.loggedInTrackers()
val source = sourceManager.getOrStub(sourceId)
return loggedInTrackers
// Map to TrackItem
.map { service -> TrackItem(find { it.trackerId == service.id }, service) }
@@ -297,7 +290,7 @@ data class TrackInfoDialogHomeScreen(
}
}
private data class TrackStatusSelectorScreen(
data class TrackStatusSelectorScreen(
private val track: Track,
private val serviceId: Long,
) : Screen() {
@@ -305,13 +298,7 @@ private data class TrackStatusSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> { create(track = track, trackerId = serviceId) }
val state by viewModel.state.collectAsState()
TrackStatusSelector(
selection = state.selection,
@@ -325,28 +312,25 @@ private data class TrackStatusSelectorScreen(
)
}
@AssistedInject
class Model(
private val track: Track,
private val tracker: Tracker,
@Assisted private val track: Track,
@Assisted private val trackerId: Long,
trackerManager: TrackerManager,
) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State(track.status))
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(track: Track, trackerId: Long): Model
}
val tracker = trackerManager.get(trackerId)!!
fun getSelections(): Map<Long, StringResource?> {
return tracker.getStatusList().associateWith { tracker.getStatus(it) }
}
@@ -368,7 +352,7 @@ private data class TrackStatusSelectorScreen(
}
}
private data class TrackChapterSelectorScreen(
data class TrackChapterSelectorScreen(
private val track: Track,
private val serviceId: Long,
) : Screen() {
@@ -376,13 +360,7 @@ private data class TrackChapterSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> { create(track = track, trackerId = serviceId) }
val state by viewModel.state.collectAsState()
TrackChapterSelector(
@@ -397,28 +375,25 @@ private data class TrackChapterSelectorScreen(
)
}
@AssistedInject
class Model(
private val track: Track,
private val tracker: Tracker,
@Assisted private val track: Track,
@Assisted private val trackerId: Long,
trackerManager: TrackerManager,
) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State(track.lastChapterRead.toInt()))
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(track: Track, trackerId: Long): Model
}
val tracker = trackerManager.get(trackerId)!!
fun getRange(): Iterable<Int> {
val endRange = if (track.totalChapters > 0) {
track.totalChapters
@@ -445,7 +420,7 @@ private data class TrackChapterSelectorScreen(
}
}
private data class TrackScoreSelectorScreen(
data class TrackScoreSelectorScreen(
private val track: Track,
private val serviceId: Long,
) : Screen() {
@@ -453,13 +428,7 @@ private data class TrackScoreSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> { create(track = track, trackerId = serviceId) }
val state by viewModel.state.collectAsState()
TrackScoreSelector(
@@ -474,26 +443,27 @@ private data class TrackScoreSelectorScreen(
)
}
@AssistedInject
class Model(
private val track: Track,
private val tracker: Tracker,
@Assisted private val track: Track,
@Assisted private val trackerId: Long,
trackerManager: TrackerManager,
) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State(tracker.displayScore(track)))
field = MutableStateFlow<Model.State>(State(""))
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(track: Track, trackerId: Long): Model
}
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
val tracker = trackerManager.get(trackerId)!!
init {
tracker.displayScore(track).let(::setSelection)
}
fun getSelections(): List<String> {
@@ -517,7 +487,7 @@ private data class TrackScoreSelectorScreen(
}
}
private data class TrackDateSelectorScreen(
data class TrackDateSelectorScreen(
private val track: Track,
private val serviceId: Long,
private val start: Boolean,
@@ -573,14 +543,9 @@ private data class TrackDateSelectorScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
set(Model.START_KEY, start)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> {
create(track = track, trackerId = serviceId, start = start)
}
val canRemove = if (start) {
track.startDate > 0
@@ -604,28 +569,23 @@ private data class TrackDateSelectorScreen(
)
}
@AssistedInject
class Model(
private val track: Track,
private val tracker: Tracker,
private val start: Boolean,
@Assisted private val track: Track,
@Assisted private val trackerId: Long,
@Assisted private val start: Boolean,
trackerManager: TrackerManager,
) : ViewModel() {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val START_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
start = get(START_KEY) as Boolean,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(track: Track, trackerId: Long, start: Boolean): Model
}
private val tracker = trackerManager.get(trackerId)!!
// In UTC
val initialSelection: Long
get() {
@@ -654,7 +614,7 @@ private data class TrackDateSelectorScreen(
}
}
private data class TrackDateRemoverScreen(
data class TrackDateRemoverScreen(
private val track: Track,
private val serviceId: Long,
private val start: Boolean,
@@ -663,14 +623,9 @@ private data class TrackDateRemoverScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
set(Model.START_KEY, start)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> {
create(track = track, trackerId = serviceId, start = start)
}
AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
icon = {
@@ -720,28 +675,23 @@ private data class TrackDateRemoverScreen(
)
}
@AssistedInject
class Model(
private val track: Track,
private val tracker: Tracker,
private val start: Boolean,
@Assisted private val track: Track,
@Assisted private val trackerId: Long,
@Assisted private val start: Boolean,
trackerManager: TrackerManager,
) : ViewModel() {
companion object {
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val START_KEY = CreationExtras.Key<Boolean>()
val Factory = viewModelFactory {
initializer {
Model(
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
start = get(START_KEY) as Boolean,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(track: Track, trackerId: Long, start: Boolean): Model
}
private val tracker = trackerManager.get(trackerId)!!
fun getServiceName() = tracker.name
fun removeDate() {
@@ -766,15 +716,14 @@ data class TrackerSearchScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_ID_KEY, mangaId)
set(Model.CURRENT_URL_KEY, currentUrl)
set(Model.INITIAL_QUERY_KEY, initialQuery)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> {
create(
mangaId = mangaId,
currentUrl = currentUrl,
initialQuery = initialQuery,
trackerId = serviceId,
)
}
val state by viewModel.state.collectAsState()
@@ -796,34 +745,32 @@ data class TrackerSearchScreen(
)
}
@AssistedInject
class Model(
private val mangaId: Long,
private val currentUrl: String?,
initialQuery: String,
private val tracker: Tracker,
@Assisted private val mangaId: Long,
@Assisted private val currentUrl: String?,
@Assisted initialQuery: String,
@Assisted private val trackerId: Long,
trackerManager: TrackerManager,
) : ViewModel() {
val state: StateFlow<Model.State>
field = MutableStateFlow<Model.State>(State())
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val CURRENT_URL_KEY = CreationExtras.Key<String?>()
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY)!!,
currentUrl = get(CURRENT_URL_KEY),
initialQuery = get(INITIAL_QUERY_KEY)!!,
tracker = get(TRACKER_KEY)!!,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(
mangaId: Long,
currentUrl: String?,
initialQuery: String,
trackerId: Long,
): Model
}
private val tracker = trackerManager.get(trackerId)!!
val supportsPrivateTracking = tracker.supportsPrivateTracking
init {
@@ -871,7 +818,7 @@ data class TrackerSearchScreen(
}
}
private data class TrackerRemoveScreen(
data class TrackerRemoveScreen(
private val mangaId: Long,
private val track: Track,
private val serviceId: Long,
@@ -880,14 +827,9 @@ private data class TrackerRemoveScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<Model>(
factory = Model.Factory,
extras = CreationExtras {
set(Model.MANGA_ID_KEY, mangaId)
set(Model.TRACK_KEY, track)
set(Model.TRACKER_KEY, Injekt.get<TrackerManager>().get(serviceId)!!)
},
)
val viewModel = assistedMetroViewModel<Model, Model.Factory> {
create(mangaId = mangaId, track = track, trackerId = serviceId)
}
val serviceName = viewModel.getName()
var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent(
@@ -950,29 +892,24 @@ private data class TrackerRemoveScreen(
)
}
@AssistedInject
class Model(
private val mangaId: Long,
private val track: Track,
private val tracker: Tracker,
private val deleteTrack: DeleteTrack = Injekt.get(),
@Assisted private val mangaId: Long,
@Assisted private val track: Track,
@Assisted private val trackerId: Long,
private val deleteTrack: DeleteTrack,
trackerManager: TrackerManager,
) : ViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val TRACK_KEY = CreationExtras.Key<Track>()
val TRACKER_KEY = CreationExtras.Key<Tracker>()
val Factory = viewModelFactory {
initializer {
Model(
mangaId = get(MANGA_ID_KEY) as Long,
track = get(TRACK_KEY) as Track,
tracker = get(TRACKER_KEY) as Tracker,
)
}
}
@AssistedFactory
@ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class)
interface Factory : ManualViewModelAssistedFactory {
fun create(mangaId: Long, track: Track, trackerId: Long): Model
}
private val tracker = trackerManager.get(trackerId)!!
fun getName() = tracker.name
fun isDeletable() = tracker is DeletableTracker
@@ -9,12 +9,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import dev.zacsweers.metrox.viewmodel.metroViewModel
import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.more.MoreScreen
@@ -34,8 +38,6 @@ import mihon.feature.support.SupportUsScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data object MoreTab : Tab {
@@ -58,7 +60,7 @@ data object MoreTab : Tab {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<MoreViewModel>()
val viewModel = metroViewModel<MoreViewModel>()
val downloadQueueState by viewModel.downloadQueueState.collectAsState()
MoreScreen(
downloadQueueStateProvider = { downloadQueueState },
@@ -77,9 +79,12 @@ data object MoreTab : Tab {
}
}
@Inject
@ViewModelKey
@ContributesIntoMap(AppScope::class)
class MoreViewModel(
private val downloadManager: DownloadManager = Injekt.get(),
preferences: BasePreferences = Injekt.get(),
private val downloadManager: DownloadManager,
preferences: BasePreferences,
) : ViewModel() {
var downloadedOnly by preferences.downloadedOnly.asState(viewModelScope)
@@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
import eu.kanade.presentation.more.NewUpdateScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.openInBrowser
@@ -23,13 +23,10 @@ class NewUpdateScreen(
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val viewModel = viewModel<NewUpdateScreenModel>(
factory = NewUpdateScreenModel.Factory,
extras = CreationExtras {
set(NewUpdateScreenModel.CHANGELOG_INFO_KEY, changelogInfo)
set(NewUpdateScreenModel.DOWNLOAD_LINK_KEY, downloadLink)
},
)
val viewModel =
assistedMetroViewModel<NewUpdateScreenModel, NewUpdateScreenModel.Factory> {
create(changelogInfo = changelogInfo, downloadLink = downloadLink)
}
val state by viewModel.state.collectAsState()

Some files were not shown because too many files have changed in this diff Show More