diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4a2c84584..ae07018d1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -100,6 +100,9 @@
+
{
BookmarksSheet.show(parentFragmentManager, manga)
}
+
+ R.id.button_related_more -> {
+ startActivity(RelatedMangaActivity.newIntent(v.context, manga))
+ }
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListFragment.kt
new file mode 100644
index 000000000..8ec35ac90
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListFragment.kt
@@ -0,0 +1,24 @@
+package org.koitharu.kotatsu.details.ui.related
+
+import android.view.Menu
+import androidx.appcompat.view.ActionMode
+import androidx.fragment.app.viewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.ui.list.ListSelectionController
+import org.koitharu.kotatsu.list.ui.MangaListFragment
+
+@AndroidEntryPoint
+class RelatedListFragment : MangaListFragment() {
+
+ override val viewModel by viewModels()
+ override val isSwipeRefreshEnabled = false
+
+ override fun onScrolledToEnd() = Unit
+
+ override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
+ mode.menuInflater.inflate(R.menu.mode_remote, menu)
+ return super.onCreateActionMode(controller, mode, menu)
+ }
+}
+
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt
new file mode 100644
index 000000000..8cc83be4b
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt
@@ -0,0 +1,99 @@
+package org.koitharu.kotatsu.details.ui.related
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.plus
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
+import org.koitharu.kotatsu.core.parser.MangaIntent
+import org.koitharu.kotatsu.core.parser.MangaRepository
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.util.ext.call
+import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
+import org.koitharu.kotatsu.core.util.ext.require
+import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
+import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.ui.MangaListViewModel
+import org.koitharu.kotatsu.list.ui.model.EmptyState
+import org.koitharu.kotatsu.list.ui.model.LoadingState
+import org.koitharu.kotatsu.list.ui.model.toErrorState
+import org.koitharu.kotatsu.list.ui.model.toUi
+import org.koitharu.kotatsu.parsers.model.Manga
+import javax.inject.Inject
+
+@HiltViewModel
+class RelatedListViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ mangaRepositoryFactory: MangaRepository.Factory,
+ settings: AppSettings,
+ private val extraProvider: ListExtraProvider,
+ downloadScheduler: DownloadWorker.Scheduler,
+) : MangaListViewModel(settings, downloadScheduler) {
+
+ private val seed = savedStateHandle.require(MangaIntent.KEY_MANGA).manga
+ private val repository = mangaRepositoryFactory.create(seed.source)
+ private val mangaList = MutableStateFlow?>(null)
+ private val listError = MutableStateFlow(null)
+ private var loadingJob: Job? = null
+
+ override val content = combine(
+ mangaList,
+ listMode,
+ listError,
+ ) { list, mode, error ->
+ when {
+ list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
+ list == null -> listOf(LoadingState)
+ list.isEmpty() -> listOf(createEmptyState())
+ else -> list.toUi(mode, extraProvider)
+ }
+ }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
+
+ init {
+ loadList()
+ }
+
+ override fun onRefresh() {
+ loadList()
+ }
+
+ override fun onRetry() {
+ loadList()
+ }
+
+ private fun loadList(): Job {
+ loadingJob?.let {
+ if (it.isActive) return it
+ }
+ return launchLoadingJob(Dispatchers.Default) {
+ try {
+ listError.value = null
+ mangaList.value = repository.getRelated(seed)
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Throwable) {
+ e.printStackTraceDebug()
+ listError.value = e
+ if (!mangaList.value.isNullOrEmpty()) {
+ errorEvent.call(e)
+ }
+ }
+ }.also { loadingJob = it }
+ }
+
+ private fun createEmptyState() = EmptyState(
+ icon = R.drawable.ic_empty_common,
+ textPrimary = R.string.nothing_found,
+ textSecondary = 0,
+ actionStringRes = 0,
+ )
+}
+
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedMangaActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedMangaActivity.kt
new file mode 100644
index 000000000..49aca965c
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedMangaActivity.kt
@@ -0,0 +1,50 @@
+package org.koitharu.kotatsu.details.ui.related
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.core.graphics.Insets
+import androidx.core.view.updatePadding
+import androidx.fragment.app.commit
+import com.google.android.material.appbar.AppBarLayout
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
+import org.koitharu.kotatsu.core.parser.MangaIntent
+import org.koitharu.kotatsu.core.ui.BaseActivity
+import org.koitharu.kotatsu.databinding.ActivityContainerBinding
+import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
+import org.koitharu.kotatsu.parsers.model.Manga
+
+@AndroidEntryPoint
+class RelatedMangaActivity : BaseActivity(), AppBarOwner {
+
+ override val appBar: AppBarLayout
+ get() = viewBinding.appbar
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityContainerBinding.inflate(layoutInflater))
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ val fm = supportFragmentManager
+ if (fm.findFragmentById(R.id.container) == null) {
+ fm.commit {
+ setReorderingAllowed(true)
+ replace(R.id.container, RelatedListFragment::class.java, intent.extras)
+ }
+ }
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ viewBinding.root.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ )
+ }
+
+ companion object {
+
+ fun newIntent(context: Context, seed: Manga) = Intent(context, RelatedMangaActivity::class.java)
+ .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(seed, withChapters = false))
+ }
+}
diff --git a/app/src/main/res/layout/item_tip.xml b/app/src/main/res/layout/item_tip.xml
index 0beb8d9f9..0c73ff9fb 100644
--- a/app/src/main/res/layout/item_tip.xml
+++ b/app/src/main/res/layout/item_tip.xml
@@ -3,7 +3,7 @@
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"
- style="?materialCardViewOutlinedStyle"
+ style="?materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small">