diff --git a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt new file mode 100644 index 000000000..137d2ec0d --- /dev/null +++ b/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> { + return databaseHandler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) } + } +} diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 2d5c5427a..95f54676d 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/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 { MangaRepositoryImpl(get()) } + addFactory { GetFavoritesBySourceId(get()) } addFactory { GetNextChapterForManga(get()) } addSingletonFactory { HistoryRepositoryImpl(get()) } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetFavoritesBySourceId.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetFavoritesBySourceId.kt new file mode 100644 index 000000000..9fd42effa --- /dev/null +++ b/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> { + return mangaRepository.getFavoritesBySourceId(sourceId) + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt new file mode 100644 index 000000000..8fb60a78a --- /dev/null +++ b/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> +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt new file mode 100644 index 000000000..1f00c52ba --- /dev/null +++ b/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 + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt new file mode 100644 index 000000000..2bb052638 --- /dev/null +++ b/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, + 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) } + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt index cfe61a12e..cc3926e67 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt +++ b/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

> : NucleusController() { +abstract class ComposeController

>(bundle: Bundle? = null) : NucleusController(bundle) { override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = ComposeControllerBinding.inflate(inflater) @@ -54,7 +55,7 @@ abstract class BasicComposeController : BaseController @Composable abstract fun ComposeContent(nestedScrollInterop: NestedScrollConnection) } -abstract class SearchableComposeController

> : SearchableNucleusController() { +abstract class SearchableComposeController

>(bundle: Bundle? = null) : SearchableNucleusController(bundle) { override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = ComposeControllerBinding.inflate(inflater) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaAdapter.kt deleted file mode 100644 index 389b76f72..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaAdapter.kt +++ /dev/null @@ -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>(null, controller, true) { - - val coverClickListener: OnCoverClickListener = controller - - interface OnCoverClickListener { - fun onCoverClick(position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt index da427905b..5adece44b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt +++ b/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, - FlexibleAdapter.OnItemClickListener, - MigrationMangaAdapter.OnCoverClickListener { - - private var adapter: MigrationMangaAdapter? = null +class MigrationMangaController : ComposeController { 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 getTitle(): String? = sourceName - override fun createPresenter(): MigrationMangaPresenter { - return MigrationMangaPresenter(sourceId) - } + override fun createPresenter(): MigrationMangaPresenter = MigrationMangaPresenter(sourceId) - override fun createBinding(inflater: LayoutInflater) = MigrationMangaControllerBinding.inflate(inflater) - - 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) { - 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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt deleted file mode 100644 index 40d615133..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaItem.kt deleted file mode 100644 index 6ad16471b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.source_list_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MigrationMangaHolder { - return MigrationMangaHolder(view, adapter as MigrationMangaAdapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: MigrationMangaHolder, - position: Int, - payloads: List?, - ) { - 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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt index 8e10fd7e6..97bab21a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt +++ b/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() { + private val _state: MutableStateFlow = MutableStateFlow(MigrateMangaState.Loading) + val state: StateFlow = _state.asStateFlow() + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - - db.getFavoriteMangas() - .asRxObservable() - .observeOn(AndroidSchedulers.mainThread()) - .map { libraryToMigrationItem(it) } - .subscribeLatestCache(MigrationMangaController::setManga) - } - - private fun libraryToMigrationItem(library: List): List { - return library.filter { it.source == sourceId } - .sortedBy { it.title } - .map { MigrationMangaItem(it) } + presenterScope.launchIO { + getFavoritesBySourceId + .subscribe(sourceId) + .catch { exception -> + _state.emit(MigrateMangaState.Error(exception)) + } + .collectLatest { list -> + _state.emit(MigrateMangaState.Success(list)) + } + } } } + +sealed class MigrateMangaState { + object Loading : MigrateMangaState() + data class Error(val error: Throwable) : MigrateMangaState() + data class Success(val list: List) : MigrateMangaState() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt index 38d1fb5a3..4c4dfe3bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt +++ b/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() + .getManga(mangaId) + .executeAsBlocking() + ) + private var newManga: Manga? = null override fun createPresenter(): GlobalSearchPresenter { diff --git a/app/src/main/res/layout/migration_manga_controller.xml b/app/src/main/res/layout/migration_manga_controller.xml deleted file mode 100644 index 368782972..000000000 --- a/app/src/main/res/layout/migration_manga_controller.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59f0fc87b..ae0798547 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -717,6 +717,7 @@ Select a source to migrate from Migrate Copy + Well, this is awkward Couldn\'t download chapters. You can try again in the downloads section diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index 4010faa48..e46bb4c5e 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -36,4 +36,10 @@ source, count(*) FROM mangas WHERE favorite = 1 -GROUP BY source; \ No newline at end of file +GROUP BY source; + +getFavoriteBySourceId: +SELECT * +FROM mangas +WHERE favorite = 1 +AND source = :sourceId; \ No newline at end of file