浏览代码

Migrate Migrate Manga screen to Compose (#7045)

* Migrate Migrate Manga screen to Compose

* Changes from review comments
Andreas 2 年之前
父节点
当前提交
bf6d59cd21

+ 15 - 0
app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt

@@ -0,0 +1,15 @@
+package eu.kanade.data.manga
+
+import eu.kanade.data.DatabaseHandler
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.repository.MangaRepository
+import kotlinx.coroutines.flow.Flow
+
+class MangaRepositoryImpl(
+    private val databaseHandler: DatabaseHandler
+) : MangaRepository {
+
+    override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
+        return databaseHandler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
+    }
+}

+ 5 - 0
app/src/main/java/eu/kanade/domain/DomainModule.kt

@@ -1,6 +1,7 @@
 package eu.kanade.domain
 
 import eu.kanade.data.history.HistoryRepositoryImpl
+import eu.kanade.data.manga.MangaRepositoryImpl
 import eu.kanade.data.source.SourceRepositoryImpl
 import eu.kanade.domain.history.interactor.DeleteHistoryTable
 import eu.kanade.domain.history.interactor.GetHistory
@@ -8,6 +9,8 @@ import eu.kanade.domain.history.interactor.GetNextChapterForManga
 import eu.kanade.domain.history.interactor.RemoveHistoryById
 import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
 import eu.kanade.domain.history.repository.HistoryRepository
+import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
+import eu.kanade.domain.manga.repository.MangaRepository
 import eu.kanade.domain.source.interactor.DisableSource
 import eu.kanade.domain.source.interactor.GetEnabledSources
 import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
