From 8c182df784c739d011faef0cd6abe71b2c68ccc5 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 29 Aug 2022 17:18:06 -0400 Subject: [PATCH] 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 (cherry picked from commit 92e83f702c775d5ab5a0b4248995308155150811) # Conflicts: # app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt # app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt # app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt --- .../source/interactor/SetMigrateSorting.kt | 5 +- .../presentation/browse/BrowseScreen.kt | 84 ++++++++ .../presentation/browse/BrowseTabWrapper.kt | 30 +++ .../presentation/browse/ExtensionsScreen.kt | 11 - .../presentation/browse/ExtensionsState.kt | 2 + .../kanade/presentation/browse/FeedScreen.kt | 172 +++++++++++++++- .../kanade/presentation/browse/FeedState.kt | 3 + .../browse/MigrateSourceScreen.kt | 27 ++- .../presentation/browse/MigrateSourceState.kt | 5 + .../presentation/browse/SourcesScreen.kt | 6 - .../eu/kanade/presentation/components/Tabs.kt | 50 +++++ .../library/components/LibraryTabs.kt | 45 +---- .../ui/base/controller/ComposeController.kt | 52 +---- .../ui/base/controller/TabbedController.kt | 13 -- .../tachiyomi/ui/browse/BrowseController.kt | 191 ++++-------------- .../tachiyomi/ui/browse/BrowsePresenter.kt | 49 +++++ .../browse/extension/ExtensionsController.kt | 120 ----------- .../browse/extension/ExtensionsPresenter.kt | 18 +- .../ui/browse/extension/ExtensionsTab.kt | 75 +++++++ .../ui/browse/feed/FeedController.kt | 144 ------------- .../tachiyomi/ui/browse/feed/FeedPresenter.kt | 34 +++- .../tachiyomi/ui/browse/feed/FeedTab.kt | 60 ++++++ .../migration/sources/MigrateSourcesTab.kt | 74 +++++++ .../sources/MigrationSourcesController.kt | 95 +-------- .../sources/MigrationSourcesPresenter.kt | 40 +++- .../MigrationSourcesPresenterWrapper.kt | 14 ++ .../ui/browse/source/SourcesController.kt | 121 +---------- .../ui/browse/source/SourcesPresenter.kt | 22 +- .../browse/source/SourcesPresenterWrapper.kt | 14 ++ .../tachiyomi/ui/browse/source/SourcesTab.kt | 80 ++++++++ .../kanade/tachiyomi/ui/main/MainActivity.kt | 14 +- .../java/exh/debug/SettingsDebugController.kt | 2 +- app/src/main/res/drawable/ic_sort_24dp.xml | 10 - .../main/res/drawable/ic_translate_24dp.xml | 9 - .../res/drawable/ic_travel_explore_24dp.xml | 9 - .../main/res/layout-sw720dp/main_activity.xml | 5 - app/src/main/res/layout/main_activity.xml | 6 - app/src/main/res/menu/browse_extensions.xml | 19 -- app/src/main/res/menu/browse_migrate.xml | 47 ----- app/src/main/res/menu/browse_sources.xml | 19 -- gradle/libs.versions.toml | 3 +- 41 files changed, 879 insertions(+), 920 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseTabWrapper.kt create mode 100644 app/src/main/java/eu/kanade/presentation/components/Tabs.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenterWrapper.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenterWrapper.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt delete mode 100644 app/src/main/res/drawable/ic_sort_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_translate_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_travel_explore_24dp.xml delete mode 100644 app/src/main/res/menu/browse_extensions.xml delete mode 100644 app/src/main/res/menu/browse_migrate.xml delete mode 100644 app/src/main/res/menu/browse_sources.xml 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/BrowseTabWrapper.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseTabWrapper.kt new file mode 100644 index 000000000..06dcc7805 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseTabWrapper.kt @@ -0,0 +1,30 @@ +package eu.kanade.presentation.browse + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.components.Scaffold + +@Composable +fun BrowseTabWrapper(tab: BrowseTab) { + Scaffold( + modifier = Modifier.statusBarsPadding(), + topBar = { + AppBar( + title = stringResource(tab.titleRes), + actions = { + AppBarActions(tab.actions) + }, + ) + }, + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + tab.content() + } + } +} 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 5f9e800d7..40f1dee6c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -23,15 +23,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 @@ -60,7 +57,6 @@ import exh.source.anyIs @Composable fun ExtensionScreen( - nestedScrollInterop: NestedScrollConnection, presenter: ExtensionsPresenter, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, @@ -71,10 +67,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, @@ -93,7 +87,6 @@ fun ExtensionScreen( onTrustExtension = onTrustExtension, onOpenExtension = onOpenExtension, onClickUpdateAll = onClickUpdateAll, - onLaunched = onLaunched, ) } } @@ -111,7 +104,6 @@ fun ExtensionContent( onTrustExtension: (Extension.Untrusted) -> Unit, onOpenExtension: (Extension.Installed) -> Unit, onClickUpdateAll: () -> Unit, - onLaunched: () -> Unit, ) { var trustState by remember { mutableStateOf(null) } @@ -190,9 +182,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/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index b6193402e..b5dc55ae7 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -7,29 +7,39 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.ContentAlpha +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +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.draw.alpha import androidx.compose.ui.draw.clip -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.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -62,11 +72,13 @@ data class FeedItemUI( @Composable fun FeedScreen( - nestedScrollInterop: NestedScrollConnection, presenter: FeedPresenter, + onClickAdd: (CatalogueSource) -> Unit, + onClickCreate: (CatalogueSource, SavedSearch?) -> Unit, onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, + onClickDeleteConfirm: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, ) { when { @@ -74,11 +86,13 @@ fun FeedScreen( presenter.isEmpty -> EmptyScreen(R.string.feed_tab_empty) else -> { FeedList( - nestedScrollConnection = nestedScrollInterop, state = presenter, + onClickAdd = onClickAdd, + onClickCreate = onClickCreate, onClickSavedSearch = onClickSavedSearch, onClickSource = onClickSource, onClickDelete = onClickDelete, + onClickDeleteConfirm = onClickDeleteConfirm, onClickManga = onClickManga, ) } @@ -87,15 +101,16 @@ fun FeedScreen( @Composable fun FeedList( - nestedScrollConnection: NestedScrollConnection, state: FeedState, + onClickAdd: (CatalogueSource) -> Unit, + onClickCreate: (CatalogueSource, SavedSearch?) -> Unit, onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, + onClickDeleteConfirm: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, ) { ScrollbarLazyColumn( - modifier = Modifier.nestedScroll(nestedScrollConnection), contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, ) { items( @@ -112,6 +127,41 @@ fun FeedList( ) } } + + when (val dialog = state.dialog) { + is FeedPresenter.Dialog.AddFeed -> { + FeedAddDialog( + sources = dialog.options, + onDismiss = { state.dialog = null }, + onClickAdd = { + state.dialog = null + onClickAdd(it ?: return@FeedAddDialog) + }, + ) + } + is FeedPresenter.Dialog.AddFeedSearch -> { + FeedAddSearchDialog( + source = dialog.source, + savedSearches = dialog.options, + onDismiss = { state.dialog = null }, + onClickAdd = { source, savedSearch -> + state.dialog = null + onClickCreate(source, savedSearch) + }, + ) + } + is FeedPresenter.Dialog.DeleteFeed -> { + FeedDeleteConfirmDialog( + feed = dialog.feed, + onDismiss = { state.dialog = null }, + onClickDeleteConfirm = { + state.dialog = null + onClickDeleteConfirm(it) + }, + ) + } + null -> Unit + } } @Composable @@ -207,7 +257,8 @@ fun FeedCardItem( .aspectRatio(MangaCover.Book.ratio), ) { MangaCover.Book( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .alpha( if (manga.favorite) 0.3f else 1.0f, ), @@ -239,3 +290,110 @@ fun FeedCardItem( ) } } + +@Composable +fun FeedAddDialog( + sources: List, + onDismiss: () -> Unit, + onClickAdd: (CatalogueSource?) -> Unit, +) { + var selected by remember { mutableStateOf(null) } + AlertDialog( + title = { + Text(text = stringResource(R.string.feed)) + }, + text = { + RadioSelector(options = sources, selected = selected) { + selected = it + } + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { onClickAdd(selected?.let { sources[it] }) }) { + Text(text = stringResource(android.R.string.ok)) + } + }, + ) +} + +@Composable +fun FeedAddSearchDialog( + source: CatalogueSource, + savedSearches: List, + onDismiss: () -> Unit, + onClickAdd: (CatalogueSource, SavedSearch?) -> Unit, +) { + var selected by remember { mutableStateOf(null) } + AlertDialog( + title = { + Text(text = source.name) + }, + text = { + val context = LocalContext.current + val savedSearchStrings = remember { + savedSearches.map { + it?.name ?: context.getString(R.string.latest) + } + } + RadioSelector( + options = savedSearches, + optionStrings = savedSearchStrings, + selected = selected, + ) { + selected = it + } + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { onClickAdd(source, selected?.let { savedSearches[it] }) }) { + Text(text = stringResource(android.R.string.ok)) + } + }, + ) +} + +@Composable +fun RadioSelector( + options: List, + optionStrings: List = remember { options.map { it.toString() } }, + selected: Int?, + onSelectOption: (Int) -> Unit, +) { + Column(Modifier.verticalScroll(rememberScrollState())) { + optionStrings.forEachIndexed { index, option -> + Row( + Modifier + .fillMaxWidth() + .height(48.dp) + .clickable { onSelectOption(index) }, + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton(selected == index, onClick = null) + Spacer(Modifier.width(4.dp)) + Text(option, maxLines = 1) + } + } + } +} + +@Composable +fun FeedDeleteConfirmDialog( + feed: FeedSavedSearch, + onDismiss: () -> Unit, + onClickDeleteConfirm: (FeedSavedSearch) -> Unit, +) { + AlertDialog( + title = { + Text(text = stringResource(R.string.feed)) + }, + text = { + Text(text = stringResource(R.string.feed_delete)) + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { onClickDeleteConfirm(feed) }) { + Text(text = stringResource(R.string.action_delete)) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedState.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedState.kt index ca421c90c..8d0a93bf6 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedState.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedState.kt @@ -4,9 +4,11 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter @Stable interface FeedState { + var dialog: FeedPresenter.Dialog? val isLoading: Boolean val isEmpty: Boolean val items: List? @@ -17,6 +19,7 @@ fun FeedState(): FeedState { } class FeedStateImpl : FeedState { + override var dialog: FeedPresenter.Dialog? by mutableStateOf(null) override var isLoading: Boolean by mutableStateOf(true) override var isEmpty: Boolean by mutableStateOf(false) override var items: List? by mutableStateOf(null) 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 dc8d7c9c7..92dbed506 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -8,6 +8,7 @@ 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.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -15,12 +16,11 @@ import androidx.compose.material3.TextButton 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 @@ -41,7 +41,6 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard @Composable fun MigrateSourceScreen( - nestedScrollInterop: NestedScrollConnection, presenter: MigrationSourcesPresenter, onClickItem: (Source) -> Unit, // SY --> @@ -54,13 +53,16 @@ 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() }, // SY --> onClickAll = onClickAll, // SY <-- @@ -70,18 +72,31 @@ fun MigrateSourceScreen( @Composable fun MigrateSourceList( - nestedScrollInterop: NestedScrollConnection, list: List>, onClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit, + sortingMode: SetMigrateSorting.Mode, + onToggleSortingMode: () -> Unit, + sortingDirection: SetMigrateSorting.Direction, + onToggleSortingDirection: () -> Unit, // SY --> onClickAll: (Source) -> Unit, // SY <-- ) { 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 3ad348a4b..04b053ddf 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember 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 @@ -51,7 +49,6 @@ import kotlinx.coroutines.flow.collectLatest @Composable fun SourcesScreen( - nestedScrollInterop: NestedScrollConnection, presenter: SourcesPresenter, onClickItem: (Source) -> Unit, onClickDisable: (Source) -> Unit, @@ -66,7 +63,6 @@ fun SourcesScreen( presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen) else -> { SourceList( - nestedScrollConnection = nestedScrollInterop, state = presenter, onClickItem = onClickItem, onClickDisable = onClickDisable, @@ -90,7 +86,6 @@ fun SourcesScreen( @Composable fun SourceList( - nestedScrollConnection: NestedScrollConnection, state: SourcesState, onClickItem: (Source) -> Unit, onClickDisable: (Source) -> Unit, @@ -100,7 +95,6 @@ fun SourceList( onClickToggleDataSaver: (Source) -> Unit, ) { ScrollbarLazyColumn( - modifier = Modifier.nestedScroll(nestedScrollConnection), contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, ) { items( 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 35a43ff4b..ea9e9dc37 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 @@ -49,13 +40,7 @@ fun LibraryTabs( ScrollableTabRow( selectedTabIndex = state.currentPage.coerceAtMost(categories.lastIndex), edgePadding = 0.dp, - indicator = { tabPositions -> - TabRowDefaults.Indicator( - Modifier - .tabIndicatorOffset(tabPositions[state.currentPage.coerceAtMost(categories.lastIndex)]) - .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)), - ) - }, + indicator = { TabIndicator(it[state.currentPage.coerceAtMost(categories.lastIndex)]) }, ) { categories.forEachIndexed { index, category -> val count by if (showMangaCount) { @@ -67,23 +52,13 @@ fun LibraryTabs( selected = state.currentPage == index, onClick = { scope.launch { state.animateScrollToPage(index) } }, text = { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - // SY --> - text = getCategoryName(category, category.visualName), - // SY <-- - 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( + // SY --> + text = getCategoryName(category, category.visualName), + // SY <-- + 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 440dbbf3b..e0c4b833f 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,174 +1,63 @@ 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.feed.FeedController -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.feed.feedTab +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( + // SY --> + startIndex = 2.takeIf { toExtensions }, + tabs = ( + if (presenter.feedTabInFront) listOf( + feedTab(router, presenter.feedPresenter), + sourcesTab(router, presenter.sourcesPresenter), + ) else listOf( + sourcesTab(router, presenter.sourcesPresenter), + feedTab(router, presenter.feedPresenter), + ) + ) + listOf( + extensionsTab(router, presenter.extensionsPresenter), + migrateSourcesTab(router, presenter.migrationSourcesPresenter), + ), + // SY <-- + ) - 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) { - // SY --> - val badge: BadgeDrawable? = getTabAt(EXTENSIONS_CONTROLLER)?.orCreateBadge - // SY <-- - badge?.isVisible = true - } else { - getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge() - } - } - } - - private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) { - - // SY --> - private val tabTitles = ( - if (preferences.feedTabInFront().get()) { - listOf( - R.string.feed, - R.string.label_sources, - R.string.label_extensions, - R.string.label_migration, - - ) - } else { - listOf( - R.string.label_sources, - R.string.feed, - R.string.label_extensions, - R.string.label_migration, - ) - } - ) - // SY <-- - .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) { - // SY --> - SOURCES_CONTROLLER -> if (preferences.feedTabInFront().get()) FeedController() else SourcesController() - FEED_CONTROLLER -> if (!preferences.feedTabInFront().get()) FeedController() else SourcesController() - // SY <-- - 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 - - // SY --> - const val FEED_CONTROLLER = 1 - const val EXTENSIONS_CONTROLLER = 2 - const val MIGRATION_CONTROLLER = 3 - // SY <-- + 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..16bc4a188 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.ui.browse + +import android.os.Bundle +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter +import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter +import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter +import eu.kanade.tachiyomi.ui.browse.source.SourcesController +import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class BrowsePresenter( + // SY --> + private val preferences: PreferencesHelper = Injekt.get(), + // SY <-- +) : BasePresenter() { + + // SY --> + val feedTabInFront = preferences.feedTabInFront().get() + // SY <-- + + // SY --> + val sourcesPresenter = SourcesPresenter(presenterScope, controllerMode = SourcesController.Mode.CATALOGUE, smartSearchConfig = null) + val feedPresenter = FeedPresenter(presenterScope) + + // SY <-- + val extensionsPresenter = ExtensionsPresenter(presenterScope) + val migrationSourcesPresenter = MigrationSourcesPresenter(presenterScope) + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + sourcesPresenter.onCreate() + // SY --> + feedPresenter.onCreate() + // SY <-- + extensionsPresenter.onCreate() + migrationSourcesPresenter.onCreate() + } + + // SY --> + override fun onDestroy() { + super.onDestroy() + feedPresenter.onDestroy() + } + // SY <-- +} 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/feed/FeedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt deleted file mode 100644 index 119010170..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt +++ /dev/null @@ -1,144 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.feed - -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 com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.presentation.browse.FeedScreen -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.base.controller.ComposeController -import eu.kanade.tachiyomi.ui.base.controller.pushController -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.lang.launchUI -import eu.kanade.tachiyomi.util.system.toast -import exh.savedsearches.models.FeedSavedSearch -import exh.savedsearches.models.SavedSearch - -/** - * This controller shows and manages the different search result in global search. - * This controller should only handle UI actions, IO actions should be done by [FeedPresenter] - * [FeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search - */ -class FeedController : ComposeController() { - - init { - setHasOptionsMenu(true) - } - - override fun getTitle(): String? { - return applicationContext?.getString(R.string.feed) - } - - /** - * Create the [FeedPresenter] used in controller. - * - * @return instance of [FeedPresenter] - */ - override fun createPresenter(): FeedPresenter { - return FeedPresenter() - } - - @Composable - override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { - FeedScreen( - nestedScrollInterop = nestedScrollInterop, - presenter = presenter, - onClickSavedSearch = ::onSavedSearchClick, - onClickSource = ::onSourceClick, - onClickDelete = ::onRemoveClick, - onClickManga = ::onMangaClick, - ) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.feed, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_add_feed -> addFeed() - } - return super.onOptionsItemSelected(item) - } - - private fun addFeed() { - viewScope.launchUI { - if (presenter.hasTooManyFeeds()) { - activity?.toast(R.string.too_many_in_feed) - return@launchUI - } - val items = presenter.getEnabledSources() - val itemsStrings = items.map { it.toString() } - var selectedIndex = 0 - - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.feed) - .setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which -> - selectedIndex = which - } - .setPositiveButton(android.R.string.ok) { _, _ -> - addFeedSearch(items[selectedIndex]) - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - } - - private fun addFeedSearch(source: CatalogueSource) { - viewScope.launchUI { - val items = presenter.getSourceSavedSearches(source.id) - val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name } - var selectedIndex = 0 - - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.feed) - .setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which -> - selectedIndex = which - } - .setPositiveButton(android.R.string.ok) { _, _ -> - presenter.createFeed(source, items.getOrNull(selectedIndex - 1)) - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - } - - /** - * Called when manga in global search is clicked, opens manga. - * - * @param manga clicked item containing manga information. - */ - private fun onMangaClick(manga: eu.kanade.domain.manga.model.Manga) { - // Open MangaController. - parentController?.router?.pushController(MangaController(manga.id, true)) - } - - /** - * Opens a catalogue with the given search. - */ - private fun onSourceClick(source: CatalogueSource) { - presenter.preferences.lastUsedSource().set(source.id) - parentController?.router?.pushController(LatestUpdatesController(source)) - } - - private fun onSavedSearchClick(savedSearch: SavedSearch, source: CatalogueSource) { - presenter.preferences.lastUsedSource().set(savedSearch.source) - parentController?.router?.pushController(BrowseSourceController(source, savedSearch = savedSearch.id)) - } - - private fun onRemoveClick(feedSavedSearch: FeedSavedSearch) { - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.feed) - .setMessage(R.string.feed_delete) - .setPositiveButton(R.string.action_delete) { _, _ -> - presenter.deleteFeed(feedSavedSearch) - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt index aa08527ae..c167c0634 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.browse.feed -import android.os.Bundle import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.InsertManga import eu.kanade.domain.manga.interactor.UpdateManga @@ -24,13 +23,13 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.logcat import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -55,6 +54,7 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer * @param preferences manages the preference calls. */ open class FeedPresenter( + private val presenterScope: CoroutineScope, private val state: FeedStateImpl = FeedState() as FeedStateImpl, val sourceManager: SourceManager = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(), @@ -67,7 +67,7 @@ open class FeedPresenter( private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(), private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(), private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(), -) : BasePresenter(), FeedState by state { +) : FeedState by state { /** * Fetches the different sources by user settings. @@ -84,9 +84,7 @@ open class FeedPresenter( */ private var fetchImageSubscription: Subscription? = null - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - + fun onCreate() { getFeedSavedSearchGlobal.subscribe() .distinctUntilChanged() .onEach { @@ -106,10 +104,24 @@ open class FeedPresenter( .launchIn(presenterScope) } - override fun onDestroy() { + fun onDestroy() { fetchSourcesSubscription?.unsubscribe() fetchImageSubscription?.unsubscribe() - super.onDestroy() + } + + fun openAddDialog() { + presenterScope.launchIO { + if (hasTooManyFeeds()) { + return@launchIO + } + dialog = Dialog.AddFeed(getEnabledSources()) + } + } + + fun openAddSearchDialog(source: CatalogueSource) { + presenterScope.launchIO { + dialog = Dialog.AddFeedSearch(source, (if (source.supportsLatest) listOf(null) else emptyList()) + getSourceSavedSearches(source.id)) + } } suspend fun hasTooManyFeeds(): Boolean { @@ -330,4 +342,10 @@ open class FeedPresenter( } return localManga?.toDbManga()!! } + + sealed class Dialog { + data class AddFeed(val options: List) : Dialog() + data class AddFeedSearch(val source: CatalogueSource, val options: List) : Dialog() + data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt new file mode 100644 index 000000000..d549dc8ba --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -0,0 +1,60 @@ +package eu.kanade.tachiyomi.ui.browse.feed + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +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.FeedScreen +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.latest.LatestUpdatesController +import eu.kanade.tachiyomi.ui.manga.MangaController + +@Composable +fun feedTab( + router: Router?, + presenter: FeedPresenter, +) = BrowseTab( + titleRes = R.string.feed, + actions = listOf( + AppBar.Action( + title = stringResource(R.string.action_add), + icon = Icons.Outlined.Add, + onClick = { + presenter.openAddDialog() + }, + ), + ), + content = { + FeedScreen( + presenter = presenter, + onClickAdd = { + presenter.openAddSearchDialog(it) + }, + onClickCreate = { source, savedSearch -> + presenter.createFeed(source, savedSearch) + }, + onClickSavedSearch = { savedSearch, source -> + presenter.preferences.lastUsedSource().set(savedSearch.source) + router?.pushController(BrowseSourceController(source, savedSearch = savedSearch.id)) + }, + onClickSource = { source -> + presenter.preferences.lastUsedSource().set(source.id) + router?.pushController(LatestUpdatesController(source)) + }, + onClickDelete = { + presenter.dialog = FeedPresenter.Dialog.DeleteFeed(it) + }, + onClickDeleteConfirm = { + presenter.deleteFeed(it) + }, + onClickManga = { manga -> + router?.pushController(MangaController(manga.id, true)) + }, + ) + }, +) 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..868ea6690 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt @@ -0,0 +1,74 @@ +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.domain.manga.interactor.GetFavorites +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.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController +import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withUIContext +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +@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, + ), + ) + }, + // SY --> + onClickAll = { source -> + // TODO: Jay wtf, need to clean this up sometime + launchIO { + val manga = Injekt.get().await() + val sourceMangas = + manga.asSequence().filter { it.source == source.id }.map { it.id }.toList() + withUIContext { + if (router != null) { + PreMigrationController.navigateToMigration( + Injekt.get().skipPreMigration().get(), + router, + sourceMangas, + ) + } + } + } + }, + // SY <-- + ) + }, + ) +} 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 index f699a2040..5b953927f 100644 --- 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 @@ -1,99 +1,16 @@ 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.domain.manga.interactor.GetFavorites -import eu.kanade.presentation.browse.MigrateSourceScreen -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -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.migration.advanced.design.PreMigrationController -import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController -import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.withUIContext -import eu.kanade.tachiyomi.util.system.openInBrowser -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get +import eu.kanade.presentation.browse.BrowseTabWrapper +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController -class MigrationSourcesController : ComposeController() { +class MigrationSourcesController : FullComposeController() { - init { - setHasOptionsMenu(true) - } - - override fun createPresenter() = MigrationSourcesPresenter() + override fun createPresenter() = MigrationSourcesPresenterWrapper() @Composable - override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { - MigrateSourceScreen( - nestedScrollInterop = nestedScrollInterop, - presenter = presenter, - onClickItem = { source -> - val parentController = parentController - if (parentController is BrowseController) { - parentController.router - } else { - router - }.pushController( - MigrationMangaController( - source.id, - source.name, - ), - ) - }, - onClickAll = { source -> - // TODO: Jay wtf, need to clean this up sometime - launchIO { - val manga = Injekt.get().await() - val sourceMangas = - manga.asSequence().filter { it.source == source.id }.map { it.id }.toList() - withUIContext { - PreMigrationController.navigateToMigration( - Injekt.get().skipPreMigration().get(), - run { - val parentController = parentController - if (parentController is BrowseController) { - parentController.router - } else { - router - } - }, - sourceMangas, - ) - } - } - }, - ) - } - - 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) - } + override fun ComposeContent() { + BrowseTabWrapper(migrateSourcesTab(router, presenter = presenter.presenter)) } } 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/migration/sources/MigrationSourcesPresenterWrapper.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenterWrapper.kt new file mode 100644 index 000000000..b367b7eb7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenterWrapper.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.ui.browse.migration.sources + +import android.os.Bundle +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter + +class MigrationSourcesPresenterWrapper() : BasePresenter() { + + val presenter = MigrationSourcesPresenter(presenterScope) + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + presenter.onCreate() + } +} 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 index d4d194841..941988276 100644 --- 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 @@ -3,46 +3,18 @@ package eu.kanade.tachiyomi.ui.browse.source import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.os.Bundle import android.os.Parcelable -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 com.bluelinelabs.conductor.Controller -import eu.kanade.domain.source.model.Source -import eu.kanade.presentation.browse.SourcesScreen +import eu.kanade.presentation.browse.BrowseTabWrapper 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.FullComposeController import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController -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 eu.kanade.tachiyomi.util.system.getParcelableCompat -import exh.ui.smartsearch.SmartSearchController import kotlinx.parcelize.Parcelize -import uy.kohesive.injekt.injectLazy -class SourcesController(bundle: Bundle? = null) : SearchableComposeController(bundle) { - - private val preferences: PreferencesHelper by injectLazy() - - // EXH --> +class SourcesController(bundle: Bundle? = null) : FullComposeController(bundle) { private val smartSearchConfig = args.getParcelableCompat(SMART_SEARCH_CONFIG) - private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH - // EXH <-- - - init { - // SY --> - setHasOptionsMenu(mode == Mode.CATALOGUE) - // SY <-- - } override fun getTitle(): String? { // SY --> @@ -53,40 +25,11 @@ class SourcesController(bundle: Bundle? = null) : SearchableComposeController */ controllerMode = mode /* SY <-- */) + override fun createPresenter() = SourcesPresenterWrapper(controllerMode = mode, smartSearchConfig = smartSearchConfig) @Composable - override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { - SourcesScreen( - nestedScrollInterop = nestedScrollInterop, - presenter = presenter, - onClickItem = { source -> - when { - mode == Mode.SMART_SEARCH -> router.pushController(SmartSearchController(source.id, smartSearchConfig!!)) - preferences.useNewSourceNavigation().get() -> openSource(source, SourceFeedController(source.id)) - else -> openSource(source, BrowseSourceController(source)) - } - }, - onClickDisable = { source -> - presenter.toggleSource(source) - }, - onClickLatest = { source -> - openSource(source, LatestUpdatesController(source)) - }, - onClickPin = { source -> - presenter.togglePin(source) - }, - onClickSetCategories = { source, categories -> - presenter.setSourceCategories(source, categories) - }, - onClickToggleDataSaver = { source -> - presenter.toggleExcludeFromDataSaver(source) - }, - ) - - LaunchedEffect(Unit) { - (activity as? MainActivity)?.ready = true - } + override fun ComposeContent() { + BrowseTabWrapper(sourcesTab(router, presenter = presenter.presenter)) } override fun onViewCreated(view: View) { @@ -94,57 +37,6 @@ class SourcesController(bundle: Bundle? = null) : SearchableComposeController { - parentController!!.router.pushController(SourceFilterController()) - true - } - else -> super.onOptionsItemSelected(item) - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - if (mode == Mode.CATALOGUE) { - 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?) { - // SY --> - if (mode == Mode.CATALOGUE) { - parentController!!.router.pushController( - GlobalSearchController(query), - ) - } - // SY <-- - } - - // SY --> @Parcelize data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable @@ -157,5 +49,4 @@ class SourcesController(bundle: Bundle? = null) : SearchableComposeController(), 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) + val useNewSourceNavigation = preferences.useNewSourceNavigation().get() + + fun onCreate() { // SY --> combine( getEnabledSources.subscribe(), @@ -105,6 +109,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/SourcesPresenterWrapper.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenterWrapper.kt new file mode 100644 index 000000000..6ae9f5aca --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenterWrapper.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.ui.browse.source + +import android.os.Bundle +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter + +class SourcesPresenterWrapper(controllerMode: SourcesController.Mode, smartSearchConfig: SourcesController.SmartSearchConfig?) : BasePresenter() { + + val presenter = SourcesPresenter(presenterScope, controllerMode = controllerMode, smartSearchConfig = smartSearchConfig) + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + presenter.onCreate() + } +} 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..6b075b583 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt @@ -0,0 +1,80 @@ +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.feed.SourceFeedController +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController +import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController +import exh.ui.smartsearch.SmartSearchController + +@Composable +fun sourcesTab( + router: Router?, + presenter: SourcesPresenter, +) = BrowseTab( + // SY --> + titleRes = when (presenter.controllerMode) { + SourcesController.Mode.CATALOGUE -> R.string.label_sources + SourcesController.Mode.SMART_SEARCH -> R.string.find_in_another_source + }, + actions = if (presenter.controllerMode == SourcesController.Mode.CATALOGUE) { + 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()) }, + ), + ) + } else emptyList(), + // SY <-- + content = { + SourcesScreen( + presenter = presenter, + onClickItem = { source -> + // SY --> + val controller = when { + presenter.controllerMode == SourcesController.Mode.SMART_SEARCH -> + SmartSearchController(source.id, presenter.smartSearchConfig!!) + presenter.useNewSourceNavigation -> SourceFeedController(source.id) + else -> BrowseSourceController(source) + } + presenter.onOpenSource(source) + router?.pushController(controller) + // SY <-- + }, + onClickDisable = { source -> + presenter.toggleSource(source) + }, + onClickLatest = { source -> + presenter.onOpenSource(source) + router?.pushController(LatestUpdatesController(source)) + }, + onClickPin = { source -> + presenter.togglePin(source) + }, + // SY --> + onClickSetCategories = { source, categories -> + presenter.setSourceCategories(source, categories) + }, + onClickToggleDataSaver = { source -> + presenter.toggleExcludeFromDataSaver(source) + }, + // SY <-- + ) + }, +) 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 8c73b7794..8866ca0ae 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -46,7 +46,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 @@ -189,7 +188,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) { @@ -644,17 +643,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/java/exh/debug/SettingsDebugController.kt b/app/src/main/java/exh/debug/SettingsDebugController.kt index 1b5205997..972e4fb11 100644 --- a/app/src/main/java/exh/debug/SettingsDebugController.kt +++ b/app/src/main/java/exh/debug/SettingsDebugController.kt @@ -114,7 +114,7 @@ class SettingsDebugController : BasicFullComposeController() { fun FunctionList( paddingValues: PaddingValues, functions: List, String>>, - toggles: List + toggles: List, ) { val scope = rememberCoroutineScope() Box(Modifier.fillMaxSize()) { 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 d402cced7..88df787f9 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]