diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt b/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt index c93c56ee1..2b74b6180 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt @@ -6,10 +6,9 @@ class SetMigrateSorting( private val preferences: PreferencesHelper, ) { - fun await(mode: Mode, isAscending: Boolean) { - val direction = if (isAscending) Direction.ASCENDING else Direction.DESCENDING - preferences.migrationSortingDirection().set(direction) + fun await(mode: Mode, direction: Direction) { preferences.migrationSortingMode().set(mode) + preferences.migrationSortingDirection().set(direction) } enum class Mode { diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseScreen.kt new file mode 100644 index 000000000..4860ac741 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseScreen.kt @@ -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, +) { + 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 = emptyList(), + val content: @Composable () -> Unit, +) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index ed7c36c29..5f4f314f9 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -22,15 +22,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -57,7 +54,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper @Composable fun ExtensionScreen( - nestedScrollInterop: NestedScrollConnection, presenter: ExtensionsPresenter, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, @@ -68,10 +64,8 @@ fun ExtensionScreen( onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, onRefresh: () -> Unit, - onLaunched: () -> Unit, ) { SwipeRefresh( - modifier = Modifier.nestedScroll(nestedScrollInterop), state = rememberSwipeRefreshState(presenter.isRefreshing), indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) }, onRefresh = onRefresh, @@ -90,7 +84,6 @@ fun ExtensionScreen( onTrustExtension = onTrustExtension, onOpenExtension = onOpenExtension, onClickUpdateAll = onClickUpdateAll, - onLaunched = onLaunched, ) } } @@ -108,7 +101,6 @@ fun ExtensionContent( onTrustExtension: (Extension.Untrusted) -> Unit, onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, - onLaunched: () -> Unit, ) { var trustState by remember { mutableStateOf(null) } @@ -187,9 +179,6 @@ fun ExtensionContent( } }, ) - LaunchedEffect(Unit) { - onLaunched() - } } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt index 7e629f90d..7d3271172 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt @@ -10,6 +10,7 @@ interface ExtensionsState { val isLoading: Boolean val isRefreshing: Boolean val items: List + val updates: Int val isEmpty: Boolean } @@ -21,5 +22,6 @@ class ExtensionsStateImpl : ExtensionsState { override var isLoading: Boolean by mutableStateOf(true) override var isRefreshing: Boolean by mutableStateOf(false) override var items: List by mutableStateOf(emptyList()) + override var updates: Int by mutableStateOf(0) override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index d0507f94a..8c18b3e22 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -8,17 +8,17 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.model.Source import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.SourceIcon @@ -39,7 +39,6 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard @Composable fun MigrateSourceScreen( - nestedScrollInterop: NestedScrollConnection, presenter: MigrationSourcesPresenter, onClickItem: (Source) -> Unit, ) { @@ -49,28 +48,44 @@ fun MigrateSourceScreen( presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library) else -> MigrateSourceList( - nestedScrollInterop = nestedScrollInterop, list = presenter.items, onClickItem = onClickItem, onLongClickItem = { source -> val sourceId = source.id.toString() context.copyToClipboard(sourceId, sourceId) }, + sortingMode = presenter.sortingMode, + onToggleSortingMode = { presenter.toggleSortingMode() }, + sortingDirection = presenter.sortingDirection, + onToggleSortingDirection = { presenter.toggleSortingDirection() }, ) } } @Composable fun MigrateSourceList( - nestedScrollInterop: NestedScrollConnection, list: List>, onClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit, + sortingMode: SetMigrateSorting.Mode, + onToggleSortingMode: () -> Unit, + sortingDirection: SetMigrateSorting.Direction, + onToggleSortingDirection: () -> Unit, ) { ScrollbarLazyColumn( - modifier = Modifier.nestedScroll(nestedScrollInterop), contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, ) { + stickyHeader { + Row { + Button(onClick = onToggleSortingMode) { + Text(sortingMode.toString()) + } + Button(onClick = onToggleSortingDirection) { + Text(sortingDirection.toString()) + } + } + } + item(key = "title") { Text( text = stringResource(R.string.migration_selection_prompt), diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt index 2a7f4cc9b..c5d9f1f5f 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt @@ -4,12 +4,15 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.model.Source interface MigrateSourceState { val isLoading: Boolean val items: List> val isEmpty: Boolean + val sortingMode: SetMigrateSorting.Mode + val sortingDirection: SetMigrateSorting.Direction } fun MigrateSourceState(): MigrateSourceState { @@ -20,4 +23,6 @@ class MigrateSourceStateImpl : MigrateSourceState { override var isLoading: Boolean by mutableStateOf(true) override var items: List> by mutableStateOf(emptyList()) 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) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index aeb210a83..e124f53e7 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -21,8 +21,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.source.LocalSource 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.toast import kotlinx.coroutines.flow.collectLatest @Composable fun SourcesScreen( - nestedScrollInterop: NestedScrollConnection, presenter: SourcesPresenter, onClickItem: (Source) -> Unit, onClickDisable: (Source) -> Unit, @@ -60,7 +56,6 @@ fun SourcesScreen( presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen) else -> { SourceList( - nestedScrollConnection = nestedScrollInterop, state = presenter, onClickItem = onClickItem, onClickDisable = onClickDisable, @@ -82,7 +77,6 @@ fun SourcesScreen( @Composable fun SourceList( - nestedScrollConnection: NestedScrollConnection, state: SourcesState, onClickItem: (Source) -> Unit, onClickDisable: (Source) -> Unit, @@ -90,7 +84,6 @@ fun SourceList( onClickPin: (Source) -> Unit, ) { ScrollbarLazyColumn( - modifier = Modifier.nestedScroll(nestedScrollConnection), contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, ) { items( @@ -119,7 +112,7 @@ fun SourceList( modifier = Modifier.animateItemPlacement(), source = model.source, onClickItem = onClickItem, - onLongClickItem = { state.dialog = Dialog(it) }, + onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) }, onClickLatest = onClickLatest, onClickPin = onClickPin, ) diff --git a/app/src/main/java/eu/kanade/presentation/components/Tabs.kt b/app/src/main/java/eu/kanade/presentation/components/Tabs.kt new file mode 100644 index 000000000..7ccc5d7bb --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/Tabs.kt @@ -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, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt index 43a84226b..388f96917 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -2,31 +2,22 @@ package eu.kanade.presentation.library.components import androidx.compose.foundation.isSystemInDarkTheme 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.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.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.sp import com.google.accompanist.pager.PagerState import eu.kanade.domain.category.model.Category import eu.kanade.presentation.category.visualName import eu.kanade.presentation.components.DownloadedOnlyModeBanner 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 @Composable @@ -46,13 +37,7 @@ fun LibraryTabs( ScrollableTabRow( selectedTabIndex = state.currentPage, edgePadding = 0.dp, - indicator = { tabPositions -> - TabRowDefaults.Indicator( - Modifier - .tabIndicatorOffset(tabPositions[state.currentPage]) - .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)), - ) - }, + indicator = { TabIndicator(it[state.currentPage]) }, ) { categories.forEachIndexed { index, category -> val count by if (showMangaCount) { @@ -64,21 +49,7 @@ fun LibraryTabs( selected = state.currentPage == index, onClick = { scope.launch { state.animateScrollToPage(index) } }, text = { - Row( - 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, - ) - } - } + TabText(category.visualName, count, state.currentPage == index) }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt index c057693e2..41237b339 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt @@ -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

>(bundle: Bundle? = null) : } } -/** - * Compose controller with a Nucleus presenter. - */ -abstract class ComposeController

>(bundle: Bundle? = null) : - NucleusController(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(), +abstract class BasicFullComposeController(bundle: Bundle? = null) : + BaseController(bundle), FullComposeContentController { override fun createBinding(inflater: LayoutInflater) = @@ -72,29 +47,6 @@ abstract class BasicFullComposeController : } } -abstract class SearchableComposeController

>(bundle: Bundle? = null) : - SearchableNucleusController(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) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt deleted file mode 100644 index 6cf26780c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt +++ /dev/null @@ -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) {} -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt index fda117aa8..ecd003562 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt @@ -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, - RootController, - TabbedController { +class BrowseController : FullComposeController, 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 = 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt new file mode 100644 index 000000000..7ced73592 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt @@ -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() { + + 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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsController.kt deleted file mode 100644 index abb33f7ad..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsController.kt +++ /dev/null @@ -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() { - - 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt index 42dd5222b..e270d4dc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt @@ -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(), ExtensionsState by state { +) : ExtensionsState by state { private val _query: MutableStateFlow = MutableStateFlow("") private var _currentDownloads = MutableStateFlow>(hashMapOf()) - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - + fun onCreate() { val context = Injekt.get() val extensionMapper: (Map) -> ((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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt new file mode 100644 index 000000000..081296460 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -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() + }, + ) + }, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt new file mode 100644 index 000000000..123144c6d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt @@ -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, + ), + ) + }, + ) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt deleted file mode 100644 index dde0a1a76..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt +++ /dev/null @@ -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() { - - 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/" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt index 6beb37980..c8754b43d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt @@ -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(), MigrateSourceState by state { +) : MigrateSourceState by state { private val _channel = Channel(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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt deleted file mode 100644 index 9de8945a3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt +++ /dev/null @@ -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() { - - 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)) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt index c26c748ad..a9e09a15b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt @@ -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(), SourcesState by state { +) : SourcesState by state { private val _events = Channel(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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt new file mode 100644 index 000000000..1ffefc3b6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt @@ -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) + }, + ) + }, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 88af188f4..f177d0446 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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) } diff --git a/app/src/main/res/drawable/ic_sort_24dp.xml b/app/src/main/res/drawable/ic_sort_24dp.xml deleted file mode 100644 index 6debd967f..000000000 --- a/app/src/main/res/drawable/ic_sort_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_translate_24dp.xml b/app/src/main/res/drawable/ic_translate_24dp.xml deleted file mode 100644 index e7c9bf411..000000000 --- a/app/src/main/res/drawable/ic_translate_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_travel_explore_24dp.xml b/app/src/main/res/drawable/ic_travel_explore_24dp.xml deleted file mode 100644 index b4e08d92b..000000000 --- a/app/src/main/res/drawable/ic_travel_explore_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout-sw720dp/main_activity.xml b/app/src/main/res/layout-sw720dp/main_activity.xml index 2c0f60db2..c4bee8733 100644 --- a/app/src/main/res/layout-sw720dp/main_activity.xml +++ b/app/src/main/res/layout-sw720dp/main_activity.xml @@ -26,11 +26,6 @@ android:layout_height="?attr/actionBarSize" android:theme="?attr/actionBarTheme" /> - - - - - - - - - - diff --git a/app/src/main/res/menu/browse_migrate.xml b/app/src/main/res/menu/browse_migrate.xml deleted file mode 100644 index 26455a0f4..000000000 --- a/app/src/main/res/menu/browse_migrate.xml +++ /dev/null @@ -1,47 +0,0 @@ -

- - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/browse_sources.xml b/app/src/main/res/menu/browse_sources.xml deleted file mode 100644 index 317e0f4f1..000000000 --- a/app/src/main/res/menu/browse_sources.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82d1c34d1..0e1f1245b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,6 @@ directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" insetter = "dev.chrisbanes.insetter:insetter:0.6.1" 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" } 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"] coil = ["coil-core", "coil-gif", "coil-compose"] flowbinding = ["flowbinding-android", "flowbinding-appcompat"] -conductor = ["conductor-core", "conductor-viewpager", "conductor-support-preference"] +conductor = ["conductor-core", "conductor-support-preference"] shizuku = ["shizuku-api", "shizuku-provider"] [plugins]