Browse Source

Use Compose in Migrate tab (#7008)

* Use Compose in Migrate tab

* Add missing header

* Remove unused files

* Fix build after rebase

* Changes from review comments
Andreas 2 years ago
parent
commit
7261fcccda
20 changed files with 429 additions and 453 deletions
  1. 6 2
      app/src/main/java/eu/kanade/data/source/SourceMapper.kt
  2. 19 2
      app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt
  3. 5 1
      app/src/main/java/eu/kanade/domain/DomainModule.kt
  4. 58 0
      app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt
  5. 24 0
      app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt
  6. 2 0
      app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt
  7. 16 0
      app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt
  8. 117 0
      app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt
  9. 26 50
      app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt
  10. 68 0
      app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt
  11. 3 3
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
  12. 40 96
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt
  13. 36 58
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt
  14. 0 62
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt
  15. 0 18
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceAdapter.kt
  16. 0 27
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt
  17. 0 48
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceItem.kt
  18. 0 31
      app/src/main/res/layout/migration_sources_controller.xml
  19. 0 54
      app/src/main/res/layout/source_main_controller_item.xml
  20. 9 1
      app/src/main/sqldelight/data/mangas.sq

+ 6 - 2
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)
+}

+ 19 - 2
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<List<Source>> {
         return sourceManager.catalogueSources.map { sources ->
-            sources.map(sourceMapper)
+            sources.map(catalogueSourceMapper)
+        }
+    }
+
+    override fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> {
+        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 }
         }
     }
 }

+ 5 - 1
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<SourceRepository> { SourceRepositoryImpl(get()) }
+        addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
         addFactory { GetEnabledSources(get(), get()) }
         addFactory { DisableSource(get()) }
         addFactory { ToggleSourcePin(get()) }
+        addFactory { GetSourcesWithFavoriteCount(get(), get()) }
+        addFactory { SetMigrateSorting(get()) }
     }
 }

+ 58 - 0
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<List<Pair<Source, Long>>> {
+        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<Pair<Source, Long>> {
+        val locale = Locale.getDefault()
+        val collator = Collator.getInstance(locale).apply {
+            strength = Collator.PRIMARY
+        }
+        val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> 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)
+        }
+    }
+}

+ 24 - 0
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;
+    }
+}

+ 2 - 0
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<List<Source>>
+
+    fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
 }

+ 16 - 0
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))
+    }
+}

+ 117 - 0
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<Pair<Source, Long>>,
+    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
+                )
+            )
+        }
+    )
+}

+ 26 - 50
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

+ 68 - 0
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
+        )
+    }
+}

+ 3 - 3
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)
 

+ 40 - 96
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<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(),
-    FlexibleAdapter.OnItemClickListener,
-    FlexibleAdapter.OnItemLongClickListener {
-
-    private val preferences: PreferencesHelper by injectLazy()
-
-    private var adapter: SourceAdapter? = null
+class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() {
 
     init {
         setHasOptionsMenu(true)
     }
 
-    override fun createPresenter(): MigrationSourcesPresenter {
-        return MigrationSourcesPresenter()
-    }
-
-    override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater)
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        binding.recycler.applyInsetter {
-            type(navigationBars = true) {
-                padding()
+    override fun createPresenter(): MigrationSourcesPresenter =
+        MigrationSourcesPresenter()
+
+    @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<SourceItem>) {
-        // 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;
     }
 }
 

+ 36 - 58
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<MigrationSourcesController>() {
 
-    private val preferences: PreferencesHelper by injectLazy()
-
-    private val sortRelay = BehaviorRelay.create(Unit)
+    private val _state: MutableStateFlow<MigrateSourceState> = MutableStateFlow(MigrateSourceState.EMPTY)
+    val state: StateFlow<MigrateSourceState> = _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<Manga>): List<SourceItem> {
-        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()
+    fun setTotalSorting(isAscending: Boolean) {
+        setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending)
     }
+}
 
-    private fun sortFn(): java.util.Comparator<SourceItem> {
-        val sort by lazy {
-            preferences.migrationSortingMode().get()
-        }
-        val direction by lazy {
-            preferences.migrationSortingDirection().get()
-        }
+data class MigrateSourceState(
+    val sources: List<Pair<Source, Long>>?
+) {
 
-        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)
-            }
-        }
+    val isLoading: Boolean
+        get() = sources == null
 
-        return when (direction) {
-            MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn)
-            MigrationSourcesController.DirectionSetting.DESCENDING -> Collections.reverseOrder(sortFn)
-        }
+    val isEmpty: Boolean
+        get() = sources.isNullOrEmpty()
+
+    companion object {
+        val EMPTY = MigrateSourceState(null)
     }
 }

+ 0 - 62
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt

@@ -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<SelectionHeader.Holder>() {
-
-    /**
-     * 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<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(
-            view,
-            adapter,
-        )
-    }
-
-    /**
-     * Binds this item to the given view holder.
-     */
-    override fun bindViewHolder(
-        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
-        holder: Holder,
-        position: Int,
-        payloads: List<Any?>?,
-    ) {
-        // 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
-    }
-}

+ 0 - 18
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceAdapter.kt

@@ -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<IFlexible<*>>(null, controller, true) {
-
-    init {
-        setDisplayHeadersAtStartUp(true)
-    }
-}

+ 0 - 27
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt

@@ -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())
-        }
-    }
-}

+ 0 - 48
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceItem.kt

@@ -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<SourceHolder, SelectionHeader>(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<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
-        return SourceHolder(
-            view,
-            adapter as SourceAdapter,
-        )
-    }
-
-    /**
-     * Binds this item to the given view holder.
-     */
-    override fun bindViewHolder(
-        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
-        holder: SourceHolder,
-        position: Int,
-        payloads: List<Any?>?,
-    ) {
-        holder.bind(this)
-    }
-}

+ 0 - 31
app/src/main/res/layout/migration_sources_controller.xml

@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/recycler"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clipToPadding="false"
-        android:paddingTop="8dp"
-        android:paddingBottom="@dimen/action_toolbar_list_padding" />
-
-    <eu.kanade.tachiyomi.widget.MaterialFastScroll
-        android:id="@+id/fast_scroller"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_gravity="end"
-        app:fastScrollerBubbleEnabled="false"
-        tools:visibility="visible" />
-
-    <eu.kanade.tachiyomi.widget.EmptyView
-        android:id="@+id/empty_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:visibility="gone" />
-
-</FrameLayout>

+ 0 - 54
app/src/main/res/layout/source_main_controller_item.xml

@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="64dp"
-    android:background="@drawable/list_item_selector_background">
-
-    <ImageView
-        android:id="@+id/image"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:paddingStart="16dp"
-        android:paddingEnd="8dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintDimensionRatio="1:1"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:ignore="ContentDescription"
-        tools:src="@mipmap/ic_launcher_round" />
-
-    <TextView
-        android:id="@+id/title"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:paddingStart="0dp"
-        android:paddingEnd="8dp"
-        android:textAppearance="?attr/textAppearanceBodyMedium"
-        app:layout_constraintBottom_toTopOf="@id/subtitle"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@+id/image"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintVertical_chainStyle="packed"
-        tools:text="Source title" />
-
-    <TextView
-        android:id="@+id/subtitle"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:maxLines="1"
-        android:textAppearance="?attr/textAppearanceBodySmall"
-        android:visibility="gone"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@id/image"
-        app:layout_constraintTop_toBottomOf="@+id/title"
-        tools:text="English"
-        tools:visibility="visible" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 9 - 1
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;
+WHERE _id = :id;
+
+getSourceIdWithFavoriteCount:
+SELECT
+source,
+count(*)
+FROM mangas
+WHERE favorite = 1
+GROUP BY source;