diff --git a/app/src/main/java/eu/kanade/data/source/SourceMapper.kt b/app/src/main/java/eu/kanade/data/source/SourceMapper.kt index a54022cd6..ed4fd7f50 100644 --- a/app/src/main/java/eu/kanade/data/source/SourceMapper.kt +++ b/app/src/main/java/eu/kanade/data/source/SourceMapper.kt @@ -3,11 +3,15 @@ package eu.kanade.data.source import eu.kanade.domain.source.model.Source import eu.kanade.tachiyomi.source.CatalogueSource -val sourceMapper: (CatalogueSource) -> Source = { source -> +val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source -> Source( source.id, source.lang, source.name, - source.supportsLatest + false ) } + +val catalogueSourceMapper: (CatalogueSource) -> Source = { source -> + sourceMapper(source).copy(supportsLatest = source.supportsLatest) +} diff --git a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt index b31f24f88..7fcc2921d 100644 --- a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt @@ -1,18 +1,36 @@ package eu.kanade.data.source +import eu.kanade.data.DatabaseHandler import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.repository.SourceRepository +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager +import exh.source.MERGED_SOURCE_ID import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class SourceRepositoryImpl( - private val sourceManager: SourceManager + private val sourceManager: SourceManager, + private val handler: DatabaseHandler ) : SourceRepository { override fun getSources(): Flow> { return sourceManager.catalogueSources.map { sources -> - sources.map(sourceMapper) + sources.map(catalogueSourceMapper) + } + } + + override fun getSourcesWithFavoriteCount(): Flow>> { + val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() } + return sourceIdWithFavoriteCount.map { sourceIdsWithCount -> + sourceIdsWithCount + .map { (sourceId, count) -> + val source = sourceManager.getOrStub(sourceId).run { + sourceMapper(this) + } + source to count + } + .filterNot { it.first.id == LocalSource.ID /* SY --> */ || it.first.id == MERGED_SOURCE_ID /* SY <-- */ } } } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index ab4e33208..a6c3957f4 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -12,6 +12,8 @@ import eu.kanade.domain.source.interactor.DisableSource import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetShowLatest import eu.kanade.domain.source.interactor.GetSourceCategories +import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount +import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetSourceCategories import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver import eu.kanade.domain.source.interactor.ToggleSourcePin @@ -33,10 +35,12 @@ class DomainModule : InjektModule { addFactory { RemoveHistoryById(get()) } addFactory { RemoveHistoryByMangaId(get()) } - addSingletonFactory { SourceRepositoryImpl(get()) } + addSingletonFactory { SourceRepositoryImpl(get(), get()) } addFactory { GetEnabledSources(get(), get()) } addFactory { DisableSource(get()) } addFactory { ToggleSourcePin(get()) } + addFactory { GetSourcesWithFavoriteCount(get(), get()) } + addFactory { SetMigrateSorting(get()) } // SY --> addFactory { GetSourceCategories(get()) } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt new file mode 100644 index 000000000..37b791c25 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt @@ -0,0 +1,58 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.domain.source.repository.SourceRepository +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import java.text.Collator +import java.util.* +import kotlin.Comparator + +class GetSourcesWithFavoriteCount( + private val repository: SourceRepository, + private val preferences: PreferencesHelper +) { + + fun subscribe(): Flow>> { + return combine( + preferences.migrationSortingDirection().asFlow(), + preferences.migrationSortingMode().asFlow(), + repository.getSourcesWithFavoriteCount() + ) { direction, mode, list -> + list.sortedWith(sortFn(direction, mode)) + } + } + + private fun sortFn( + direction: SetMigrateSorting.Direction, + sorting: SetMigrateSorting.Mode + ): java.util.Comparator> { + val locale = Locale.getDefault() + val collator = Collator.getInstance(locale).apply { + strength = Collator.PRIMARY + } + val sortFn: (Pair, Pair) -> Int = { a, b -> + val id1 = a.first.name.toLongOrNull() + val id2 = b.first.name.toLongOrNull() + when (sorting) { + SetMigrateSorting.Mode.ALPHABETICAL -> { + collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale)) + } + SetMigrateSorting.Mode.TOTAL -> { + when { + id1 != null && id2 != null -> a.second.compareTo(b.second) + id1 != null && id2 == null -> -1 + id2 != null && id1 == null -> 1 + else -> a.second.compareTo(b.second) + } + } + } + } + + return when (direction) { + SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn) + SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn) + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt b/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt new file mode 100644 index 000000000..8728a9a54 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt @@ -0,0 +1,24 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper + +class SetMigrateSorting( + private val preferences: PreferencesHelper +) { + + fun await(mode: Mode, isAscending: Boolean) { + val direction = if (isAscending) Direction.ASCENDING else Direction.DESCENDING + preferences.migrationSortingDirection().set(direction) + preferences.migrationSortingMode().set(mode) + } + + enum class Mode { + ALPHABETICAL, + TOTAL; + } + + enum class Direction { + ASCENDING, + DESCENDING; + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt index dc139e93e..b58509de9 100644 --- a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt +++ b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt @@ -6,4 +6,6 @@ import kotlinx.coroutines.flow.Flow interface SourceRepository { fun getSources(): Flow> + + fun getSourcesWithFavoriteCount(): Flow>> } diff --git a/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt b/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt new file mode 100644 index 000000000..6097ad471 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt @@ -0,0 +1,16 @@ +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LoadingScreen() { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator(modifier = Modifier.size(64.dp)) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt new file mode 100644 index 000000000..0bbca9fad --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt @@ -0,0 +1,132 @@ +package eu.kanade.presentation.source + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.LoadingScreen +import eu.kanade.presentation.source.components.BaseSourceItem +import eu.kanade.presentation.theme.header +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter + +@Composable +fun MigrateSourceScreen( + nestedScrollInterop: NestedScrollConnection, + presenter: MigrationSourcesPresenter, + onClickItem: (Source) -> Unit, + onLongClickItem: (Source) -> Unit, + onClickAll: (Source) -> Unit, +) { + val state by presenter.state.collectAsState() + when { + state.isLoading -> LoadingScreen() + state.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library) + else -> { + MigrateSourceList( + nestedScrollInterop = nestedScrollInterop, + list = state.sources!!, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, + onClickAll = onClickAll + ) + } + } +} + +@Composable +fun MigrateSourceList( + nestedScrollInterop: NestedScrollConnection, + list: List>, + onClickItem: (Source) -> Unit, + onLongClickItem: (Source) -> Unit, + onClickAll: (Source) -> Unit, +) { + LazyColumn( + modifier = Modifier.nestedScroll(nestedScrollInterop), + contentPadding = WindowInsets.navigationBars.asPaddingValues(), + ) { + item(key = "title") { + Text( + text = stringResource(id = R.string.migration_selection_prompt), + modifier = Modifier + .animateItemPlacement() + .padding(horizontal = horizontalPadding, vertical = 8.dp), + style = MaterialTheme.typography.header + ) + } + + items( + items = list, + key = { (source, _) -> + source.id + } + ) { (source, count) -> + MigrateSourceItem( + modifier = Modifier.animateItemPlacement(), + source = source, + count = count, + onClickItem = { onClickItem(source) }, + onLongClickItem = { onLongClickItem(source) }, + onClickAll = { onClickAll(source) } + ) + } + } +} + +@Composable +fun MigrateSourceItem( + modifier: Modifier = Modifier, + source: Source, + count: Long, + onClickItem: () -> Unit, + onLongClickItem: () -> Unit, + onClickAll: () -> Unit, +) { + BaseSourceItem( + modifier = modifier, + source = source, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, + action = { + Text( + text = "$count", + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.primary) + .padding(horizontal = 8.dp, vertical = 2.dp), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onPrimary + ) + ) + TextButton(onClick = onClickAll) { + Text( + text = stringResource(id = R.string.all), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.primary + ) + ) + } + } + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt b/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt index 76a807905..c0529369c 100644 --- a/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt @@ -2,9 +2,7 @@ package eu.kanade.presentation.source import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio @@ -19,7 +17,6 @@ import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -32,19 +29,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Source import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.PreferenceRow +import eu.kanade.presentation.source.components.BaseSourceItem import eu.kanade.presentation.theme.header import eu.kanade.presentation.util.horizontalPadding import eu.kanade.tachiyomi.R @@ -67,7 +64,7 @@ fun SourceScreen( val state by presenter.state.collectAsState() when { - state.isLoading -> CircularProgressIndicator() + state.isLoading -> LoadingScreen() state.hasError -> Text(text = state.error!!.message!!) state.isEmpty -> EmptyScreen(message = "") else -> SourceList( @@ -134,7 +131,7 @@ fun SourceList( } is UiModel.Item -> SourceItem( modifier = Modifier.animateItemPlacement(), - item = model.source, + source = model.source, showLatest = showLatest, showPin = showPin, onClickItem = onClickItem, @@ -204,7 +201,7 @@ fun SourceHeader( @Composable fun SourceItem( modifier: Modifier = Modifier, - item: Source, + source: Source, // SY --> showLatest: Boolean, showPin: Boolean, @@ -214,54 +211,32 @@ fun SourceItem( onClickLatest: (Source) -> Unit, onClickPin: (Source) -> Unit ) { - Row( - modifier = modifier - .combinedClickable( - onClick = { onClickItem(item) }, - onLongClick = { onLongClickItem(item) } - ) - .padding(horizontal = horizontalPadding, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SourceIcon(source = item) - Column( - modifier = Modifier - .padding(horizontal = horizontalPadding) - .weight(1f) - ) { - Text( - text = item.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = LocaleHelper.getDisplayName(item.lang), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodySmall - ) - } - if (item.supportsLatest /* SY --> */ && showLatest /* SY <-- */) { - TextButton(onClick = { onClickLatest(item) }) { - Text( - text = stringResource(id = R.string.latest), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.primary - ), + BaseSourceItem( + modifier = modifier, + source = source, + onClickItem = { onClickItem(source) }, + onLongClickItem = { onLongClickItem(source) }, + action = { source -> + if (source.supportsLatest /* SY --> */ && showLatest /* SY <-- */) { + TextButton(onClick = { onClickLatest(source) }) { + Text( + text = stringResource(id = R.string.latest), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.primary + ) + ) + } + } + // SY --> + if (showPin) { + SourcePinButton( + isPinned = Pin.Pinned in source.pin, + onClick = { onClickPin(source) } ) } - } - - // SY --> - if (showPin) { - SourcePinButton( - isPinned = Pin.Pinned in item.pin, - onClick = { onClickPin(item) }, - ) - } - // SY <-- - } + // SY <-- + }, + ) } @Composable diff --git a/app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt b/app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt new file mode 100644 index 000000000..87681db7e --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt @@ -0,0 +1,68 @@ +package eu.kanade.presentation.source.components + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.source.SourceIcon +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.tachiyomi.util.system.LocaleHelper + +@Composable +fun BaseSourceItem( + modifier: Modifier = Modifier, + source: Source, + onClickItem: () -> Unit = {}, + onLongClickItem: () -> Unit = {}, + icon: @Composable RowScope.(Source) -> Unit = defaultIcon, + action: @Composable RowScope.(Source) -> Unit = {}, + content: @Composable RowScope.(Source) -> Unit = defaultContent, +) { + Row( + modifier = modifier + .combinedClickable( + onClick = onClickItem, + onLongClick = onLongClickItem + ) + .padding(horizontal = horizontalPadding, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + icon.invoke(this, source) + content.invoke(this, source) + action.invoke(this, source) + } +} + +private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source -> + SourceIcon(source = source) +} + +private val defaultContent: @Composable RowScope.(Source) -> Unit = { source -> + Column( + modifier = Modifier + .padding(horizontal = horizontalPadding) + .weight(1f) + ) { + Text( + text = source.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = LocaleHelper.getDisplayName(source.lang), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 8b62856cd..76b6563f8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -7,11 +7,11 @@ import androidx.core.content.edit import androidx.core.net.toUri import androidx.preference.PreferenceManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.anilist.Anilist -import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController import eu.kanade.tachiyomi.ui.library.LibraryGroup import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting @@ -265,8 +265,8 @@ class PreferencesHelper(val context: Context) { fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL) fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING) - fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL) - fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING) + fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, SetMigrateSorting.Mode.ALPHABETICAL) + fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, SetMigrateSorting.Direction.ASCENDING) fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt index adf44a3e6..e37bc3795 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt @@ -1,171 +1,105 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter +import androidx.compose.runtime.Composable +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import eu.kanade.presentation.source.MigrateSourceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.ComposeController import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController -import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser -import exh.util.executeOnIO import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -class MigrationSourcesController : - NucleusController(), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - // SY --> - SourceAdapter.OnAllClickListener { - // SY <-- - - private val preferences: PreferencesHelper by injectLazy() - - private var adapter: SourceAdapter? = null +class MigrationSourcesController : ComposeController() { init { setHasOptionsMenu(true) } - override fun createPresenter(): MigrationSourcesPresenter { - return MigrationSourcesPresenter() + override fun createPresenter(): MigrationSourcesPresenter = + MigrationSourcesPresenter() + + @Composable + override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { + MigrateSourceScreen( + nestedScrollInterop = nestedScrollInterop, + presenter = presenter, + onClickItem = { source -> + val parentController = parentController + if (parentController is BrowseController) { + parentController.router + } else { + router + }.pushController( + MigrationMangaController( + source.id, + source.name + ) + ) + }, + onLongClickItem = { source -> + val sourceId = source.id.toString() + activity?.copyToClipboard(sourceId, sourceId) + }, + onClickAll = { source -> + // TODO: Jay wtf, need to clean this up sometime + launchIO { + val manga = Injekt.get().getFavoriteMangas().executeAsBlocking() + val sourceMangas = + manga.asSequence().filter { it.source == source.id }.mapNotNull { it.id } + .toList() + withUIContext { + PreMigrationController.navigateToMigration( + Injekt.get().skipPreMigration().get(), + run { + val parentController = parentController + if (parentController is BrowseController) { + parentController.router + } else { + router + } + }, + sourceMangas, + ) + } + } + }, + ) } - override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater) - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = SourceAdapter(this) - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.adapter = adapter - adapter?.fastScroller = binding.fastScroller - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = inflater.inflate(R.menu.browse_migrate, menu) - } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (val itemId = item.itemId) { - R.id.action_source_migration_help -> activity?.openInBrowser(HELP_URL) - R.id.asc_alphabetical, R.id.desc_alphabetical -> { - setSortingDirection(SortSetting.ALPHABETICAL, itemId == R.id.asc_alphabetical) + return when (val itemId = item.itemId) { + R.id.action_source_migration_help -> { + activity?.openInBrowser(HELP_URL) + true } - R.id.asc_count, R.id.desc_count -> { - setSortingDirection(SortSetting.TOTAL, itemId == R.id.asc_count) + R.id.asc_alphabetical, + R.id.desc_alphabetical -> { + presenter.setAlphabeticalSorting(itemId == R.id.asc_alphabetical) + true } - } - return super.onOptionsItemSelected(item) - } - - private fun setSortingDirection(sortSetting: SortSetting, isAscending: Boolean) { - val direction = if (isAscending) { - DirectionSetting.ASCENDING - } else { - DirectionSetting.DESCENDING - } - - preferences.migrationSortingDirection().set(direction) - preferences.migrationSortingMode().set(sortSetting) - - presenter.requestSortUpdate() - } - - fun setSources(sourcesWithManga: List) { - // Show empty view if needed - if (sourcesWithManga.isNotEmpty()) { - binding.emptyView.hide() - } else { - binding.emptyView.show(R.string.information_empty_library) - } - - adapter?.updateDataSet(sourcesWithManga) - } - - // SY --> - override fun getTitle(): String? { - return resources?.getString(R.string.source_migration) - } - - override fun onItemClick(view: View?, position: Int): Boolean { - val item = adapter?.getItem(position) as? SourceItem ?: return false - val controller = MigrationMangaController(item.source.id, item.source.name) - val parentController = parentController - if (parentController is BrowseController) { - parentController.router.pushController(controller) - } else { - router.pushController(controller) - } - - return false - } - - override fun onItemLongClick(position: Int) { - val item = adapter?.getItem(position) as? SourceItem ?: return - val sourceId = item.source.id.toString() - activity?.copyToClipboard(sourceId, sourceId) - } - - override fun onAllClick(position: Int) { - val item = adapter?.getItem(position) as? SourceItem ?: return - - launchUI { - val manga = Injekt.get().getFavoriteMangas().executeOnIO() - val sourceMangas = manga.asSequence().filter { it.source == item.source.id }.mapNotNull { it.id }.toList() - withUIContext { - PreMigrationController.navigateToMigration( - Injekt.get().skipPreMigration().get(), - run { - val parentController = parentController - if (parentController is BrowseController) { - parentController.router - } else { - router - } - }, - sourceMangas, - ) + R.id.asc_count, + R.id.desc_count -> { + presenter.setTotalSorting(itemId == R.id.asc_count) + true } + else -> super.onOptionsItemSelected(item) } } - // SY <-- - - enum class DirectionSetting { - ASCENDING, - DESCENDING; - } - - enum class SortSetting { - ALPHABETICAL, - TOTAL; - } } private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt index 6aacc8007..1d4ca46c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt @@ -1,83 +1,60 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount +import eu.kanade.domain.source.interactor.SetMigrateSorting +import eu.kanade.domain.source.model.Source import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.combineLatest -import exh.source.MERGED_SOURCE_ID -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import java.text.Collator -import java.util.Collections -import java.util.Locale class MigrationSourcesPresenter( - private val sourceManager: SourceManager = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), + private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), + private val setMigrateSorting: SetMigrateSorting = Injekt.get() ) : BasePresenter() { - private val preferences: PreferencesHelper by injectLazy() - - private val sortRelay = BehaviorRelay.create(Unit) + private val _state: MutableStateFlow = MutableStateFlow(MigrateSourceState.EMPTY) + val state: StateFlow = _state.asStateFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - db.getFavoriteMangas() - .asRxObservable() - .combineLatest(sortRelay.observeOn(Schedulers.io())) { sources, _ -> sources } - .observeOn(AndroidSchedulers.mainThread()) - .map { findSourcesWithManga(it) } - .subscribeLatestCache(MigrationSourcesController::setSources) + presenterScope.launchIO { + getSourcesWithFavoriteCount.subscribe() + .collectLatest { sources -> + _state.update { state -> + state.copy(sources = sources) + } + } + } } - fun requestSortUpdate() { - sortRelay.call(Unit) + fun setAlphabeticalSorting(isAscending: Boolean) { + setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending) } - private fun findSourcesWithManga(library: List): List { - val header = SelectionHeader() - return library - .groupBy { it.source } - .filterKeys { it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */ } - .map { - val source = sourceManager.getOrStub(it.key) - SourceItem(source, it.value.size, header) - } - .sortedWith(sortFn()) - .toList() - } - - private fun sortFn(): java.util.Comparator { - val sort by lazy { - preferences.migrationSortingMode().get() - } - val direction by lazy { - preferences.migrationSortingDirection().get() - } - - val locale = Locale.getDefault() - val collator = Collator.getInstance(locale).apply { - strength = Collator.PRIMARY - } - val sortFn: (SourceItem, SourceItem) -> Int = { a, b -> - when (sort) { - MigrationSourcesController.SortSetting.ALPHABETICAL -> collator.compare(a.source.name.lowercase(locale), b.source.name.lowercase(locale)) - MigrationSourcesController.SortSetting.TOTAL -> a.mangaCount.compareTo(b.mangaCount) - } - } - - return when (direction) { - MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn) - MigrationSourcesController.DirectionSetting.DESCENDING -> Collections.reverseOrder(sortFn) - } + fun setTotalSorting(isAscending: Boolean) { + setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending) + } +} + +data class MigrateSourceState( + val sources: List>? +) { + + val isLoading: Boolean + get() = sources == null + + val isEmpty: Boolean + get() = sources.isNullOrEmpty() + + companion object { + val EMPTY = MigrateSourceState(null) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt deleted file mode 100644 index c2d4dcb57..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt +++ /dev/null @@ -1,63 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.sources - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractHeaderItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding - -/** - * Item that contains the selection header. - */ -class SelectionHeader : AbstractHeaderItem() { - - /** - * Returns the layout resource of this item. - */ - override fun getLayoutRes(): Int { - return R.layout.section_header_item - } - - /** - * Creates a new view holder for this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder( - view, - adapter, - ) - } - - /** - * Binds this item to the given view holder. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: Holder, - position: Int, - // SY --> - payloads: MutableList?, - // SY <-- - ) { - // Intentionally empty - } - - class Holder(view: View, adapter: FlexibleAdapter */ IFlexible /* SY <-- */>) : FlexibleViewHolder(view, adapter) { - private val binding = SectionHeaderItemBinding.bind(view) - - init { - binding.title.text = view.context.getString(/* SY --> */ R.string.select_a_source_to_migrate_from /* SY <-- */) - } - } - - override fun equals(other: Any?): Boolean { - return other is SelectionHeader - } - - override fun hashCode(): Int { - return 0 - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceAdapter.kt deleted file mode 100644 index 139df67f3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceAdapter.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.sources - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible - -/** - * Adapter that holds the catalogue cards. - * - * @param controller instance of [MigrationSourcesController]. - */ -class SourceAdapter(controller: MigrationSourcesController) : - FlexibleAdapter>(null, controller, true) { - - init { - setDisplayHeadersAtStartUp(true) - } - - // SY --> - /** - * Listener for auto item clicks. - */ - val allClickListener: OnAllClickListener? = controller - - /** - * Listener which should be called when user clicks select. - */ - interface OnAllClickListener { - fun onAllClick(position: Int) - } - // SY <-- -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt deleted file mode 100644 index c22a02d6a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.sources - -import android.view.View -import androidx.core.view.isVisible -import coil.load -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.SourceMainControllerItemBinding -import eu.kanade.tachiyomi.source.icon -import eu.kanade.tachiyomi.util.system.LocaleHelper - -class SourceHolder(view: View, val adapter: SourceAdapter) : - FlexibleViewHolder(view, adapter) { - - private val binding = SourceMainControllerItemBinding.bind(view) - - // SY --> - init { - binding.sourceLatest.isVisible = true - binding.sourceLatest.text = view.context.getString(R.string.all) - binding.sourceLatest.setOnClickListener { - adapter.allClickListener?.onAllClick(bindingAdapterPosition) - } - } - // SY <-- - - fun bind(item: SourceItem) { - val source = item.source - - binding.title.text = "${source.name} (${item.mangaCount})" - binding.subtitle.isVisible = source.lang != "" - binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) - - itemView.post { - binding.image.load(source.icon()) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceItem.kt deleted file mode 100644 index b2bde56c7..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceItem.kt +++ /dev/null @@ -1,48 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.sources - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.Source - -/** - * Item that contains source information. - * - * @param source Instance of [Source] containing source information. - * @param header The header for this item. - */ -data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) : - AbstractSectionableItem(header) { - - /** - * Returns the layout resource of this item. - */ - override fun getLayoutRes(): Int { - return R.layout.source_main_controller_item - } - - /** - * Creates a new view holder for this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): SourceHolder { - return SourceHolder( - view, - adapter as SourceAdapter, - ) - } - - /** - * Binds this item to the given view holder. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: SourceHolder, - position: Int, - payloads: List?, - ) { - holder.bind(this) - } -} diff --git a/app/src/main/res/layout/migration_sources_controller.xml b/app/src/main/res/layout/migration_sources_controller.xml deleted file mode 100644 index 94d3cc4e2..000000000 --- a/app/src/main/res/layout/migration_sources_controller.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/source_main_controller_item.xml b/app/src/main/res/layout/source_main_controller_item.xml deleted file mode 100644 index f3b94ae0c..000000000 --- a/app/src/main/res/layout/source_main_controller_item.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - -