Initial conversion of browse tabs to full Compose

TODO:
- Global search should launch a controller with the search textfield focused. This is pending a Compose rewrite of that screen.
- Better migrate sort UI
- Extensions search
This commit is contained in:
arkon
2022-08-29 17:18:06 -04:00
parent 084e6a964e
commit 92e83f702c
32 changed files with 458 additions and 701 deletions

View File

@@ -4,10 +4,7 @@ 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.compose.ui.platform.rememberNestedScrollInteropConnection
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.view.setComposeContent
import nucleus.presenter.Presenter
@@ -29,33 +26,11 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
}
}
/**
* Compose controller with a Nucleus presenter.
*/
abstract class ComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
NucleusController<ComposeControllerBinding, P>(bundle),
ComposeContentController {
override fun createBinding(inflater: LayoutInflater) =
ComposeControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.root.apply {
setComposeContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
ComposeContent(nestedScrollInterop)
}
}
}
}
/**
* Basic Compose controller without a presenter.
*/
abstract class BasicFullComposeController :
BaseController<ComposeControllerBinding>(),
abstract class BasicFullComposeController(bundle: Bundle? = null) :
BaseController<ComposeControllerBinding>(bundle),
FullComposeContentController {
override fun createBinding(inflater: LayoutInflater) =
@@ -72,29 +47,6 @@ abstract class BasicFullComposeController :
}
}
abstract class SearchableComposeController<P : BasePresenter<*>>(bundle: Bundle? = null) :
SearchableNucleusController<ComposeControllerBinding, P>(bundle),
ComposeContentController {
override fun createBinding(inflater: LayoutInflater) =
ComposeControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.root.apply {
setComposeContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
ComposeContent(nestedScrollInterop)
}
}
}
}
interface FullComposeContentController {
@Composable fun ComposeContent()
}
interface ComposeContentController {
@Composable fun ComposeContent(nestedScrollInterop: NestedScrollConnection)
}

View File

@@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.ui.base.controller
import com.google.android.material.tabs.TabLayout
interface TabbedController {
/**
* @return true to let activity updates tabs visibility (to visible)
*/
fun configureTabs(tabs: TabLayout): Boolean = true
fun cleanupTabs(tabs: TabLayout) {}
}

View File

@@ -1,149 +1,53 @@
package eu.kanade.tachiyomi.ui.browse
import android.Manifest
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.PagerControllerBinding
import eu.kanade.presentation.browse.BrowseScreen
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsController
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourcesTab
import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
import eu.kanade.tachiyomi.ui.main.MainActivity
import uy.kohesive.injekt.injectLazy
class BrowseController :
RxController<PagerControllerBinding>,
RootController,
TabbedController {
class BrowseController : FullComposeController<BrowsePresenter>, RootController {
@Suppress("unused")
constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false)
constructor(toExtensions: Boolean = false) : super(
bundleOf(TO_EXTENSIONS_EXTRA to toExtensions),
)
@Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getBoolean(TO_EXTENSIONS_EXTRA))
private val preferences: PreferencesHelper by injectLazy()
private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false)
val extensionListUpdateRelay: PublishRelay<Boolean> = PublishRelay.create()
override fun createPresenter() = BrowsePresenter()
private var adapter: BrowseAdapter? = null
@Composable
override fun ComposeContent() {
BrowseScreen(
startIndex = 1.takeIf { toExtensions },
tabs = listOf(
sourcesTab(router, presenter.sourcesPresenter),
extensionsTab(router, presenter.extensionsPresenter),
migrateSourcesTab(router, presenter.migrationSourcesPresenter),
),
)
override fun getTitle(): String? {
return resources!!.getString(R.string.browse)
LaunchedEffect(Unit) {
(activity as? MainActivity)?.ready = true
}
}
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = BrowseAdapter()
binding.pager.adapter = adapter
if (toExtensions) {
binding.pager.currentItem = EXTENSIONS_CONTROLLER
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
adapter = null
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
(activity as? MainActivity)?.binding?.tabs?.apply {
setupWithViewPager(binding.pager)
// Show badge on tab for extension updates
setExtensionUpdateBadge()
}
}
}
override fun configureTabs(tabs: TabLayout): Boolean {
with(tabs) {
tabGravity = TabLayout.GRAVITY_FILL
tabMode = TabLayout.MODE_FIXED
}
return true
}
override fun cleanupTabs(tabs: TabLayout) {
// Remove extension update badge
tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
}
fun setExtensionUpdateBadge() {
/* It's possible to switch to the Library controller by the time setExtensionUpdateBadge
is called, resulting in a badge being put on the category tabs (if enabled).
This check prevents that from happening */
if (router.backstack.lastOrNull()?.controller !is BrowseController) return
(activity as? MainActivity)?.binding?.tabs?.apply {
val updates = preferences.extensionUpdatesCount().get()
if (updates > 0) {
val badge: BadgeDrawable? = getTabAt(1)?.orCreateBadge
badge?.isVisible = true
} else {
getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
}
}
}
private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) {
private val tabTitles = listOf(
R.string.label_sources,
R.string.label_extensions,
R.string.label_migration,
)
.map { resources!!.getString(it) }
override fun getCount(): Int {
return tabTitles.size
}
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
val controller: Controller = when (position) {
SOURCES_CONTROLLER -> SourcesController()
EXTENSIONS_CONTROLLER -> ExtensionsController()
MIGRATION_CONTROLLER -> MigrationSourcesController()
else -> error("Wrong position $position")
}
router.setRoot(RouterTransaction.with(controller))
}
}
override fun getPageTitle(position: Int): CharSequence {
return tabTitles[position]
}
}
companion object {
const val TO_EXTENSIONS_EXTRA = "to_extensions"
const val SOURCES_CONTROLLER = 0
const val EXTENSIONS_CONTROLLER = 1
const val MIGRATION_CONTROLLER = 2
requestPermissionsSafe(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 301)
}
}
private const val TO_EXTENSIONS_EXTRA = "to_extensions"

