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.domain.source.model.Source
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
val sourceMapper: (CatalogueSource) -> Source = { source -> val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
Source( Source(
source.id, source.id,
source.lang, source.lang,
source.name, 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 package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class SourceRepositoryImpl( class SourceRepositoryImpl(
private val sourceManager: SourceManager private val sourceManager: SourceManager,
private val handler: DatabaseHandler
) : SourceRepository { ) : SourceRepository {
override fun getSources(): Flow<List<Source>> { override fun getSources(): Flow<List<Source>> {
return sourceManager.catalogueSources.map { sources -> 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.history.repository.HistoryRepository
import eu.kanade.domain.source.interactor.DisableSource import eu.kanade.domain.source.interactor.DisableSource
import eu.kanade.domain.source.interactor.GetEnabledSources 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.interactor.ToggleSourcePin
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
@ -29,9 +31,11 @@ class DomainModule : InjektModule {
addFactory { RemoveHistoryById(get()) } addFactory { RemoveHistoryById(get()) }
addFactory { RemoveHistoryByMangaId(get()) } addFactory { RemoveHistoryByMangaId(get()) }
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get()) } addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
addFactory { GetEnabledSources(get(), get()) } addFactory { GetEnabledSources(get(), get()) }
addFactory { DisableSource(get()) } addFactory { DisableSource(get()) }
addFactory { ToggleSourcePin(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 { interface SourceRepository {
fun getSources(): Flow<List<Source>> 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.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio 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.filled.PushPin
import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@ -30,18 +27,18 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.components.EmptyScreen 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.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -62,7 +59,7 @@ fun SourceScreen(
val state by presenter.state.collectAsState() val state by presenter.state.collectAsState()
when { when {
state.isLoading -> CircularProgressIndicator() state.isLoading -> LoadingScreen()
state.hasError -> Text(text = state.error!!.message!!) state.hasError -> Text(text = state.error!!.message!!)
state.isEmpty -> EmptyScreen(message = "") state.isEmpty -> EmptyScreen(message = "")
else -> SourceList( else -> SourceList(
@ -115,7 +112,7 @@ fun SourceList(
} }
is UiModel.Item -> SourceItem( is UiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
item = model.source, source = model.source,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { onLongClickItem = {
setSourceState(it) setSourceState(it)
@ -160,55 +157,34 @@ fun SourceHeader(
@Composable @Composable
fun SourceItem( fun SourceItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: Source, source: Source,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
onClickLatest: (Source) -> Unit, onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit onClickPin: (Source) -> Unit
) { ) {
Row( BaseSourceItem(
modifier = modifier modifier = modifier,
.combinedClickable( source = source,
onClick = { onClickItem(item) }, onClickItem = { onClickItem(source) },
onLongClick = { onLongClickItem(item) } onLongClickItem = { onLongClickItem(source) },
) action = { source ->
.padding(horizontal = horizontalPadding, vertical = 8.dp), if (source.supportsLatest) {
verticalAlignment = Alignment.CenterVertically, TextButton(onClick = { onClickLatest(source) }) {
) {
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(
text = stringResource(id = R.string.latest), text = stringResource(id = R.string.latest),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
), )
) )
} }
} }
SourcePinButton( SourcePinButton(
isPinned = Pin.Pinned in item.pin, isPinned = Pin.Pinned in source.pin,
onClick = { onClickPin(item) } onClick = { onClickPin(source) }
)
},
) )
}
} }
@Composable @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.core.net.toUri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist 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.DisplayModeSetting
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting 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 librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING) fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL) fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, SetMigrateSorting.Mode.ALPHABETICAL)
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING) fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, SetMigrateSorting.Direction.ASCENDING)
fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true) fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true)

View File

@ -1,124 +1,68 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import androidx.compose.runtime.Composable
import androidx.recyclerview.widget.LinearLayoutManager import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import dev.chrisbanes.insetter.applyInsetter import eu.kanade.presentation.source.MigrateSourceScreen
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import uy.kohesive.injekt.injectLazy
class MigrationSourcesController : class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() {
NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener {
private val preferences: PreferencesHelper by injectLazy()
private var adapter: SourceAdapter? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createPresenter(): MigrationSourcesPresenter { override fun createPresenter(): MigrationSourcesPresenter =
return MigrationSourcesPresenter() MigrationSourcesPresenter()
}
override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater) @Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
override fun onViewCreated(view: View) { MigrateSourceScreen(
super.onViewCreated(view) nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
binding.recycler.applyInsetter { onClickItem = { source ->
type(navigationBars = true) { parentController!!.router.pushController(
padding() MigrationMangaController(
} source.id,
} source.name
)
adapter = SourceAdapter(this) )
binding.recycler.layoutManager = LinearLayoutManager(view.context) },
binding.recycler.adapter = adapter onLongClickItem = { source ->
adapter?.fastScroller = binding.fastScroller val sourceId = source.id.toString()
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
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)
}
R.id.asc_count, R.id.desc_count -> {
setSortingDirection(SortSetting.TOTAL, itemId == R.id.asc_count)
}
}
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) activity?.copyToClipboard(sourceId, sourceId)
} }
)
enum class DirectionSetting {
ASCENDING,
DESCENDING;
} }
enum class SortSetting { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
ALPHABETICAL, inflater.inflate(R.menu.browse_migrate, menu)
TOTAL;
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (val itemId = item.itemId) {
R.id.action_source_migration_help -> {
activity?.openInBrowser(HELP_URL)
true
}
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)
}
} }
} }

View File

@ -1,82 +1,60 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.domain.source.model.Source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.combineLatest import eu.kanade.tachiyomi.util.lang.launchIO
import rx.android.schedulers.AndroidSchedulers import kotlinx.coroutines.flow.MutableStateFlow
import rx.schedulers.Schedulers 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.Injekt
import uy.kohesive.injekt.api.get 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( class MigrationSourcesPresenter(
private val sourceManager: SourceManager = Injekt.get(), private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get()
) : BasePresenter<MigrationSourcesController>() { ) : BasePresenter<MigrationSourcesController>() {
private val preferences: PreferencesHelper by injectLazy() private val _state: MutableStateFlow<MigrateSourceState> = MutableStateFlow(MigrateSourceState.EMPTY)
val state: StateFlow<MigrateSourceState> = _state.asStateFlow()
private val sortRelay = BehaviorRelay.create(Unit)
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
db.getFavoriteMangas() presenterScope.launchIO {
.asRxObservable() getSourcesWithFavoriteCount.subscribe()
.combineLatest(sortRelay.observeOn(Schedulers.io())) { sources, _ -> sources } .collectLatest { sources ->
.observeOn(AndroidSchedulers.mainThread()) _state.update { state ->
.map { findSourcesWithManga(it) } state.copy(sources = sources)
.subscribeLatestCache(MigrationSourcesController::setSources)
} }
fun requestSortUpdate() {
sortRelay.call(Unit)
} }
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) { fun setAlphabeticalSorting(isAscending: Boolean) {
MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn) setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending)
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

@ -29,3 +29,11 @@ getMangaById:
SELECT * SELECT *
FROM mangas FROM mangas
WHERE _id = :id; WHERE _id = :id;
getSourceIdWithFavoriteCount:
SELECT
source,
count(*)
FROM mangas
WHERE favorite = 1
GROUP BY source;