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..52d826fc6 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,35 @@ 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 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 } } } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 2f6da5f7f..2d5c5427a 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -10,6 +10,8 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.source.interactor.DisableSource import eu.kanade.domain.source.interactor.GetEnabledSources +import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount +import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.repository.SourceRepository import uy.kohesive.injekt.api.InjektModule @@ -29,9 +31,11 @@ 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()) } } } 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..454386553 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt @@ -0,0 +1,117 @@ +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.MaterialTheme +import androidx.compose.material3.Text +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, +) { + 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, + ) + } + } +} + +@Composable +fun MigrateSourceList( + nestedScrollInterop: NestedScrollConnection, + list: List>, + onClickItem: (Source) -> Unit, + onLongClickItem: (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) } + ) + } + } +} + +@Composable +fun MigrateSourceItem( + modifier: Modifier = Modifier, + source: Source, + count: Long, + onClickItem: () -> Unit, + onLongClickItem: () -> 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 + ) + ) + } + ) +} 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 b1441967d..9a8274986 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 @@ -18,7 +16,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -30,18 +27,18 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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.source.components.BaseSourceItem import eu.kanade.presentation.theme.header import eu.kanade.presentation.util.horizontalPadding import eu.kanade.tachiyomi.R @@ -62,7 +59,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( @@ -115,7 +112,7 @@ fun SourceList( } is UiModel.Item -> SourceItem( modifier = Modifier.animateItemPlacement(), - item = model.source, + source = model.source, onClickItem = onClickItem, onLongClickItem = { setSourceState(it) @@ -160,55 +157,34 @@ fun SourceHeader( @Composable fun SourceItem( modifier: Modifier = Modifier, - item: Source, + source: Source, onClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit, 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) { - 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) { + TextButton(onClick = { onClickLatest(source) }) { + Text( + text = stringResource(id = R.string.latest), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.primary + ) + ) + } } - } - SourcePinButton( - isPinned = Pin.Pinned in item.pin, - onClick = { onClickPin(item) } - ) - } + SourcePinButton( + isPinned = Pin.Pinned in source.pin, + onClick = { onClickPin(source) } + ) + }, + ) } @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 00185224f..082c7128a 100644 --- 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.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting @@ -254,8 +254,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 1844836b6..c793c7c75 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,124 +1,68 @@ 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.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.migration.manga.MigrationMangaController import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser -import uy.kohesive.injekt.injectLazy -class MigrationSourcesController : - NucleusController(), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener { - - 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() - override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater) - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() + @Composable + override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { + MigrateSourceScreen( + nestedScrollInterop = nestedScrollInterop, + presenter = presenter, + onClickItem = { source -> + parentController!!.router.pushController( + MigrationMangaController( + source.id, + source.name + ) + ) + }, + onLongClickItem = { source -> + val sourceId = source.id.toString() + activity?.copyToClipboard(sourceId, sourceId) } - } - - 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 } + R.id.asc_count, + R.id.desc_count -> { + presenter.setTotalSorting(itemId == R.id.asc_count) + true + } + else -> super.onOptionsItemSelected(item) } - 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) - } - - 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) - parentController!!.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) - } - - enum class DirectionSetting { - ASCENDING, - DESCENDING; - } - - enum class SortSetting { - ALPHABETICAL, - TOTAL; } } 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 814cbfbfd..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,82 +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 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 } - .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 43abb3448..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt +++ /dev/null @@ -1,62 +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, - payloads: List?, - ) { - // Intentionally empty - } - - class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { - - private val binding = SectionHeaderItemBinding.bind(view) - - init { - binding.title.text = view.context.getString(R.string.migration_selection_prompt) - } - } - - 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 ca6dd6307..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.sources - -import com.bluelinelabs.conductor.Controller -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible - -/** - * Adapter that holds the catalogue cards. - * - * @param controller instance of [MigrationController]. - */ -class SourceAdapter(controller: Controller) : - FlexibleAdapter>(null, controller, true) { - - init { - setDisplayHeadersAtStartUp(true) - } -} 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 ed5730bb1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt +++ /dev/null @@ -1,27 +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.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) - - 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 3580d090c..000000000 --- a/app/src/main/res/layout/source_main_controller_item.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index 30b462be6..4010faa48 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -28,4 +28,12 @@ CREATE INDEX mangas_url_index ON mangas(url); getMangaById: SELECT * FROM mangas -WHERE _id = :id; \ No newline at end of file +WHERE _id = :id; + +getSourceIdWithFavoriteCount: +SELECT +source, +count(*) +FROM mangas +WHERE favorite = 1 +GROUP BY source; \ No newline at end of file