View File

@@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.ui.browse
import android.os.Bundle
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
import uy.kohesive.injekt.api.get
class BrowsePresenter : BasePresenter<BrowseController>() {
val sourcesPresenter = SourcesPresenter(presenterScope)
val extensionsPresenter = ExtensionsPresenter(presenterScope)
val migrationSourcesPresenter = MigrationSourcesPresenter(presenterScope)
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
sourcesPresenter.onCreate()
extensionsPresenter.onCreate()
migrationSourcesPresenter.onCreate()
}
}

View File

@@ -1,120 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.presentation.browse.ExtensionScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.queryTextChanges
class ExtensionsController : ComposeController<ExtensionsPresenter>() {
private var query = ""
init {
setHasOptionsMenu(true)
}
override fun getTitle() = applicationContext?.getString(R.string.label_extensions)
override fun createPresenter() = ExtensionsPresenter()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
ExtensionScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
else -> presenter.uninstallExtension(extension.pkgName)
}
},
onClickItemCancel = { extension ->
presenter.cancelInstallUpdateExtension(extension)
},
onClickUpdateAll = {
presenter.updateAllExtensions()
},
onLaunched = {
val ctrl = parentController as BrowseController
ctrl.setExtensionUpdateBadge()
ctrl.extensionListUpdateRelay.call(true)
},
onInstallExtension = {
presenter.installExtension(it)
},
onOpenExtension = {
val controller = ExtensionDetailsController(it.pkgName)
parentController!!.router.pushController(controller)
},
onTrustExtension = {
presenter.trustSignature(it.signatureHash)
},
onUninstallExtension = {
presenter.uninstallExtension(it.pkgName)
},
onUpdateExtension = {
presenter.updateExtension(it)
},
onRefresh = {
presenter.findAvailableExtensions()
},
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_search -> expandActionViewFromInteraction = true
R.id.action_settings -> {
parentController!!.router.pushController(ExtensionFilterController())
}
}
return super.onOptionsItemSelected(item)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isPush) {
presenter.findAvailableExtensions()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.browse_extensions, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextChanges()
.filter { router.backstack.lastOrNull()?.controller == this }
.onEach {
query = it.toString()
presenter.search(query)
}
.launchIn(viewScope)
}
}

View File

