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

@ -6,10 +6,9 @@ class SetMigrateSorting(
private val preferences: PreferencesHelper, private val preferences: PreferencesHelper,
) { ) {
fun await(mode: Mode, isAscending: Boolean) { fun await(mode: Mode, direction: Direction) {
val direction = if (isAscending) Direction.ASCENDING else Direction.DESCENDING
preferences.migrationSortingDirection().set(direction)
preferences.migrationSortingMode().set(mode) preferences.migrationSortingMode().set(mode)
preferences.migrationSortingDirection().set(direction)
} }
enum class Mode { enum class Mode {

View file

@ -0,0 +1,84 @@
package eu.kanade.presentation.browse
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
@Composable
fun BrowseScreen(
startIndex: Int? = null,
tabs: List<BrowseTab>,
) {
val scope = rememberCoroutineScope()
val state = rememberPagerState()
LaunchedEffect(startIndex) {
if (startIndex != null) {
state.scrollToPage(startIndex)
}
}
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.browse),
actions = {
AppBarActions(tabs[state.currentPage].actions)
},
)
},
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
TabRow(
selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage]) },
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
TabText(stringResource(tab.titleRes), tab.badgeNumber, state.currentPage == index)
},
)
}
}
HorizontalPager(
count = tabs.size,
modifier = Modifier.fillMaxSize(),
state = state,
verticalAlignment = Alignment.Top,
) { page ->
tabs[page].content()
}
}
}
}
data class BrowseTab(
@StringRes val titleRes: Int,
val badgeNumber: Int? = null,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable () -> Unit,
)

View file

@ -22,15 +22,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -57,7 +54,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable @Composable
fun ExtensionScreen( fun ExtensionScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionsPresenter, presenter: ExtensionsPresenter,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
@ -68,10 +64,8 @@ fun ExtensionScreen(
onOpenExtension: (Extension.Installed) -> Unit, onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onLaunched: () -> Unit,
) { ) {
SwipeRefresh( SwipeRefresh(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = rememberSwipeRefreshState(presenter.isRefreshing), state = rememberSwipeRefreshState(presenter.isRefreshing),
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) }, indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
onRefresh = onRefresh, onRefresh = onRefresh,
@ -90,7 +84,6 @@ fun ExtensionScreen(
onTrustExtension = onTrustExtension, onTrustExtension = onTrustExtension,
onOpenExtension = onOpenExtension, onOpenExtension = onOpenExtension,
onClickUpdateAll = onClickUpdateAll, onClickUpdateAll = onClickUpdateAll,
onLaunched = onLaunched,
) )
} }
} }
@ -108,7 +101,6 @@ fun ExtensionContent(
onTrustExtension: (Extension.Untrusted) -> Unit, onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit, onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
onLaunched: () -> Unit,
) { ) {
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) } var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
@ -187,9 +179,6 @@ fun ExtensionContent(
} }
}, },
) )
LaunchedEffect(Unit) {
onLaunched()
}
} }
} }
} }

View file

@ -10,6 +10,7 @@ interface ExtensionsState {
val isLoading: Boolean val isLoading: Boolean
val isRefreshing: Boolean val isRefreshing: Boolean
val items: List<ExtensionUiModel> val items: List<ExtensionUiModel>
val updates: Int
val isEmpty: Boolean val isEmpty: Boolean
} }
@ -21,5 +22,6 @@ class ExtensionsStateImpl : ExtensionsState {
override var isLoading: Boolean by mutableStateOf(true) override var isLoading: Boolean by mutableStateOf(true)
override var isRefreshing: Boolean by mutableStateOf(false) override var isRefreshing: Boolean by mutableStateOf(false)
override var items: List<ExtensionUiModel> by mutableStateOf(emptyList()) override var items: List<ExtensionUiModel> by mutableStateOf(emptyList())
override var updates: Int by mutableStateOf(0)
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
} }

View file

@ -8,17 +8,17 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment 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.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
@ -39,7 +39,6 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable @Composable
fun MigrateSourceScreen( fun MigrateSourceScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: MigrationSourcesPresenter, presenter: MigrationSourcesPresenter,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
) { ) {
@ -49,28 +48,44 @@ fun MigrateSourceScreen(
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library) presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library)
else -> else ->
MigrateSourceList( MigrateSourceList(
nestedScrollInterop = nestedScrollInterop,
list = presenter.items, list = presenter.items,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { source -> onLongClickItem = { source ->
val sourceId = source.id.toString() val sourceId = source.id.toString()
context.copyToClipboard(sourceId, sourceId) context.copyToClipboard(sourceId, sourceId)
}, },
sortingMode = presenter.sortingMode,
onToggleSortingMode = { presenter.toggleSortingMode() },
sortingDirection = presenter.sortingDirection,
onToggleSortingDirection = { presenter.toggleSortingDirection() },
) )
} }
} }
@Composable @Composable
fun MigrateSourceList( fun MigrateSourceList(
nestedScrollInterop: NestedScrollConnection,
list: List<Pair<Source, Long>>, list: List<Pair<Source, Long>>,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
sortingMode: SetMigrateSorting.Mode,
onToggleSortingMode: () -> Unit,
sortingDirection: SetMigrateSorting.Direction,
onToggleSortingDirection: () -> Unit,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
stickyHeader {
Row {
Button(onClick = onToggleSortingMode) {
Text(sortingMode.toString())
}
Button(onClick = onToggleSortingDirection) {
Text(sortingDirection.toString())
}
}
}
item(key = "title") { item(key = "title") {
Text( Text(
text = stringResource(R.string.migration_selection_prompt), text = stringResource(R.string.migration_selection_prompt),

View file

@ -4,12 +4,15 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
interface MigrateSourceState { interface MigrateSourceState {
val isLoading: Boolean val isLoading: Boolean
val items: List<Pair<Source, Long>> val items: List<Pair<Source, Long>>
val isEmpty: Boolean val isEmpty: Boolean
val sortingMode: SetMigrateSorting.Mode
val sortingDirection: SetMigrateSorting.Direction
} }
fun MigrateSourceState(): MigrateSourceState { fun MigrateSourceState(): MigrateSourceState {
@ -20,4 +23,6 @@ class MigrateSourceStateImpl : MigrateSourceState {
override var isLoading: Boolean by mutableStateOf(true) override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList()) override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
override var sortingMode: SetMigrateSorting.Mode by mutableStateOf(SetMigrateSorting.Mode.ALPHABETICAL)
override var sortingDirection: SetMigrateSorting.Direction by mutableStateOf(SetMigrateSorting.Direction.ASCENDING)
} }

View file

@ -21,8 +21,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -40,14 +38,12 @@ import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter.Dialog
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesScreen( fun SourcesScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: SourcesPresenter, presenter: SourcesPresenter,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
@ -60,7 +56,6 @@ fun SourcesScreen(
presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen) presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
else -> { else -> {
SourceList( SourceList(
nestedScrollConnection = nestedScrollInterop,
state = presenter, state = presenter,
onClickItem = onClickItem, onClickItem = onClickItem,
onClickDisable = onClickDisable, onClickDisable = onClickDisable,
@ -82,7 +77,6 @@ fun SourcesScreen(
@Composable @Composable
fun SourceList( fun SourceList(
nestedScrollConnection: NestedScrollConnection,
state: SourcesState, state: SourcesState,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
@ -90,7 +84,6 @@ fun SourceList(
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
items( items(
@ -119,7 +112,7 @@ fun SourceList(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
source = model.source, source = model.source,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { state.dialog = Dialog(it) }, onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
onClickLatest = onClickLatest, onClickLatest = onClickLatest,
onClickPin = onClickPin, onClickPin = onClickPin,
) )

View file

@ -0,0 +1,50 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun TabIndicator(currentTabPosition: TabPosition) {
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(currentTabPosition)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
)
}
@Composable
fun TabText(
text: String,
badgeCount: Int? = null,
isCurrentPage: Boolean,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = text,
color = if (isCurrentPage) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
)
if (badgeCount != null) {
Pill(
text = "$badgeCount",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
}

View file

@ -2,31 +2,22 @@ package eu.kanade.presentation.library.components
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
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.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.DownloadedOnlyModeBanner import eu.kanade.presentation.components.DownloadedOnlyModeBanner
import eu.kanade.presentation.components.IncognitoModeBanner import eu.kanade.presentation.components.IncognitoModeBanner
import eu.kanade.presentation.components.Pill import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -46,13 +37,7 @@ fun LibraryTabs(
ScrollableTabRow( ScrollableTabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = state.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,
indicator = { tabPositions -> indicator = { TabIndicator(it[state.currentPage]) },
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[state.currentPage])
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
)
},
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
val count by if (showMangaCount) { val count by if (showMangaCount) {
@ -64,21 +49,7 @@ fun LibraryTabs(
selected = state.currentPage == index, selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } }, onClick = { scope.launch { state.animateScrollToPage(index) } },
text = { text = {
Row( TabText(category.visualName, count, state.currentPage == index)
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = category.visualName,
color = if (state.currentPage == index) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
)
if (count != null) {
Pill(
text = "$count",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
}, },
) )
} }

View file

@ -4,10 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.compose.runtime.Composable 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.databinding.ComposeControllerBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import nucleus.presenter.Presenter 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. * Basic Compose controller without a presenter.
*/ */
abstract class BasicFullComposeController : abstract class BasicFullComposeController(bundle: Bundle? = null) :
BaseController<ComposeControllerBinding>(), BaseController<ComposeControllerBinding>(bundle),
FullComposeContentController { FullComposeContentController {
override fun createBinding(inflater: LayoutInflater) = 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 { interface FullComposeContentController {
@Composable fun ComposeContent() @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 package eu.kanade.tachiyomi.ui.browse
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.Controller import eu.kanade.presentation.browse.BrowseScreen
import com.bluelinelabs.conductor.ControllerChangeHandler import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
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.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.RxController import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsController import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourcesTab
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import uy.kohesive.injekt.injectLazy
class BrowseController : class BrowseController : FullComposeController<BrowsePresenter>, RootController {
RxController<PagerControllerBinding>,
RootController, @Suppress("unused")
TabbedController { constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false)
constructor(toExtensions: Boolean = false) : super( constructor(toExtensions: Boolean = false) : super(
bundleOf(TO_EXTENSIONS_EXTRA to toExtensions), 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) 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? { LaunchedEffect(Unit) {
return resources!!.getString(R.string.browse) (activity as? MainActivity)?.ready = true
}
} }
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
requestPermissionsSafe(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 301)
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
} }
} }
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 package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import android.os.Bundle
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.domain.extension.interactor.GetExtensionsByType import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.presentation.browse.ExtensionState import eu.kanade.presentation.browse.ExtensionState
import eu.kanade.presentation.browse.ExtensionsState import eu.kanade.presentation.browse.ExtensionsState
import eu.kanade.presentation.browse.ExtensionsStateImpl import eu.kanade.presentation.browse.ExtensionsStateImpl
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.online.HttpSource 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.lang.launchIO
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionsPresenter( class ExtensionsPresenter(
private val presenterScope: CoroutineScope,
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl, private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(), private val getExtensions: GetExtensionsByType = Injekt.get(),
) : BasePresenter<ExtensionsController>(), ExtensionsState by state { ) : ExtensionsState by state {
private val _query: MutableStateFlow<String> = MutableStateFlow("") private val _query: MutableStateFlow<String> = MutableStateFlow("")
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
override fun onCreate(savedState: Bundle?) { fun onCreate() {
super.onCreate(savedState)
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map -> val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map ->
{ {
@ -114,6 +116,10 @@ class ExtensionsPresenter(
} }
presenterScope.launchIO { findAvailableExtensions() } presenterScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount().asFlow()
.onEach { state.updates = it }
.launchIn(presenterScope)
} }
fun search(query: String) { 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 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.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.presentation.browse.MigrateSourceState import eu.kanade.presentation.browse.MigrateSourceState
import eu.kanade.presentation.browse.MigrateSourceStateImpl 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.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrationSourcesPresenter( class MigrationSourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl, private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state { ) : MigrateSourceState by state {
private val _channel = Channel<Event>(Int.MAX_VALUE) private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow() val channel = _channel.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { fun onCreate() {
super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getSourcesWithFavoriteCount.subscribe() getSourcesWithFavoriteCount.subscribe()
.catch { exception -> .catch { exception ->
@ -39,14 +41,32 @@ class MigrationSourcesPresenter(
state.isLoading = false 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) { fun toggleSortingMode() {
setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending) val newMode = when (state.sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL
SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL
} }
fun setTotalSorting(isAscending: Boolean) { setMigrateSorting.await(newMode, state.sortingDirection)
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 { 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 package eu.kanade.tachiyomi.ui.browse.source
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin 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.SourceUiModel
import eu.kanade.presentation.browse.SourcesState import eu.kanade.presentation.browse.SourcesState
import eu.kanade.presentation.browse.SourcesStateImpl 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.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -22,17 +22,18 @@ import uy.kohesive.injekt.api.get
import java.util.TreeMap import java.util.TreeMap
class SourcesPresenter( class SourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl, private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val getEnabledSources: GetEnabledSources = Injekt.get(), private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : BasePresenter<SourcesController>(), SourcesState by state { ) : SourcesState by state {
private val _events = Channel<Event>(Int.MAX_VALUE) private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { fun onCreate() {
super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getEnabledSources.subscribe() getEnabledSources.subscribe()
.catch { exception -> .catch { exception ->
@ -76,6 +77,12 @@ class SourcesPresenter(
state.items = uiModels state.items = uiModels
} }
fun onOpenSource(source: Source) {
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
}
fun toggleSource(source: Source) { fun toggleSource(source: Source) {
toggleSource.await(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.FabController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController
import eu.kanade.tachiyomi.ui.base.controller.RootController 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.pushController
import eu.kanade.tachiyomi.ui.base.controller.setRoot import eu.kanade.tachiyomi.ui.base.controller.setRoot
import eu.kanade.tachiyomi.ui.browse.BrowseController 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_library -> router.setRoot(LibraryController(), id)
R.id.nav_updates -> router.setRoot(UpdatesController(), id) R.id.nav_updates -> router.setRoot(UpdatesController(), id)
R.id.nav_history -> router.setRoot(HistoryController(), 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) R.id.nav_more -> router.setRoot(MoreController(), id)
} }
} else if (!isHandlingShortcut) { } else if (!isHandlingShortcut) {
@ -590,17 +589,6 @@ class MainActivity : BaseActivity() {
showNav(true) 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) { if (from is FabController) {
from.cleanupFab(binding.fabLayout.rootFab) from.cleanupFab(binding.fabLayout.rootFab)
} }

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M19.3,16.9c0.4,-0.7 0.7,-1.5 0.7,-2.4c0,-2.5 -2,-4.5 -4.5,-4.5S11,12 11,14.5s2,4.5 4.5,4.5c0.9,0 1.7,-0.3 2.4,-0.7l3.2,3.2l1.4,-1.4L19.3,16.9zM15.5,17c-1.4,0 -2.5,-1.1 -2.5,-2.5s1.1,-2.5 2.5,-2.5s2.5,1.1 2.5,2.5S16.9,17 15.5,17zM12,20v2C6.48,22 2,17.52 2,12C2,6.48 6.48,2 12,2c4.84,0 8.87,3.44 9.8,8h-2.07c-0.64,-2.46 -2.4,-4.47 -4.73,-5.41V5c0,1.1 -0.9,2 -2,2h-2v2c0,0.55 -0.45,1 -1,1H8v2h2v3H9l-4.79,-4.79C4.08,10.79 4,11.38 4,12C4,16.41 7.59,20 12,20z"/>
</vector>

View file

@ -26,11 +26,6 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" /> android:theme="?attr/actionBarTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView <TextView
android:id="@+id/downloaded_only" android:id="@+id/downloaded_only"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -25,12 +25,6 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" /> android:theme="?attr/actionBarTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/downloaded_only" android:id="@+id/downloaded_only"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -1,19 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_translate_24dp"
android:title="@string/action_filter"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -1,47 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_sort"
android:icon="@drawable/ic_sort_24dp"
android:title="@string/action_sort"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" >
<menu>
<item
android:id="@+id/action_sort_alphabetical"
android:title="@string/action_sort_alpha"
app:showAsAction="never">
<menu>
<item
android:id="@+id/asc_alphabetical"
android:title="@string/action_asc" />
<item
android:id="@+id/desc_alphabetical"
android:title="@string/action_desc" />
</menu>
</item>
<item
android:id="@+id/action_sort_count"
android:title="@string/action_sort_count"
app:showAsAction="never">
<menu>
<item
android:id="@+id/asc_count"
android:title="@string/action_asc" />
<item
android:id="@+id/desc_count"
android:title="@string/action_desc" />
</menu>
</item>
</menu>
</item>
<item
android:id="@+id/action_source_migration_help"
android:icon="@drawable/ic_help_24dp"
android:title="@string/migration_help_guide"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -1,19 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_travel_explore_24dp"
android:title="@string/action_global_search"
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_filter_list_24dp"
android:title="@string/action_filter"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -64,7 +64,6 @@ directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1" insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" } conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" }
conductor-viewpager = { module = "com.bluelinelabs:conductor-viewpager", version.ref = "conductor_version" }
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" }
flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding_version" } flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding_version" }
@ -99,7 +98,7 @@ sqlite = ["sqlitektx", "sqlite-android"]
nucleus = ["nucleus-core", "nucleus-supportv7"] nucleus = ["nucleus-core", "nucleus-supportv7"]
coil = ["coil-core", "coil-gif", "coil-compose"] coil = ["coil-core", "coil-gif", "coil-compose"]
flowbinding = ["flowbinding-android", "flowbinding-appcompat"] flowbinding = ["flowbinding-android", "flowbinding-appcompat"]
conductor = ["conductor-core", "conductor-viewpager", "conductor-support-preference"] conductor = ["conductor-core", "conductor-support-preference"]
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
[plugins] [plugins]