@@ -23,6 +26,8 @@ import uy.kohesive.injekt.api.get
 class DomainModule : InjektModule {
 
     override fun InjektRegistrar.registerInjectables() {
+        addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
+        addFactory { GetFavoritesBySourceId(get()) }
         addFactory { GetNextChapterForManga(get()) }
 
         addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }

+ 14 - 0
app/src/main/java/eu/kanade/domain/manga/interactor/GetFavoritesBySourceId.kt

@@ -0,0 +1,14 @@
+package eu.kanade.domain.manga.interactor
+
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.repository.MangaRepository
+import kotlinx.coroutines.flow.Flow
+
+class GetFavoritesBySourceId(
+    private val mangaRepository: MangaRepository
+) {
+
+    fun subscribe(sourceId: Long): Flow<List<Manga>> {
+        return mangaRepository.getFavoritesBySourceId(sourceId)
+    }
+}

+ 9 - 0
app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt

@@ -0,0 +1,9 @@
+package eu.kanade.domain.manga.repository
+
+import eu.kanade.domain.manga.model.Manga
+import kotlinx.coroutines.flow.Flow
+
+interface MangaRepository {
+
+    fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
+}

+ 65 - 0
app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt

@@ -0,0 +1,65 @@
+package eu.kanade.presentation.manga.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+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.manga.model.Manga
+import eu.kanade.presentation.components.MangaCover
+import eu.kanade.presentation.util.horizontalPadding
+
+@Composable
+fun BaseMangaListItem(
+    modifier: Modifier = Modifier,
+    manga: Manga,
+    onClickItem: () -> Unit = {},
+    onClickCover: () -> Unit = onClickItem,
+    cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
+    actions: @Composable RowScope.() -> Unit = {},
+    content: @Composable RowScope.() -> Unit = { defaultContent(manga) },
+) {
+    Row(
+        modifier = modifier
+            .clickable(onClick = onClickItem)
+            .height(56.dp)
+            .padding(horizontal = horizontalPadding),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        cover()
+        content()
+        actions()
+    }
+}
+
+private val defaultCover: @Composable RowScope.(Manga, () -> Unit) -> Unit = { manga, onClick ->
+    MangaCover.Square(
+        modifier = Modifier
+            .padding(vertical = 8.dp)
+            .clickable(onClick = onClick)
+            .fillMaxHeight(),
+        data = manga.thumbnailUrl
+    )
+}
+
+private val defaultContent: @Composable RowScope.(Manga) -> Unit = {
+    Box(modifier = Modifier.weight(1f)) {
+        Text(
+            text = it.title,
+            modifier = Modifier
+                .padding(start = horizontalPadding),
+            overflow = TextOverflow.Ellipsis,
+            maxLines = 1,
+            style = MaterialTheme.typography.bodyMedium
+        )
+    }
+}

+ 84 - 0
app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt

@@ -0,0 +1,84 @@
+package eu.kanade.presentation.source
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+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.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.presentation.components.EmptyScreen
+import eu.kanade.presentation.components.LoadingScreen
+import eu.kanade.presentation.manga.components.BaseMangaListItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
+import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaPresenter
+
+@Composable
+fun MigrateMangaScreen(
+    nestedScrollInterop: NestedScrollConnection,
+    presenter: MigrationMangaPresenter,
+    onClickItem: (Manga) -> Unit,
+    onClickCover: (Manga) -> Unit
+) {
+    val state by presenter.state.collectAsState()
+
+    when (state) {
+        MigrateMangaState.Loading -> LoadingScreen()
+        is MigrateMangaState.Error -> Text(text = (state as MigrateMangaState.Error).error.message!!)
+        is MigrateMangaState.Success -> {
+            MigrateMangaContent(
+                nestedScrollInterop = nestedScrollInterop,
+                list = (state as MigrateMangaState.Success).list,
+                onClickItem = onClickItem,
+                onClickCover = onClickCover,
+            )
+        }
+    }
+}
+
+@Composable
+fun MigrateMangaContent(
+    nestedScrollInterop: NestedScrollConnection,
+    list: List<Manga>,
+    onClickItem: (Manga) -> Unit,
+    onClickCover: (Manga) -> Unit
+) {
+    if (list.isEmpty()) {
+        EmptyScreen(textResource = R.string.migrate_empty_screen)
+        return
+    }
+    LazyColumn(
+        modifier = Modifier.nestedScroll(nestedScrollInterop),
+        contentPadding = WindowInsets.navigationBars.asPaddingValues(),
+    ) {
+        items(list) { manga ->
+            MigrateMangaItem(
+                manga = manga,
+                onClickItem = onClickItem,
+                onClickCover = onClickCover
+            )
+        }
+    }
+}
+
+@Composable
+fun MigrateMangaItem(
+    modifier: Modifier = Modifier,
+    manga: Manga,
+    onClickItem: (Manga) -> Unit,
+    onClickCover: (Manga) -> Unit
+) {
+    BaseMangaListItem(
+        modifier = modifier,
+        manga = manga,
+        onClickItem = { onClickItem(manga) },
+        onClickCover = { onClickCover(manga) }
+    )
+}

+ 3 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt

@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.ui.base.controller
 
+import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import androidx.compose.runtime.Composable
@@ -13,7 +14,7 @@ import nucleus.presenter.Presenter
 /**
  * Compose controller with a Nucleus presenter.
  */
-abstract class ComposeController<P : Presenter<*>> : NucleusController<ComposeControllerBinding, P>() {
+abstract class ComposeController<P : Presenter<*>>(bundle: Bundle? = null) : NucleusController<ComposeControllerBinding, P>(bundle) {
 
     override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
         ComposeControllerBinding.inflate(inflater)
@@ -54,7 +55,7 @@ abstract class BasicComposeController : BaseController<ComposeControllerBinding>
     @Composable abstract fun ComposeContent(nestedScrollInterop: NestedScrollConnection)
 }
 
-abstract class SearchableComposeController<P : BasePresenter<*>> : SearchableNucleusController<ComposeControllerBinding, P>() {
+abstract class SearchableComposeController<P : BasePresenter<*>>(bundle: Bundle? = null) : SearchableNucleusController<ComposeControllerBinding, P>(bundle) {
 
     override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
         ComposeControllerBinding.inflate(inflater)

+ 0 - 14
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaAdapter.kt

@@ -1,14 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.migration.manga
-
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-
-class MigrationMangaAdapter(controller: MigrationMangaController) :
-    FlexibleAdapter<IFlexible<*>>(null, controller, true) {
-
-    val coverClickListener: OnCoverClickListener = controller
-
-    interface OnCoverClickListener {
-        fun onCoverClick(position: Int)
-    }
-}

+ 18 - 54
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt

@@ -1,24 +1,16 @@
 package eu.kanade.tachiyomi.ui.browse.migration.manga
 
 import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.core.os.bundleOf
-import androidx.recyclerview.widget.LinearLayoutManager
-import dev.chrisbanes.insetter.applyInsetter
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.presentation.source.MigrateMangaScreen
+import eu.kanade.tachiyomi.ui.base.controller.ComposeController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
 import eu.kanade.tachiyomi.ui.manga.MangaController
 
-class MigrationMangaController :
-    NucleusController<MigrationMangaControllerBinding, MigrationMangaPresenter>,
-    FlexibleAdapter.OnItemClickListener,
-    MigrationMangaAdapter.OnCoverClickListener {
-
-    private var adapter: MigrationMangaAdapter? = null
+class MigrationMangaController : ComposeController<MigrationMangaPresenter> {
 
     constructor(sourceId: Long, sourceName: String?) : super(
         bundleOf(
@@ -36,50 +28,22 @@ class MigrationMangaController :
     private val sourceId: Long = args.getLong(SOURCE_ID_EXTRA)
     private val sourceName: String? = args.getString(SOURCE_NAME_EXTRA)
 
-    override fun getTitle(): String? {
-        return sourceName
-    }
-
-    override fun createPresenter(): MigrationMangaPresenter {
-        return MigrationMangaPresenter(sourceId)
-    }
+    override fun getTitle(): String? = sourceName
 
-    override fun createBinding(inflater: LayoutInflater) = MigrationMangaControllerBinding.inflate(inflater)
+    override fun createPresenter(): MigrationMangaPresenter = MigrationMangaPresenter(sourceId)
 
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        binding.recycler.applyInsetter {
-            type(navigationBars = true) {
-                padding()
+    @Composable
+    override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
+        MigrateMangaScreen(
+            nestedScrollInterop = nestedScrollInterop,
+            presenter = presenter,
+            onClickItem = {
+                router.pushController(SearchController(it.id))
+            },
+            onClickCover = {
+                router.pushController(MangaController(it.id))
             }
-        }
-
-        adapter = MigrationMangaAdapter(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)
-    }
-
-    fun setManga(manga: List<MigrationMangaItem>) {
-        adapter?.updateDataSet(manga)
-    }
-
-    override fun onItemClick(view: View, position: Int): Boolean {
-        val item = adapter?.getItem(position) as? MigrationMangaItem ?: return false
-        val controller = SearchController(item.manga)
-        router.pushController(controller)
-        return false
-    }
-
-    override fun onCoverClick(position: Int) {
-        val mangaItem = adapter?.getItem(position) as? MigrationMangaItem ?: return
-        router.pushController(MangaController(mangaItem.manga))
+        )
     }
 
     companion object {

+ 0 - 29
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt

@@ -1,29 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.migration.manga
-
-import android.view.View
-import coil.dispose
-import coil.load
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.databinding.SourceListItemBinding
-
-class MigrationMangaHolder(
-    view: View,
-    private val adapter: MigrationMangaAdapter,
-) : FlexibleViewHolder(view, adapter) {
-
-    private val binding = SourceListItemBinding.bind(view)
-
-    init {
-        binding.thumbnail.setOnClickListener {
-            adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
-        }
-    }
-
-    fun bind(item: MigrationMangaItem) {
-        binding.title.text = item.manga.title
-
-        // Update the cover
-        binding.thumbnail.dispose()
-        binding.thumbnail.load(item.manga)
-    }
-}

+ 0 - 40
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaItem.kt

@@ -1,40 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.migration.manga
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-
-class MigrationMangaItem(val manga: Manga) : AbstractFlexibleItem<MigrationMangaHolder>() {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.source_list_item
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationMangaHolder {
-        return MigrationMangaHolder(view, adapter as MigrationMangaAdapter)
-    }
-
-    override fun bindViewHolder(
-        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
-        holder: MigrationMangaHolder,
-        position: Int,
-        payloads: List<Any?>?,
-    ) {
-        holder.bind(this)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (other is MigrationMangaItem) {
-            return manga.id == other.manga.id
-        }
-        return false
-    }
-
-    override fun hashCode(): Int {
-        return manga.id!!.hashCode()
-    }
-}

+ 27 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt

@@ -1,31 +1,43 @@
 package eu.kanade.tachiyomi.ui.browse.migration.manga
 
 import android.os.Bundle
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
+import eu.kanade.domain.manga.model.Manga
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import rx.android.schedulers.AndroidSchedulers
+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.catch
+import kotlinx.coroutines.flow.collectLatest
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
 class MigrationMangaPresenter(
     private val sourceId: Long,
-    private val db: DatabaseHelper = Injekt.get(),
+    private val getFavoritesBySourceId: GetFavoritesBySourceId = Injekt.get()
 ) : BasePresenter<MigrationMangaController>() {
 
+    private val _state: MutableStateFlow<MigrateMangaState> = MutableStateFlow(MigrateMangaState.Loading)
+    val state: StateFlow<MigrateMangaState> = _state.asStateFlow()
+
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
-
-        db.getFavoriteMangas()
-            .asRxObservable()
-            .observeOn(AndroidSchedulers.mainThread())
-            .map { libraryToMigrationItem(it) }
-            .subscribeLatestCache(MigrationMangaController::setManga)
+        presenterScope.launchIO {
+            getFavoritesBySourceId
+                .subscribe(sourceId)
+                .catch { exception ->
+                    _state.emit(MigrateMangaState.Error(exception))
+                }
+                .collectLatest { list ->
+                    _state.emit(MigrateMangaState.Success(list))
+                }
+        }
     }
+}
 
-    private fun libraryToMigrationItem(library: List<Manga>): List<MigrationMangaItem> {
-        return library.filter { it.source == sourceId }
-            .sortedBy { it.title }
-            .map { MigrationMangaItem(it) }
-    }
+sealed class MigrateMangaState {
+    object Loading : MigrateMangaState()
+    data class Error(val error: Throwable) : MigrateMangaState()
+    data class Success(val list: List<Manga>) : MigrateMangaState()
 }

+ 9 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt

@@ -7,6 +7,7 @@ import com.bluelinelabs.conductor.Controller
 import com.bluelinelabs.conductor.RouterTransaction
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import eu.kanade.tachiyomi.R
+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.CatalogueSource
@@ -16,12 +17,20 @@ import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
 import eu.kanade.tachiyomi.ui.manga.MangaController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
 
 class SearchController(
     private var manga: Manga? = null,
 ) : GlobalSearchController(manga?.title) {
 
+    constructor(mangaId: Long) : this(
+        Injekt.get<DatabaseHelper>()
+            .getManga(mangaId)
+            .executeAsBlocking()
+    )
+
     private var newManga: Manga? = null
 
     override fun createPresenter(): GlobalSearchPresenter {

+ 0 - 21
app/src/main/res/layout/migration_manga_controller.xml

@@ -1,21 +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" />
-
-    <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" />
-
-</FrameLayout>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -717,6 +717,7 @@
     <string name="migration_selection_prompt">Select a source to migrate from</string>
     <string name="migrate">Migrate</string>
     <string name="copy">Copy</string>
+    <string name="migrate_empty_screen">Well, this is awkward</string>
 
     <!-- Downloads activity and service -->
     <string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>

+ 7 - 1
app/src/main/sqldelight/data/mangas.sq

@@ -36,4 +36,10 @@ source,
 count(*)
 FROM mangas
 WHERE favorite = 1
-GROUP BY source;
+GROUP BY source;
+
+getFavoriteBySourceId:
+SELECT *
+FROM mangas
+WHERE favorite = 1
+AND source = :sourceId;