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
This commit is contained in:
Andreas 2022-04-27 14:36:16 +02:00 committed by GitHub
parent a4a4503311
commit 7261fcccda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 432 additions and 456 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
}
}

View file

@ -6,4 +6,6 @@ import kotlinx.coroutines.flow.Flow
interface SourceRepository {
fun getSources(): Flow<List<Source>>
fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
}

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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 createPresenter(): MigrationSourcesPresenter =
MigrationSourcesPresenter()
override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
MigrateSourceScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickItem = { source ->
parentController!!.router.pushController(
MigrationMangaController(
source.id,
source.name
)
)
},
onLongClickItem = { source ->
val sourceId = source.id.toString()
activity?.copyToClipboard(sourceId, sourceId)
}
}
adapter = SourceAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
adapter?.fastScroller = binding.fastScroller
)
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
inflater.inflate(R.menu.browse_migrate, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (val itemId = item.itemId) {
R.id.action_source_migration_help -> activity?.openInBrowser(HELP_URL)
R.id.asc_alphabetical, R.id.desc_alphabetical -> {
setSortingDirection(SortSetting.ALPHABETICAL, itemId == R.id.asc_alphabetical)
return when (val itemId = item.itemId) {
R.id.action_source_migration_help -> {
activity?.openInBrowser(HELP_URL)
true
}
R.id.asc_count, R.id.desc_count -> {
setSortingDirection(SortSetting.TOTAL, itemId == R.id.asc_count)
R.id.asc_alphabetical,
R.id.desc_alphabetical -> {
presenter.setAlphabeticalSorting(itemId == R.id.asc_alphabetical)
true
}
R.id.asc_count,
R.id.desc_count -> {
presenter.setTotalSorting(itemId == R.id.asc_count)
true
}
else -> super.onOptionsItemSelected(item)
}
return super.onOptionsItemSelected(item)
}
private fun setSortingDirection(sortSetting: SortSetting, isAscending: Boolean) {
val direction = if (isAscending) {
DirectionSetting.ASCENDING
} else {
DirectionSetting.DESCENDING
}
preferences.migrationSortingDirection().set(direction)
preferences.migrationSortingMode().set(sortSetting)
presenter.requestSortUpdate()
}
fun setSources(sourcesWithManga: List<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;
}
}

View file

@ -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()
}
private fun sortFn(): java.util.Comparator<SourceItem> {
val sort by lazy {
preferences.migrationSortingMode().get()
}
val direction by lazy {
preferences.migrationSortingDirection().get()
}
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
}
val sortFn: (SourceItem, SourceItem) -> Int = { a, b ->
when (sort) {
MigrationSourcesController.SortSetting.ALPHABETICAL -> collator.compare(a.source.name.lowercase(locale), b.source.name.lowercase(locale))
MigrationSourcesController.SortSetting.TOTAL -> a.mangaCount.compareTo(b.mangaCount)
}
}
return when (direction) {
MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn)
MigrationSourcesController.DirectionSetting.DESCENDING -> Collections.reverseOrder(sortFn)
}
fun setTotalSorting(isAscending: Boolean) {
setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending)
}
}
data class MigrateSourceState(
val sources: List<Pair<Source, Long>>?
) {
val isLoading: Boolean
get() = sources == null
val isEmpty: Boolean
get() = sources.isNullOrEmpty()
companion object {
val EMPTY = MigrateSourceState(null)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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;