@@ -1,41 +1,43 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import android.os.Bundle
import androidx.annotation.StringRes
import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.presentation.browse.ExtensionState
import eu.kanade.presentation.browse.ExtensionsState
import eu.kanade.presentation.browse.ExtensionsStateImpl
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionsPresenter(
private val presenterScope: CoroutineScope,
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(),
) : BasePresenter<ExtensionsController>(), ExtensionsState by state {
) : ExtensionsState by state {
private val _query: MutableStateFlow<String> = MutableStateFlow("")
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
fun onCreate() {
val context = Injekt.get<Application>()
val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map ->
{
@@ -114,6 +116,10 @@ class ExtensionsPresenter(
}
presenterScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount().asFlow()
.onEach { state.updates = it }
.launchIn(presenterScope)
}
fun search(query: String) {

View File

@@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.ExtensionScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
@Composable
fun extensionsTab(
router: Router?,
presenter: ExtensionsPresenter,
) = BrowseTab(
titleRes = R.string.label_extensions,
badgeNumber = presenter.updates.takeIf { it > 0 },
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = {
// TODO: extensions search
// presenter.search(query)
},
),
AppBar.Action(
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList,
onClick = { router?.pushController(ExtensionFilterController()) },
),
),
content = {
ExtensionScreen(
presenter = presenter,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
else -> presenter.uninstallExtension(extension.pkgName)
}
},
onClickItemCancel = { extension ->
presenter.cancelInstallUpdateExtension(extension)
},
onClickUpdateAll = {
presenter.updateAllExtensions()
},
onInstallExtension = {
presenter.installExtension(it)
},
onOpenExtension = {
router?.pushController(ExtensionDetailsController(it.pkgName))
},
onTrustExtension = {
presenter.trustSignature(it.signatureHash)
},
onUninstallExtension = {
presenter.uninstallExtension(it.pkgName)
},
onUpdateExtension = {
presenter.updateExtension(it)
},
onRefresh = {
presenter.findAvailableExtensions()
},
)
},
)

View File

@@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
@Composable
fun migrateSourcesTab(
router: Router?,
presenter: MigrationSourcesPresenter,
): BrowseTab {
val uriHandler = LocalUriHandler.current
return BrowseTab(
titleRes = R.string.label_migration,
actions = listOf(
AppBar.Action(
title = stringResource(R.string.migration_help_guide),
icon = Icons.Outlined.HelpOutline,
onClick = {
uriHandler.openUri("https://tachiyomi.org/help/guides/source-migration/")
},
),
),
content = {
MigrateSourceScreen(
presenter = presenter,
onClickItem = { source ->
router?.pushController(
MigrationMangaController(
source.id,
source.name,
),
)
},
)
},
)
}

View File

@@ -1,65 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.tachiyomi.R
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.openInBrowser
class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() {
init {
setHasOptionsMenu(true)
}
override fun createPresenter() = MigrationSourcesPresenter()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
MigrateSourceScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickItem = { source ->
parentController!!.router.pushController(
MigrationMangaController(
source.id,
source.name,
),
)
},
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
inflater.inflate(R.menu.browse_migrate, menu)
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)
}
}
}
private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"

View File

@@ -1,33 +1,35 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.presentation.browse.MigrateSourceState
import eu.kanade.presentation.browse.MigrateSourceStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationSourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state {
) : MigrateSourceState by state {
private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
fun onCreate() {
presenterScope.launchIO {
getSourcesWithFavoriteCount.subscribe()
.catch { exception ->
@@ -39,14 +41,32 @@ class MigrationSourcesPresenter(
state.isLoading = false
}
}
preferences.migrationSortingDirection().asFlow()
.onEach { state.sortingDirection = it }
.launchIn(presenterScope)
preferences.migrationSortingMode().asFlow()
.onEach { state.sortingMode = it }
.launchIn(presenterScope)
}
fun setAlphabeticalSorting(isAscending: Boolean) {
setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending)
fun toggleSortingMode() {
val newMode = when (state.sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL
SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL
}
setMigrateSorting.await(newMode, state.sortingDirection)
}
fun setTotalSorting(isAscending: Boolean) {
setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending)
fun toggleSortingDirection() {
val newDirection = when (state.sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> SetMigrateSorting.Direction.DESCENDING
SetMigrateSorting.Direction.DESCENDING -> SetMigrateSorting.Direction.ASCENDING
}
setMigrateSorting.await(state.sortingMode, newDirection)
}
sealed class Event {

View File

@@ -1,106 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourcesScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.main.MainActivity
import uy.kohesive.injekt.injectLazy
class SourcesController : SearchableComposeController<SourcesPresenter>() {
private val preferences: PreferencesHelper by injectLazy()
init {
setHasOptionsMenu(true)
}
override fun getTitle() = resources?.getString(R.string.label_sources)
override fun createPresenter() = SourcesPresenter()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
SourcesScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickItem = { source ->
openSource(source, BrowseSourceController(source))
},
onClickDisable = { source ->
presenter.toggleSource(source)
},
onClickLatest = { source ->
openSource(source, LatestUpdatesController(source))
},
onClickPin = { source ->
presenter.togglePin(source)
},
)
LaunchedEffect(Unit) {
(activity as? MainActivity)?.ready = true
}
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
}
/**
* Opens a catalogue with the given controller.
*/
private fun openSource(source: Source, controller: BrowseSourceController) {
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
parentController!!.router.pushController(controller)
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
parentController!!.router.pushController(SourceFilterController())
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(
menu,
inflater,
R.menu.browse_sources,
R.id.action_search,
R.string.action_global_search_hint,
false, // GlobalSearch handles the searching here
)
}
override fun onSearchViewQueryTextSubmit(query: String?) {
parentController!!.router.pushController(GlobalSearchController(query))
}
}

View File

@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.browse.source
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -9,9 +8,10 @@ import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourceUiModel
import eu.kanade.presentation.browse.SourcesState
import eu.kanade.presentation.browse.SourcesStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
@@ -22,17 +22,18 @@ import uy.kohesive.injekt.api.get
import java.util.TreeMap
class SourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : BasePresenter<SourcesController>(), SourcesState by state {
) : SourcesState by state {
private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
fun onCreate() {
presenterScope.launchIO {
getEnabledSources.subscribe()
.catch { exception ->
@@ -76,6 +77,12 @@ class SourcesPresenter(
state.items = uiModels
}
fun onOpenSource(source: Source) {
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
}
fun toggleSource(source: Source) {
toggleSource.await(source)
}

View File

@@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.TravelExplore
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.SourcesScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
@Composable
fun sourcesTab(
router: Router?,
presenter: SourcesPresenter,
) = BrowseTab(
titleRes = R.string.label_sources,
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_global_search),
icon = Icons.Outlined.TravelExplore,
onClick = { router?.pushController(GlobalSearchController()) },
),
AppBar.Action(
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList,
onClick = { router?.pushController(SourceFilterController()) },
),
),
content = {
SourcesScreen(
presenter = presenter,
onClickItem = { source ->
presenter.onOpenSource(source)
router?.pushController(BrowseSourceController(source))
},
onClickDisable = { source ->
presenter.toggleSource(source)
},
onClickLatest = { source ->
presenter.onOpenSource(source)
router?.pushController(LatestUpdatesController(source))
},
onClickPin = { source ->
presenter.togglePin(source)
},
)
},
)

View File

@@ -45,7 +45,6 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.setRoot
import eu.kanade.tachiyomi.ui.browse.BrowseController
@@ -162,7 +161,7 @@ class MainActivity : BaseActivity() {
R.id.nav_library -> router.setRoot(LibraryController(), id)
R.id.nav_updates -> router.setRoot(UpdatesController(), id)
R.id.nav_history -> router.setRoot(HistoryController(), id)
R.id.nav_browse -> router.setRoot(BrowseController(), id)
R.id.nav_browse -> router.setRoot(BrowseController(toExtensions = false), id)
R.id.nav_more -> router.setRoot(MoreController(), id)
}
} else if (!isHandlingShortcut) {
@@ -590,17 +589,6 @@ class MainActivity : BaseActivity() {
showNav(true)
}
if (from is TabbedController) {
from.cleanupTabs(binding.tabs)
}
if (internalTo is TabbedController) {
if (internalTo.configureTabs(binding.tabs)) {
binding.tabs.isVisible = true
}
} else {
binding.tabs.isVisible = false
}
if (from is FabController) {
from.cleanupFab(binding.fabLayout.rootFab)
}