Fix compile time errors and make it build

This commit is contained in:
NGB-Was-Taken
2025-11-15 16:56:17 +05:45
parent bdbaecd975
commit 4c1124fdb0
22 changed files with 35 additions and 1299 deletions
@@ -95,7 +95,7 @@ class SourcePreferences(
fun defaultMangaOrder() = preferenceStore.getString("default_manga_order", "")
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
//fun migrationSources() = preferenceStore.getString("migrate_sources", "")
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
@@ -74,6 +74,7 @@ fun ChapterListDialog(
downloadManager.isChapterDownloaded(
chapterItem.chapter.name,
chapterItem.chapter.scanlator,
chapterItem.chapter.url,
chapterItem.manga.ogTitle,
chapterItem.manga.source,
)
@@ -145,7 +145,7 @@ class DownloadProvider(
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return emptyList()
return mangaDir.listFiles().orEmpty().asList().filter {
chapters.find { chp ->
getValidChapterDirNames(chp.name, chp.scanlator).any { dir ->
getValidChapterDirNames(chp.name, chp.scanlator, chp.url).any { dir ->
mangaDir.findFile(dir) != null
}
} == null ||
@@ -8,7 +8,7 @@ import uy.kohesive.injekt.api.get
fun Source.getNameForMangaInfo(
// SY -->
mergeSources: List<Source>?,
mergeSources: List<Source>? = null,
enabledLanguages: List<String> = Injekt.get<SourcePreferences>().enabledLanguages().get()
.filterNot { it in listOf("all", "other") },
// SY <--
@@ -1,135 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.view.LayoutInflater
import android.widget.CompoundButton
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.util.system.toast
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.util.lang.toLong
import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.injectLazy
@Composable
fun MigrationBottomSheetDialog(
onDismissRequest: () -> Unit,
onStartMigration: (extraParam: String?) -> Unit,
) {
val startMigration = rememberUpdatedState(onStartMigration)
val state = remember {
MigrationBottomSheetDialogState(startMigration)
}
AdaptiveSheet(onDismissRequest = onDismissRequest) {
AndroidView(
factory = { factoryContext ->
val binding = MigrationBottomSheetBinding.inflate(LayoutInflater.from(factoryContext))
state.initPreferences(binding)
binding.root
},
modifier = Modifier.fillMaxWidth(),
)
}
}
class MigrationBottomSheetDialogState(private val onStartMigration: State<(extraParam: String?) -> Unit>) {
private val preferences: SourcePreferences by injectLazy()
/**
* Init general reader preferences.
*/
fun initPreferences(binding: MigrationBottomSheetBinding) {
val flags = preferences.migrateFlags().get()
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
binding.migNotes.isChecked = MigrationFlags.hasNotes(flags)
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migNotes.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
binding.extraSearchParamText.isVisible = false
binding.extraSearchParam.setOnCheckedChangeListener { _, isChecked ->
binding.extraSearchParamText.isVisible = isChecked
}
binding.sourceGroup.bindToPreference(preferences.useSourceWithMost())
binding.skipStep.isChecked = preferences.skipPreMigration().get()
binding.HideNotFoundManga.isChecked = preferences.hideNotFoundMigration().get()
binding.OnlyShowUpdates.isChecked = preferences.showOnlyUpdatesMigration().get()
binding.skipStep.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
binding.root.context.toast(
SYMR.strings.pre_migration_skip_toast,
Toast.LENGTH_LONG,
)
}
}
binding.migrateBtn.setOnClickListener {
preferences.skipPreMigration().set(binding.skipStep.isChecked)
preferences.hideNotFoundMigration().set(binding.HideNotFoundManga.isChecked)
preferences.showOnlyUpdatesMigration().set(binding.OnlyShowUpdates.isChecked)
onStartMigration.value(
if (binding.useSmartSearch.isChecked && binding.extraSearchParamText.text.isNotBlank()) {
binding.extraSearchParamText.toString()
} else {
null
},
)
}
}
private fun setFlags(binding: MigrationBottomSheetBinding) {
var flags = 0
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
if (binding.migNotes.isChecked) flags = flags or MigrationFlags.NOTES
preferences.migrateFlags().set(flags)
}
/**
* Binds a checkbox or switch view with a boolean preference.
*/
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
isChecked = pref.get()
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
}
/**
* Binds a radio group with a boolean preference.
*/
private fun RadioGroup.bindToPreference(pref: Preference<Boolean>) {
(getChildAt(pref.get().toLong().toInt()) as RadioButton).isChecked = true
setOnCheckedChangeListener { _, value ->
val index = indexOfChild(findViewById(value))
pref.set(index == 1)
}
}
}
@@ -1,213 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.view.LayoutInflater
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Deselect
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import java.io.Serializable
import kotlin.math.roundToInt
sealed class MigrationType : Serializable {
data class MangaList(val mangaIds: List<Long>) : MigrationType()
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
}
class PreMigrationScreen(val migration: MigrationType) : Screen() {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val navigator = LocalNavigator.currentOrThrow
var fabExpanded by remember { mutableStateOf(true) }
val items by screenModel.state.collectAsState()
val adapter by screenModel.adapter.collectAsState()
LaunchedEffect(items.isNotEmpty(), adapter != null) {
if (adapter != null && items.isNotEmpty()) {
adapter?.updateDataSet(items)
}
}
val nestedScrollConnection = remember {
// All this lines just for fab state :/
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
fabExpanded = available.y >= 0
return scrollBehavior.nestedScrollConnection.onPreScroll(available, source)
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available)
}
}
}
Scaffold(
topBar = {
AppBar(
title = stringResource(SYMR.strings.select_sources),
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(SYMR.strings.select_none),
icon = Icons.Outlined.Deselect,
onClick = { screenModel.massSelect(false) },
),
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = { screenModel.massSelect(true) },
),
AppBar.OverflowAction(
title = stringResource(SYMR.strings.match_enabled_sources),
onClick = { screenModel.matchSelection(true) },
),
AppBar.OverflowAction(
title = stringResource(SYMR.strings.match_pinned_sources),
onClick = { screenModel.matchSelection(false) },
),
),
)
},
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_migrate)) },
icon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowForward,
contentDescription = stringResource(MR.strings.action_migrate),
)
},
onClick = {
screenModel.onMigrationSheet(true)
},
expanded = fabExpanded,
)
},
) { contentPadding ->
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() }
val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() }
val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() }
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
screenModel.controllerBinding = PreMigrationListBinding.inflate(LayoutInflater.from(context))
screenModel.adapter.value = MigrationSourceAdapter(screenModel.clickListener)
screenModel.controllerBinding.root.adapter = screenModel.adapter.value
screenModel.adapter.value?.isHandleDragEnabled = true
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
screenModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
right = right,
bottom = bottom,
)
},
)
}
}
val migrationSheetOpen by screenModel.migrationSheetOpen.collectAsState()
if (migrationSheetOpen) {
MigrationBottomSheetDialog(
onDismissRequest = { screenModel.onMigrationSheet(false) },
onStartMigration = { extraParam ->
screenModel.onMigrationSheet(false)
screenModel.saveEnabledSources()
navigator replace MigrationListScreen(MigrationProcedureConfig(migration, extraParam))
},
)
}
}
companion object {
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), null),
)
} else {
PreMigrationScreen(MigrationType.MangaList(mangaIds))
},
)
}
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, fromMangaId: Long, toManga: Long?) {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(MigrationType.MangaSingle(fromMangaId, toManga), null),
)
} else {
PreMigrationScreen(MigrationType.MangaSingle(fromMangaId, toManga))
},
)
}
}
}
@@ -1,137 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PreMigrationScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : ScreenModel {
private val _state = MutableStateFlow(emptyList<MigrationSourceItem>())
val state = _state.asStateFlow()
private val _migrationSheetOpen = MutableStateFlow(false)
val migrationSheetOpen = _migrationSheetOpen.asStateFlow()
lateinit var controllerBinding: PreMigrationListBinding
var adapter: MutableStateFlow<MigrationSourceAdapter?> = MutableStateFlow(null)
val clickListener = FlexibleAdapter.OnItemClickListener { _, position ->
val adapter = adapter.value ?: return@OnItemClickListener false
adapter.getItem(position)?.let {
it.sourceEnabled = !it.sourceEnabled
}
adapter.notifyItemChanged(position)
false
}
init {
screenModelScope.launchIO {
val enabledSources = getEnabledSources()
_state.update { enabledSources }
}
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<MigrationSourceItem> {
val languages = sourcePreferences.enabledLanguages().get()
val sourcesSaved = sourcePreferences.migrationSources().get().split("/")
.mapNotNull { it.toLongOrNull() }
val disabledSources = sourcePreferences.disabledSources().get()
.mapNotNull { it.toLongOrNull() }
val sources = sourceManager.getVisibleCatalogueSources()
.asSequence()
.filterIsInstance<HttpSource>()
.filter { it.lang in languages }
.sortedBy { "(${it.lang}) ${it.name}" }
.map {
MigrationSourceItem(
it,
isEnabled(
sourcesSaved,
disabledSources,
it.id,
),
)
}
.toList()
return sources
.filter { it.sourceEnabled }
.sortedBy { sourcesSaved.indexOf(it.source.id) }
.plus(
sources.filterNot { it.sourceEnabled },
)
}
fun isEnabled(
sourcesSaved: List<Long>,
disabledSources: List<Long>,
id: Long,
): Boolean {
return if (sourcesSaved.isEmpty()) {
id !in disabledSources
} else {
id in sourcesSaved
}
}
fun massSelect(selectAll: Boolean) {
val adapter = adapter.value ?: return
adapter.currentItems.forEach {
it.sourceEnabled = selectAll
}
adapter.notifyDataSetChanged()
}
fun matchSelection(matchEnabled: Boolean) {
val adapter = adapter.value ?: return
val enabledSources = if (matchEnabled) {
sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() }
} else {
sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
}
val items = adapter.currentItems.toList()
items.forEach {
it.sourceEnabled = if (matchEnabled) {
it.source.id !in enabledSources
} else {
it.source.id in enabledSources
}
}
val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled }
adapter.updateDataSet(sortedItems)
}
fun onMigrationSheet(isOpen: Boolean) {
_migrationSheetOpen.value = isOpen
}
fun saveEnabledSources() {
val listOfSources = adapter.value?.currentItems
?.filterIsInstance<MigrationSourceItem>()
?.filter {
it.sourceEnabled
}
?.joinToString("/") { it.source.id.toString() }
.orEmpty()
sourcePreferences.migrationSources().set(listOfSources)
}
}
@@ -1,155 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrationListScreen
import eu.kanade.presentation.browse.components.MigrationExitDialog
import eu.kanade.presentation.browse.components.MigrationMangaDialog
import eu.kanade.presentation.browse.components.MigrationProgressDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast
import exh.util.overEq
import exh.util.underEq
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.i18n.sy.SYMR
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() {
var newSelectedItem: Pair<Long, Long>? = null
@Composable
override fun Content() {
val screenModel = rememberScreenModel { MigrationListScreenModel(config) }
val items by screenModel.migratingItems.collectAsState()
val migrationDone by screenModel.migrationDone.collectAsState()
val unfinishedCount by screenModel.unfinishedCount.collectAsState()
val dialog by screenModel.dialog.collectAsState()
val migrateProgress by screenModel.migratingProgress.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
LaunchedEffect(items) {
if (items?.isEmpty() == true) {
val manualMigrations = screenModel.manualMigrations.value
context.toast(
context.pluralStringResource(
SYMR.plurals.entry_migrated,
manualMigrations,
manualMigrations,
),
)
if (!screenModel.hideNotFound) {
navigator.pop()
}
}
}
LaunchedEffect(newSelectedItem) {
if (newSelectedItem != null) {
val (oldId, newId) = newSelectedItem!!
screenModel.useMangaForMigration(context, newId, oldId)
newSelectedItem = null
}
}
LaunchedEffect(screenModel) {
screenModel.navigateOut.collect {
if (items.orEmpty().size == 1 && navigator.items.any { it is MangaScreen }) {
val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.id
withUIContext {
if (mangaId != null) {
val newStack = navigator.items.filter {
it !is MangaScreen &&
it !is MigrationListScreen &&
it !is PreMigrationScreen
} + MangaScreen(mangaId)
navigator replaceAll newStack.first()
navigator.push(newStack.drop(1))
// need to set the navigator in a pop state to dispose of everything properly
navigator.push(this@MigrationListScreen)
navigator.pop()
} else {
navigator.pop()
}
}
} else {
withUIContext {
navigator.pop()
}
}
}
}
MigrationListScreen(
items = items ?: persistentListOf(),
migrationDone = migrationDone,
unfinishedCount = unfinishedCount,
getManga = screenModel::getManga,
getChapterInfo = screenModel::getChapterInfo,
getSourceName = screenModel::getSourceName,
onMigrationItemClick = {
navigator.push(MangaScreen(it.id, true))
},
openMigrationDialog = screenModel::openMigrateDialog,
skipManga = { screenModel.removeManga(it) },
searchManually = { migrationItem ->
val sources = screenModel.getMigrationSources()
val validSources = if (sources.size == 1) {
sources
} else {
sources.filter { it.id != migrationItem.manga.source }
}
val searchScreen = MigrateSearchScreen(migrationItem.manga.id, validSources.map { it.id })
navigator push searchScreen
},
migrateNow = { screenModel.migrateManga(it, false) },
copyNow = { screenModel.migrateManga(it, true) },
)
val onDismissRequest = { screenModel.dialog.value = null }
when (
@Suppress("NAME_SHADOWING")
val dialog = dialog
) {
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
MigrationMangaDialog(
onDismissRequest = onDismissRequest,
copy = dialog.copy,
mangaSet = dialog.mangaSet,
mangaSkipped = dialog.mangaSkipped,
copyManga = screenModel::copyMangas,
migrateManga = screenModel::migrateMangas,
)
}
MigrationListScreenModel.Dialog.MigrationExitDialog -> {
MigrationExitDialog(
onDismissRequest = onDismissRequest,
exitMigration = navigator::pop,
)
}
null -> Unit
}
if (!migrateProgress.isNaN() && migrateProgress overEq 0f && migrateProgress underEq 1f) {
MigrationProgressDialog(
progress = migrateProgress,
exitMigration = screenModel::cancelMigrate,
)
}
BackHandler(true) {
screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog
}
}
}
@@ -1,609 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.content.Context
import android.widget.Toast
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.util.system.toast
import exh.smartsearch.SmartSourceSearchEngine
import exh.source.MERGED_SOURCE_ID
import exh.util.ThrottleManager
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.DeleteTrack
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.atomic.AtomicInteger
class MigrationListScreenModel(
private val config: MigrationProcedureConfig,
private val preferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getHistoryByMangaId: GetHistory = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val deleteTrack: DeleteTrack = Injekt.get(),
) : ScreenModel {
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
private val throttleManager = ThrottleManager()
val migratingItems = MutableStateFlow<ImmutableList<MigratingManga>?>(null)
val migrationDone = MutableStateFlow(false)
val unfinishedCount = MutableStateFlow(0)
val manualMigrations = MutableStateFlow(0)
val hideNotFound = preferences.hideNotFoundMigration().get()
val showOnlyUpdates = preferences.showOnlyUpdatesMigration().get()
val navigateOut = MutableSharedFlow<Unit>()
val dialog = MutableStateFlow<Dialog?>(null)
val migratingProgress = MutableStateFlow(Float.MAX_VALUE)
private var migrateJob: Job? = null
init {
screenModelScope.launchIO {
val mangaIds = when (val migration = config.migration) {
is MigrationType.MangaList -> {
migration.mangaIds
}
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
}
runMigrations(
mangaIds
.map {
async {
val manga = getManga.await(it) ?: return@async null
MigratingManga(
manga = manga,
chapterInfo = getChapterInfo(it),
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
if (manga.source == MERGED_SOURCE_ID) {
getMergedReferencesById.await(manga.id)
.map { sourceManager.getOrStub(it.mangaSourceId) }
} else {
null
},
),
parentContext = screenModelScope.coroutineContext,
)
}
}
.awaitAll()
.filterNotNull()
.also {
migratingItems.value = it.toImmutableList()
},
)
}
}
suspend fun getManga(result: SearchResult.Result) = getManga(result.id)
suspend fun getManga(id: Long) = getManga.await(id)
suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id)
suspend fun getChapterInfo(id: Long) = getChaptersByMangaId.await(id).let { chapters ->
MigratingManga.ChapterInfo(
latestChapter = chapters.maxOfOrNull { it.chapterNumber },
chapterCount = chapters.size,
)
}
fun getSourceName(manga: Manga) = sourceManager.getOrStub(manga.source).getNameForMangaInfo(null)
fun getMigrationSources() = preferences.migrationSources().get().split("/").mapNotNull {
val value = it.toLongOrNull() ?: return@mapNotNull null
sourceManager.get(value) as? CatalogueSource
}
private suspend fun runMigrations(mangas: List<MigratingManga>) {
throttleManager.resetThrottle()
unfinishedCount.value = mangas.size
val useSourceWithMost = preferences.useSourceWithMost().get()
val useSmartSearch = preferences.smartMigration().get()
val sources = getMigrationSources()
for (manga in mangas) {
if (!currentCoroutineContext().isActive) {
break
}
// in case it was removed
when (val migration = config.migration) {
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
continue
}
else -> Unit
}
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
val mangaObj = manga.manga
val mangaSource = sourceManager.getOrStub(mangaObj.source)
val result = try {
manga.migrationScope.async {
val validSources = if (sources.size == 1) {
sources
} else {
sources.filter { it.id != mangaSource.id }
}
when (val migration = config.migration) {
is MigrationType.MangaSingle -> if (migration.toManga != null) {
val localManga = getManga.await(migration.toManga)
if (localManga != null) {
val source = sourceManager.get(localManga.source) as? CatalogueSource
if (source != null) {
val chapters = if (source is EHentai) {
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
} else {
source.getChapterList(localManga.toSManga())
}
try {
syncChaptersWithSource.await(chapters, localManga, source)
} catch (_: Exception) {
}
manga.progress.value = validSources.size to validSources.size
return@async localManga
}
}
}
else -> Unit
}
if (useSourceWithMost) {
val sourceSemaphore = Semaphore(3)
val processedSources = AtomicInteger()
validSources.map { source ->
async async2@{
sourceSemaphore.withPermit {
try {
val searchResult = if (useSmartSearch) {
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
} else {
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
}
if (searchResult != null &&
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
) {
val localManga = networkToLocalManga(searchResult)
val chapters = if (source is EHentai) {
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
} else {
source.getChapterList(localManga.toSManga())
}
try {
syncChaptersWithSource.await(chapters, localManga, source)
} catch (_: Exception) {
return@async2 null
}
manga.progress.value =
validSources.size to processedSources.incrementAndGet()
localManga to chapters.size
} else {
null
}
} catch (e: CancellationException) {
// Ignore cancellations
throw e
} catch (_: Exception) {
null
}
}
}
}.mapNotNull { it.await() }.maxByOrNull { it.second }?.first
} else {
validSources.forEachIndexed { index, source ->
val searchResult = try {
val searchResult = if (useSmartSearch) {
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
} else {
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
}
if (searchResult != null) {
val localManga = networkToLocalManga(searchResult)
val chapters = try {
if (source is EHentai) {
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
} else {
source.getChapterList(localManga.toSManga())
}
} catch (e: Exception) {
this@MigrationListScreenModel.logcat(LogPriority.ERROR, e)
emptyList()
}
syncChaptersWithSource.await(chapters, localManga, source)
localManga
} else {
null
}
} catch (e: CancellationException) {
// Ignore cancellations
throw e
} catch (_: Exception) {
null
}
manga.progress.value = validSources.size to (index + 1)
if (searchResult != null) return@async searchResult
}
null
}
}.await()
} catch (_: CancellationException) {
// Ignore canceled migrations
continue
}
if (result != null && result.thumbnailUrl == null) {
try {
val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga())
updateManga.awaitUpdateFromSource(result, newManga, true)
} catch (e: CancellationException) {
// Ignore cancellations
throw e
} catch (_: Exception) {
}
}
manga.searchResult.value = if (result == null) {
SearchResult.NotFound
} else {
SearchResult.Result(result.id)
}
if (result == null && hideNotFound) {
removeManga(manga)
}
if (result != null &&
showOnlyUpdates &&
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
) {
removeManga(manga)
}
sourceFinished()
}
}
}
private suspend fun sourceFinished() {
unfinishedCount.value = migratingItems.value.orEmpty().count {
it.searchResult.value != SearchResult.Searching
}
if (allMangasDone()) {
migrationDone.value = true
}
if (migratingItems.value?.isEmpty() == true) {
navigateOut()
}
}
fun allMangasDone() = migratingItems.value.orEmpty().all { it.searchResult.value != SearchResult.Searching } &&
migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Result }
fun mangasSkipped() = migratingItems.value.orEmpty().count { it.searchResult.value == SearchResult.NotFound }
private suspend fun migrateMangaInternal(
prevManga: Manga,
manga: Manga,
replace: Boolean,
) {
if (prevManga.id == manga.id) return // Nothing to migrate
val flags = preferences.migrateFlags().get()
// Update chapters read
if (MigrationFlags.hasChapters(flags)) {
val prevMangaChapters = getChaptersByMangaId.await(prevManga.id)
val maxChapterRead = prevMangaChapters.filter(Chapter::read)
.maxOfOrNull(Chapter::chapterNumber)
val dbChapters = getChaptersByMangaId.await(manga.id)
val prevHistoryList = getHistoryByMangaId.await(prevManga.id)
val chapterUpdates = mutableListOf<ChapterUpdate>()
val historyUpdates = mutableListOf<HistoryUpdate>()
dbChapters.forEach { chapter ->
if (chapter.isRecognizedNumber) {
val prevChapter = prevMangaChapters.find {
it.isRecognizedNumber &&
it.chapterNumber == chapter.chapterNumber
}
if (prevChapter != null) {
chapterUpdates += ChapterUpdate(
id = chapter.id,
bookmark = prevChapter.bookmark,
read = prevChapter.read,
dateFetch = prevChapter.dateFetch,
)
prevHistoryList.find { it.chapterId == prevChapter.id }?.let { prevHistory ->
historyUpdates += HistoryUpdate(
chapter.id,
prevHistory.readAt ?: return@let,
prevHistory.readDuration,
)
}
} else if (maxChapterRead != null && chapter.chapterNumber <= maxChapterRead) {
chapterUpdates += ChapterUpdate(
id = chapter.id,
read = true,
)
}
}
}
updateChapter.awaitAll(chapterUpdates)
upsertHistory.awaitAll(historyUpdates)
}
// Update categories
if (MigrationFlags.hasCategories(flags)) {
val categories = getCategories.await(prevManga.id)
setMangaCategories.await(manga.id, categories.map { it.id })
}
// Update track
if (MigrationFlags.hasTracks(flags)) {
val tracks = getTracks.await(prevManga.id)
if (tracks.isNotEmpty()) {
getTracks.await(manga.id).forEach {
deleteTrack.await(manga.id, it.trackerId)
}
insertTrack.awaitAll(tracks.map { it.copy(mangaId = manga.id) })
}
}
// Update custom cover
if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) {
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga.id).inputStream())
}
var mangaUpdate = MangaUpdate(manga.id, favorite = true, dateAdded = System.currentTimeMillis())
var prevMangaUpdate: MangaUpdate? = null
// Update extras
if (MigrationFlags.hasExtra(flags)) {
mangaUpdate = mangaUpdate.copy(
chapterFlags = prevManga.chapterFlags,
viewerFlags = prevManga.viewerFlags,
)
}
// Delete downloaded
if (MigrationFlags.hasDeleteChapters(flags)) {
val oldSource = sourceManager.get(prevManga.source)
if (oldSource != null) {
downloadManager.deleteManga(prevManga, oldSource)
}
}
// Update favorite status
if (replace) {
prevMangaUpdate = MangaUpdate(
id = prevManga.id,
favorite = false,
dateAdded = 0,
)
mangaUpdate = mangaUpdate.copy(
dateAdded = prevManga.dateAdded,
)
}
updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate))
}
fun useMangaForMigration(context: Context, newMangaId: Long, selectedMangaId: Long) {
val migratingManga = migratingItems.value.orEmpty().find { it.manga.id == selectedMangaId }
?: return
migratingManga.searchResult.value = SearchResult.Searching
screenModelScope.launchIO {
val result = migratingManga.migrationScope.async {
val manga = getManga(newMangaId)!!
val localManga = networkToLocalManga(manga)
try {
val source = sourceManager.get(manga.source)!!
val chapters = source.getChapterList(localManga.toSManga())
syncChaptersWithSource.await(chapters, localManga, source)
} catch (_: Exception) {
return@async null
}
localManga
}.await()
if (result != null) {
try {
val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga())
updateManga.awaitUpdateFromSource(result, newManga, true)
} catch (e: CancellationException) {
// Ignore cancellations
throw e
} catch (_: Exception) {
}
migratingManga.searchResult.value = SearchResult.Result(result.id)
} else {
migratingManga.searchResult.value = SearchResult.NotFound
withUIContext {
context.toast(SYMR.strings.no_chapters_found_for_migration, Toast.LENGTH_LONG)
}
}
}
}
fun migrateMangas() {
migrateMangas(true)
}
fun copyMangas() {
migrateMangas(false)
}
private fun migrateMangas(replace: Boolean) {
dialog.value = null
migrateJob = screenModelScope.launchIO {
migratingProgress.value = 0f
val items = migratingItems.value.orEmpty()
try {
items.forEachIndexed { index, manga ->
try {
ensureActive()
val toMangaObj = manga.searchResult.value.let {
if (it is SearchResult.Result) {
getManga.await(it.id)
} else {
null
}
}
if (toMangaObj != null) {
migrateMangaInternal(
manga.manga,
toMangaObj,
replace,
)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
logcat(LogPriority.WARN, throwable = e)
}
migratingProgress.value = index.toFloat() / items.size
}
navigateOut()
} finally {
migratingProgress.value = Float.MAX_VALUE
migrateJob = null
}
}
}
fun cancelMigrate() {
migrateJob?.cancel()
migrateJob = null
}
private suspend fun navigateOut() {
navigateOut.emit(Unit)
}
fun migrateManga(mangaId: Long, copy: Boolean) {
manualMigrations.value++
screenModelScope.launchIO {
val manga = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
?: return@launchIO
val toMangaObj = getManga.await((manga.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO)
?: return@launchIO
migrateMangaInternal(
manga.manga,
toMangaObj,
!copy,
)
removeManga(mangaId)
}
}
fun removeManga(mangaId: Long) {
screenModelScope.launchIO {
val item = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
?: return@launchIO
removeManga(item)
item.migrationScope.cancel()
sourceFinished()
}
}
fun removeManga(item: MigratingManga) {
when (val migration = config.migration) {
is MigrationType.MangaList -> {
val ids = migration.mangaIds.toMutableList()
val index = ids.indexOf(item.manga.id)
if (index > -1) {
ids.removeAt(index)
config.migration = MigrationType.MangaList(ids)
val index2 = migratingItems.value.orEmpty().indexOf(item)
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
}
}
is MigrationType.MangaSingle -> Unit
}
}
override fun onDispose() {
super.onDispose()
migratingItems.value.orEmpty().forEach {
it.migrationScope.cancel()
}
}
fun openMigrateDialog(
copy: Boolean,
) {
dialog.value = Dialog.MigrateMangaDialog(
copy,
migratingItems.value.orEmpty().size,
mangasSkipped(),
)
}
sealed class Dialog {
data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog()
object MigrationExitDialog : Dialog()
}
}
@@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
import java.io.Serializable
data class MigrationProcedureConfig(
var migration: MigrationType,
val extraSearchParams: String?,
) : Serializable
@@ -2,16 +2,13 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -10,14 +10,13 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreen
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.DelicateCoroutinesApi
import mihon.feature.migration.config.MigrationConfigScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.manga.interactor.GetFavorites
@@ -62,10 +61,10 @@ fun Screen.migrateSourceTab(): TabContent {
val sourceMangas =
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
withUIContext {
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
sourceMangas,
navigator.push(
MigrationConfigScreen(
sourceMangas
)
)
}
}
@@ -216,11 +216,11 @@ class HistoryScreenModel(
}
}
/*SY -->fun showMigrateDialog(target: Manga, current: Manga) {
fun showMigrateDialog(target: Manga, current: Manga) {
mutableState.update { currentState ->
currentState.copy(dialog = Dialog.Migrate(target = target, current = current))
}
} SY <--*/
}
fun showChangeCategoryDialog(manga: Manga) {
screenModelScope.launch {
@@ -252,7 +252,7 @@ class HistoryScreenModel(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
/* SY --> data class Migrate(val target: Manga, val current: Manga) : Dialog SY <-- */
data class Migrate(val target: Manga, val current: Manga) : Dialog
}
sealed interface Event {
@@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import mihon.feature.migration.dialog.MigrateMangaDialog
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.i18n.MR
@@ -129,7 +130,7 @@ data object HistoryTab : Tab {
},
)
}
/*SY -->is HistoryScreenModel.Dialog.Migrate -> {
is HistoryScreenModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = dialog.current,
target = dialog.target,
@@ -137,7 +138,7 @@ data object HistoryTab : Tab {
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismissRequest,
)
} SY <--*/
}
null -> {}
}
@@ -23,6 +23,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
@@ -764,6 +764,7 @@ class LibraryScreenModel(
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
mergedManga.ogTitle,
mergedManga.source,
)
@@ -51,8 +51,8 @@ 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 com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.analytics.analytics
import com.google.firebase.Firebase
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.presentation.components.AppStateBanners
@@ -52,9 +52,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast
import exh.metadata.metadata.RaisedSearchMetadata
import exh.source.getMainSource
import exh.source.isEhBasedManga
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper
import exh.log.xLogD
@@ -1656,9 +1653,7 @@ class MangaScreenModel(
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
/* SY -->
data class Migrate(val target: Manga, val current: Manga) : Dialog
SY <-- */
data class SetFetchInterval(val manga: Manga) : Dialog
// SY -->
@@ -1691,11 +1686,10 @@ class MangaScreenModel(
updateSuccessState { it.copy(dialog = Dialog.FullCover) }
}
/* SY -->
fun showMigrateDialog(duplicate: Manga) {
val manga = successState?.manga ?: return
updateSuccessState { it.copy(dialog = Dialog.Migrate(target = manga, current = duplicate)) }
} SY <-- */
}
fun setExcludedScanlators(excludedScanlators: Set<String>) {
screenModelScope.launchIO {
@@ -48,6 +48,7 @@ import eu.kanade.tachiyomi.util.chapter.filterDownloaded
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.MAX_FILE_NAME_BYTES
import eu.kanade.tachiyomi.util.storage.cacheImageDir
@@ -111,6 +111,7 @@ class ChapterLoader(
val isMergedMangaDownloaded = downloadManager.isChapterDownloaded(
chapterName = chapter.chapter.name,
chapterScanlator = chapter.chapter.scanlator,
chapterUrl = chapter.chapter.url,
mangaTitle = manga.ogTitle,
sourceId = manga.source,
skipCache = true,
@@ -1,8 +1,8 @@
package exh.log
import com.elvishew.xlog.printer.Printer
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.Firebase
import eu.kanade.tachiyomi.BuildConfig
class CrashlyticsPrinter(private val logLevel: Int) : Printer {
@@ -13,26 +13,23 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import exh.ui.ifSourcesLoaded
import mihon.feature.migration.dialog.MigrateMangaDialog
import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
@@ -100,21 +97,22 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = dialog.current,
target = dialog.target,
// Initiated from the context of [dialog.target] so we show [dialog.current].
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismissRequest,
)
}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = {
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
it.id,
dialog.manga.id,
)
},
onMigrate = { screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, it)) },
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {