mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 12:08:56 +01:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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/"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user