mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Initial conversion of browse tabs to full Compose
TODO: - Global search should launch a controller with the search textfield focused. This is pending a Compose rewrite of that screen. - Better migrate sort UI - Extensions search
This commit is contained in:
		| @@ -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 { | ||||
|   | ||||
| @@ -0,0 +1,84 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.statusBarsPadding | ||||
| import androidx.compose.material3.Tab | ||||
| import androidx.compose.material3.TabRow | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import com.google.accompanist.pager.HorizontalPager | ||||
| import com.google.accompanist.pager.rememberPagerState | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.components.TabIndicator | ||||
| import eu.kanade.presentation.components.TabText | ||||
| import eu.kanade.tachiyomi.R | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| @Composable | ||||
| fun BrowseScreen( | ||||
|     startIndex: Int? = null, | ||||
|     tabs: List<BrowseTab>, | ||||
| ) { | ||||
|     val scope = rememberCoroutineScope() | ||||
|     val state = rememberPagerState() | ||||
|  | ||||
|     LaunchedEffect(startIndex) { | ||||
|         if (startIndex != null) { | ||||
|             state.scrollToPage(startIndex) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Scaffold( | ||||
|         modifier = Modifier.statusBarsPadding(), | ||||
|         topBar = { | ||||
|             AppBar( | ||||
|                 title = stringResource(R.string.browse), | ||||
|                 actions = { | ||||
|                     AppBarActions(tabs[state.currentPage].actions) | ||||
|                 }, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         Column(modifier = Modifier.padding(paddingValues)) { | ||||
|             TabRow( | ||||
|                 selectedTabIndex = state.currentPage, | ||||
|                 indicator = { TabIndicator(it[state.currentPage]) }, | ||||
|             ) { | ||||
|                 tabs.forEachIndexed { index, tab -> | ||||
|                     Tab( | ||||
|                         selected = state.currentPage == index, | ||||
|                         onClick = { scope.launch { state.animateScrollToPage(index) } }, | ||||
|                         text = { | ||||
|                             TabText(stringResource(tab.titleRes), tab.badgeNumber, state.currentPage == index) | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             HorizontalPager( | ||||
|                 count = tabs.size, | ||||
|                 modifier = Modifier.fillMaxSize(), | ||||
|                 state = state, | ||||
|                 verticalAlignment = Alignment.Top, | ||||
|             ) { page -> | ||||
|                 tabs[page].content() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| data class BrowseTab( | ||||
|     @StringRes val titleRes: Int, | ||||
|     val badgeNumber: Int? = null, | ||||
|     val actions: List<AppBar.Action> = emptyList(), | ||||
|     val content: @Composable () -> Unit, | ||||
| ) | ||||
| @@ -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<Extension.Untrusted?>(null) } | ||||
|  | ||||
| @@ -187,9 +179,6 @@ fun ExtensionContent( | ||||
|                             } | ||||
|                         }, | ||||
|                     ) | ||||
|                     LaunchedEffect(Unit) { | ||||
|                         onLaunched() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -10,6 +10,7 @@ interface ExtensionsState { | ||||
|     val isLoading: Boolean | ||||
|     val isRefreshing: Boolean | ||||
|     val items: List<ExtensionUiModel> | ||||
|     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<ExtensionUiModel> by mutableStateOf(emptyList()) | ||||
|     override var updates: Int by mutableStateOf(0) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
|   | ||||
| @@ -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<Pair<Source, Long>>, | ||||
|     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), | ||||
|   | ||||
| @@ -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<Pair<Source, Long>> | ||||
|     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<Pair<Source, Long>> 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) | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|                 ) | ||||
|   | ||||
							
								
								
									
										50
									
								
								app/src/main/java/eu/kanade/presentation/components/Tabs.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/src/main/java/eu/kanade/presentation/components/Tabs.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.TabPosition | ||||
| import androidx.compose.material3.TabRowDefaults | ||||
| import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
|  | ||||
| @Composable | ||||
| fun TabIndicator(currentTabPosition: TabPosition) { | ||||
|     TabRowDefaults.Indicator( | ||||
|         Modifier | ||||
|             .tabIndicatorOffset(currentTabPosition) | ||||
|             .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun TabText( | ||||
|     text: String, | ||||
|     badgeCount: Int? = null, | ||||
|     isCurrentPage: Boolean, | ||||
| ) { | ||||
|     val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f | ||||
|  | ||||
|     Row( | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|     ) { | ||||
|         Text( | ||||
|             text = text, | ||||
|             color = if (isCurrentPage) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground, | ||||
|         ) | ||||
|         if (badgeCount != null) { | ||||
|             Pill( | ||||
|                 text = "$badgeCount", | ||||
|                 color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha), | ||||
|                 fontSize = 10.sp, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|   | ||||
| @@ -4,10 +4,7 @@ import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import androidx.compose.ui.platform.rememberNestedScrollInteropConnection | ||||
| import eu.kanade.tachiyomi.databinding.ComposeControllerBinding | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.view.setComposeContent | ||||
| import nucleus.presenter.Presenter | ||||
|  | ||||
| @@ -29,33 +26,11 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) : | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Compose controller with a Nucleus presenter. | ||||
|  */ | ||||
| abstract class ComposeController<P : Presenter<*>>(bundle: Bundle? = null) : | ||||
|     NucleusController<ComposeControllerBinding, P>(bundle), | ||||
|     ComposeContentController { | ||||
|  | ||||
|     override fun createBinding(inflater: LayoutInflater) = | ||||
|         ComposeControllerBinding.inflate(inflater) | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         binding.root.apply { | ||||
|             setComposeContent { | ||||
|                 val nestedScrollInterop = rememberNestedScrollInteropConnection() | ||||
|                 ComposeContent(nestedScrollInterop) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Basic Compose controller without a presenter. | ||||
|  */ | ||||
| abstract class BasicFullComposeController : | ||||
|     BaseController<ComposeControllerBinding>(), | ||||
| abstract class BasicFullComposeController(bundle: Bundle? = null) : | ||||
|     BaseController<ComposeControllerBinding>(bundle), | ||||
|     FullComposeContentController { | ||||
|  | ||||
|     override fun createBinding(inflater: LayoutInflater) = | ||||
| @@ -72,29 +47,6 @@ abstract class BasicFullComposeController : | ||||
|     } | ||||
| } | ||||
|  | ||||
| abstract class SearchableComposeController<P : BasePresenter<*>>(bundle: Bundle? = null) : | ||||
|     SearchableNucleusController<ComposeControllerBinding, P>(bundle), | ||||
|     ComposeContentController { | ||||
|  | ||||
|     override fun createBinding(inflater: LayoutInflater) = | ||||
|         ComposeControllerBinding.inflate(inflater) | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         binding.root.apply { | ||||
|             setComposeContent { | ||||
|                 val nestedScrollInterop = rememberNestedScrollInteropConnection() | ||||
|                 ComposeContent(nestedScrollInterop) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| interface FullComposeContentController { | ||||
|     @Composable fun ComposeContent() | ||||
| } | ||||
|  | ||||
| interface ComposeContentController { | ||||
|     @Composable fun ComposeContent(nestedScrollInterop: NestedScrollConnection) | ||||
| } | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.base.controller | ||||
|  | ||||
| import com.google.android.material.tabs.TabLayout | ||||
|  | ||||
| interface TabbedController { | ||||
|  | ||||
|     /** | ||||
|      * @return true to let activity updates tabs visibility (to visible) | ||||
|      */ | ||||
|     fun configureTabs(tabs: TabLayout): Boolean = true | ||||
|  | ||||
|     fun cleanupTabs(tabs: TabLayout) {} | ||||
| } | ||||
| @@ -1,149 +1,53 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse | ||||
|  | ||||
| import android.Manifest | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.core.os.bundleOf | ||||
| import com.bluelinelabs.conductor.Controller | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import com.bluelinelabs.conductor.RouterTransaction | ||||
| import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter | ||||
| import com.google.android.material.badge.BadgeDrawable | ||||
| import com.google.android.material.tabs.TabLayout | ||||
| import com.jakewharton.rxrelay.PublishRelay | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.databinding.PagerControllerBinding | ||||
| import eu.kanade.presentation.browse.BrowseScreen | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RxController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.TabbedController | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsController | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.SourcesController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourcesTab | ||||
| import eu.kanade.tachiyomi.ui.browse.source.sourcesTab | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class BrowseController : | ||||
|     RxController<PagerControllerBinding>, | ||||
|     RootController, | ||||
|     TabbedController { | ||||
| class BrowseController : FullComposeController<BrowsePresenter>, RootController { | ||||
|  | ||||
|     @Suppress("unused") | ||||
|     constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false) | ||||
|  | ||||
|     constructor(toExtensions: Boolean = false) : super( | ||||
|         bundleOf(TO_EXTENSIONS_EXTRA to toExtensions), | ||||
|     ) | ||||
|  | ||||
|     @Suppress("unused") | ||||
|     constructor(bundle: Bundle) : this(bundle.getBoolean(TO_EXTENSIONS_EXTRA)) | ||||
|  | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false) | ||||
|  | ||||
|     val extensionListUpdateRelay: PublishRelay<Boolean> = PublishRelay.create() | ||||
|     override fun createPresenter() = BrowsePresenter() | ||||
|  | ||||
|     private var adapter: BrowseAdapter? = null | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         BrowseScreen( | ||||
|             startIndex = 1.takeIf { toExtensions }, | ||||
|             tabs = listOf( | ||||
|                 sourcesTab(router, presenter.sourcesPresenter), | ||||
|                 extensionsTab(router, presenter.extensionsPresenter), | ||||
|                 migrateSourcesTab(router, presenter.migrationSourcesPresenter), | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     override fun getTitle(): String? { | ||||
|         return resources!!.getString(R.string.browse) | ||||
|         LaunchedEffect(Unit) { | ||||
|             (activity as? MainActivity)?.ready = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater) | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         adapter = BrowseAdapter() | ||||
|         binding.pager.adapter = adapter | ||||
|  | ||||
|         if (toExtensions) { | ||||
|             binding.pager.currentItem = EXTENSIONS_CONTROLLER | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         super.onDestroyView(view) | ||||
|         adapter = null | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         super.onChangeStarted(handler, type) | ||||
|         if (type.isEnter) { | ||||
|             (activity as? MainActivity)?.binding?.tabs?.apply { | ||||
|                 setupWithViewPager(binding.pager) | ||||
|  | ||||
|                 // Show badge on tab for extension updates | ||||
|                 setExtensionUpdateBadge() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun configureTabs(tabs: TabLayout): Boolean { | ||||
|         with(tabs) { | ||||
|             tabGravity = TabLayout.GRAVITY_FILL | ||||
|             tabMode = TabLayout.MODE_FIXED | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     override fun cleanupTabs(tabs: TabLayout) { | ||||
|         // Remove extension update badge | ||||
|         tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge() | ||||
|     } | ||||
|  | ||||
|     fun setExtensionUpdateBadge() { | ||||
|         /* It's possible to switch to the Library controller by the time setExtensionUpdateBadge | ||||
|         is called, resulting in a badge being put on the category tabs (if enabled). | ||||
|         This check prevents that from happening */ | ||||
|         if (router.backstack.lastOrNull()?.controller !is BrowseController) return | ||||
|  | ||||
|         (activity as? MainActivity)?.binding?.tabs?.apply { | ||||
|             val updates = preferences.extensionUpdatesCount().get() | ||||
|             if (updates > 0) { | ||||
|                 val badge: BadgeDrawable? = getTabAt(1)?.orCreateBadge | ||||
|                 badge?.isVisible = true | ||||
|             } else { | ||||
|                 getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) { | ||||
|  | ||||
|         private val tabTitles = listOf( | ||||
|             R.string.label_sources, | ||||
|             R.string.label_extensions, | ||||
|             R.string.label_migration, | ||||
|         ) | ||||
|             .map { resources!!.getString(it) } | ||||
|  | ||||
|         override fun getCount(): Int { | ||||
|             return tabTitles.size | ||||
|         } | ||||
|  | ||||
|         override fun configureRouter(router: Router, position: Int) { | ||||
|             if (!router.hasRootController()) { | ||||
|                 val controller: Controller = when (position) { | ||||
|                     SOURCES_CONTROLLER -> SourcesController() | ||||
|                     EXTENSIONS_CONTROLLER -> ExtensionsController() | ||||
|                     MIGRATION_CONTROLLER -> MigrationSourcesController() | ||||
|                     else -> error("Wrong position $position") | ||||
|                 } | ||||
|                 router.setRoot(RouterTransaction.with(controller)) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         override fun getPageTitle(position: Int): CharSequence { | ||||
|             return tabTitles[position] | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val TO_EXTENSIONS_EXTRA = "to_extensions" | ||||
|  | ||||
|         const val SOURCES_CONTROLLER = 0 | ||||
|         const val EXTENSIONS_CONTROLLER = 1 | ||||
|         const val MIGRATION_CONTROLLER = 2 | ||||
|         requestPermissionsSafe(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 301) | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val TO_EXTENSIONS_EXTRA = "to_extensions" | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter | ||||
| import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class BrowsePresenter : BasePresenter<BrowseController>() { | ||||
|  | ||||
|     val sourcesPresenter = SourcesPresenter(presenterScope) | ||||
|     val extensionsPresenter = ExtensionsPresenter(presenterScope) | ||||
|     val migrationSourcesPresenter = MigrationSourcesPresenter(presenterScope) | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         sourcesPresenter.onCreate() | ||||
|         extensionsPresenter.onCreate() | ||||
|         migrationSourcesPresenter.onCreate() | ||||
|     } | ||||
| } | ||||
| @@ -1,120 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.extension | ||||
|  | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import androidx.appcompat.widget.SearchView | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import eu.kanade.presentation.browse.ExtensionScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.ui.base.controller.ComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.BrowseController | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController | ||||
| import kotlinx.coroutines.flow.filter | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import reactivecircus.flowbinding.appcompat.queryTextChanges | ||||
|  | ||||
| class ExtensionsController : ComposeController<ExtensionsPresenter>() { | ||||
|  | ||||
|     private var query = "" | ||||
|  | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     override fun getTitle() = applicationContext?.getString(R.string.label_extensions) | ||||
|  | ||||
|     override fun createPresenter() = ExtensionsPresenter() | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { | ||||
|         ExtensionScreen( | ||||
|             nestedScrollInterop = nestedScrollInterop, | ||||
|             presenter = presenter, | ||||
|             onLongClickItem = { extension -> | ||||
|                 when (extension) { | ||||
|                     is Extension.Available -> presenter.installExtension(extension) | ||||
|                     else -> presenter.uninstallExtension(extension.pkgName) | ||||
|                 } | ||||
|             }, | ||||
|             onClickItemCancel = { extension -> | ||||
|                 presenter.cancelInstallUpdateExtension(extension) | ||||
|             }, | ||||
|             onClickUpdateAll = { | ||||
|                 presenter.updateAllExtensions() | ||||
|             }, | ||||
|             onLaunched = { | ||||
|                 val ctrl = parentController as BrowseController | ||||
|                 ctrl.setExtensionUpdateBadge() | ||||
|                 ctrl.extensionListUpdateRelay.call(true) | ||||
|             }, | ||||
|             onInstallExtension = { | ||||
|                 presenter.installExtension(it) | ||||
|             }, | ||||
|             onOpenExtension = { | ||||
|                 val controller = ExtensionDetailsController(it.pkgName) | ||||
|                 parentController!!.router.pushController(controller) | ||||
|             }, | ||||
|             onTrustExtension = { | ||||
|                 presenter.trustSignature(it.signatureHash) | ||||
|             }, | ||||
|             onUninstallExtension = { | ||||
|                 presenter.uninstallExtension(it.pkgName) | ||||
|             }, | ||||
|             onUpdateExtension = { | ||||
|                 presenter.updateExtension(it) | ||||
|             }, | ||||
|             onRefresh = { | ||||
|                 presenter.findAvailableExtensions() | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.action_search -> expandActionViewFromInteraction = true | ||||
|             R.id.action_settings -> { | ||||
|                 parentController!!.router.pushController(ExtensionFilterController()) | ||||
|             } | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         super.onChangeStarted(handler, type) | ||||
|         if (type.isPush) { | ||||
|             presenter.findAvailableExtensions() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         inflater.inflate(R.menu.browse_extensions, menu) | ||||
|  | ||||
|         val searchItem = menu.findItem(R.id.action_search) | ||||
|         val searchView = searchItem.actionView as SearchView | ||||
|         searchView.maxWidth = Int.MAX_VALUE | ||||
|  | ||||
|         // Fixes problem with the overflow icon showing up in lieu of search | ||||
|         searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() }) | ||||
|  | ||||
|         if (query.isNotEmpty()) { | ||||
|             searchItem.expandActionView() | ||||
|             searchView.setQuery(query, true) | ||||
|             searchView.clearFocus() | ||||
|         } | ||||
|  | ||||
|         searchView.queryTextChanges() | ||||
|             .filter { router.backstack.lastOrNull()?.controller == this } | ||||
|             .onEach { | ||||
|                 query = it.toString() | ||||
|                 presenter.search(query) | ||||
|             } | ||||
|             .launchIn(viewScope) | ||||
|     } | ||||
| } | ||||
| @@ -1,41 +1,43 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.extension | ||||
|  | ||||
| import android.app.Application | ||||
| import android.os.Bundle | ||||
| import androidx.annotation.StringRes | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionsByType | ||||
| import eu.kanade.presentation.browse.ExtensionState | ||||
| import eu.kanade.presentation.browse.ExtensionsState | ||||
| import eu.kanade.presentation.browse.ExtensionsStateImpl | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.model.InstallStep | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.update | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class ExtensionsPresenter( | ||||
|     private val presenterScope: CoroutineScope, | ||||
|     private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl, | ||||
|     private val preferences: PreferencesHelper = Injekt.get(), | ||||
|     private val extensionManager: ExtensionManager = Injekt.get(), | ||||
|     private val getExtensions: GetExtensionsByType = Injekt.get(), | ||||
| ) : BasePresenter<ExtensionsController>(), ExtensionsState by state { | ||||
| ) : ExtensionsState by state { | ||||
|  | ||||
|     private val _query: MutableStateFlow<String> = MutableStateFlow("") | ||||
|  | ||||
|     private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|     fun onCreate() { | ||||
|         val context = Injekt.get<Application>() | ||||
|         val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map -> | ||||
|             { | ||||
| @@ -114,6 +116,10 @@ class ExtensionsPresenter( | ||||
|         } | ||||
|  | ||||
|         presenterScope.launchIO { findAvailableExtensions() } | ||||
|  | ||||
|         preferences.extensionUpdatesCount().asFlow() | ||||
|             .onEach { state.updates = it } | ||||
|             .launchIn(presenterScope) | ||||
|     } | ||||
|  | ||||
|     fun search(query: String) { | ||||
|   | ||||
| @@ -0,0 +1,75 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.extension | ||||
|  | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.FilterList | ||||
| import androidx.compose.material.icons.outlined.Search | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import eu.kanade.presentation.browse.BrowseTab | ||||
| import eu.kanade.presentation.browse.ExtensionScreen | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController | ||||
|  | ||||
| @Composable | ||||
| fun extensionsTab( | ||||
|     router: Router?, | ||||
|     presenter: ExtensionsPresenter, | ||||
| ) = BrowseTab( | ||||
|     titleRes = R.string.label_extensions, | ||||
|     badgeNumber = presenter.updates.takeIf { it > 0 }, | ||||
|     actions = listOf( | ||||
|         AppBar.Action( | ||||
|             title = stringResource(R.string.action_search), | ||||
|             icon = Icons.Outlined.Search, | ||||
|             onClick = { | ||||
|                 // TODO: extensions search | ||||
|                 // presenter.search(query) | ||||
|             }, | ||||
|         ), | ||||
|  | ||||
|         AppBar.Action( | ||||
|             title = stringResource(R.string.action_filter), | ||||
|             icon = Icons.Outlined.FilterList, | ||||
|             onClick = { router?.pushController(ExtensionFilterController()) }, | ||||
|         ), | ||||
|     ), | ||||
|     content = { | ||||
|         ExtensionScreen( | ||||
|             presenter = presenter, | ||||
|             onLongClickItem = { extension -> | ||||
|                 when (extension) { | ||||
|                     is Extension.Available -> presenter.installExtension(extension) | ||||
|                     else -> presenter.uninstallExtension(extension.pkgName) | ||||
|                 } | ||||
|             }, | ||||
|             onClickItemCancel = { extension -> | ||||
|                 presenter.cancelInstallUpdateExtension(extension) | ||||
|             }, | ||||
|             onClickUpdateAll = { | ||||
|                 presenter.updateAllExtensions() | ||||
|             }, | ||||
|             onInstallExtension = { | ||||
|                 presenter.installExtension(it) | ||||
|             }, | ||||
|             onOpenExtension = { | ||||
|                 router?.pushController(ExtensionDetailsController(it.pkgName)) | ||||
|             }, | ||||
|             onTrustExtension = { | ||||
|                 presenter.trustSignature(it.signatureHash) | ||||
|             }, | ||||
|             onUninstallExtension = { | ||||
|                 presenter.uninstallExtension(it.pkgName) | ||||
|             }, | ||||
|             onUpdateExtension = { | ||||
|                 presenter.updateExtension(it) | ||||
|             }, | ||||
|             onRefresh = { | ||||
|                 presenter.findAvailableExtensions() | ||||
|             }, | ||||
|         ) | ||||
|     }, | ||||
| ) | ||||
| @@ -0,0 +1,48 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.HelpOutline | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import eu.kanade.presentation.browse.BrowseTab | ||||
| import eu.kanade.presentation.browse.MigrateSourceScreen | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController | ||||
|  | ||||
| @Composable | ||||
| fun migrateSourcesTab( | ||||
|     router: Router?, | ||||
|     presenter: MigrationSourcesPresenter, | ||||
| ): BrowseTab { | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|  | ||||
|     return BrowseTab( | ||||
|         titleRes = R.string.label_migration, | ||||
|         actions = listOf( | ||||
|             AppBar.Action( | ||||
|                 title = stringResource(R.string.migration_help_guide), | ||||
|                 icon = Icons.Outlined.HelpOutline, | ||||
|                 onClick = { | ||||
|                     uriHandler.openUri("https://tachiyomi.org/help/guides/source-migration/") | ||||
|                 }, | ||||
|             ), | ||||
|         ), | ||||
|         content = { | ||||
|             MigrateSourceScreen( | ||||
|                 presenter = presenter, | ||||
|                 onClickItem = { source -> | ||||
|                     router?.pushController( | ||||
|                         MigrationMangaController( | ||||
|                             source.id, | ||||
|                             source.name, | ||||
|                         ), | ||||
|                     ) | ||||
|                 }, | ||||
|             ) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import eu.kanade.presentation.browse.MigrateSourceScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.controller.ComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
|  | ||||
| class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() { | ||||
|  | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     override fun createPresenter() = MigrationSourcesPresenter() | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { | ||||
|         MigrateSourceScreen( | ||||
|             nestedScrollInterop = nestedScrollInterop, | ||||
|             presenter = presenter, | ||||
|             onClickItem = { source -> | ||||
|                 parentController!!.router.pushController( | ||||
|                     MigrationMangaController( | ||||
|                         source.id, | ||||
|                         source.name, | ||||
|                     ), | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = | ||||
|         inflater.inflate(R.menu.browse_migrate, menu) | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         return when (val itemId = item.itemId) { | ||||
|             R.id.action_source_migration_help -> { | ||||
|                 activity?.openInBrowser(HELP_URL) | ||||
|                 true | ||||
|             } | ||||
|             R.id.asc_alphabetical, | ||||
|             R.id.desc_alphabetical, | ||||
|             -> { | ||||
|                 presenter.setAlphabeticalSorting(itemId == R.id.asc_alphabetical) | ||||
|                 true | ||||
|             } | ||||
|             R.id.asc_count, | ||||
|             R.id.desc_count, | ||||
|             -> { | ||||
|                 presenter.setTotalSorting(itemId == R.id.asc_count) | ||||
|                 true | ||||
|             } | ||||
|             else -> super.onOptionsItemSelected(item) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/" | ||||
| @@ -1,33 +1,35 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.presentation.browse.MigrateSourceState | ||||
| import eu.kanade.presentation.browse.MigrateSourceStateImpl | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.receiveAsFlow | ||||
| import logcat.LogPriority | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class MigrationSourcesPresenter( | ||||
|     private val presenterScope: CoroutineScope, | ||||
|     private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl, | ||||
|     private val preferences: PreferencesHelper = Injekt.get(), | ||||
|     private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), | ||||
|     private val setMigrateSorting: SetMigrateSorting = Injekt.get(), | ||||
| ) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state { | ||||
| ) : MigrateSourceState by state { | ||||
|  | ||||
|     private val _channel = Channel<Event>(Int.MAX_VALUE) | ||||
|     val channel = _channel.receiveAsFlow() | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|     fun onCreate() { | ||||
|         presenterScope.launchIO { | ||||
|             getSourcesWithFavoriteCount.subscribe() | ||||
|                 .catch { exception -> | ||||
| @@ -39,14 +41,32 @@ class MigrationSourcesPresenter( | ||||
|                     state.isLoading = false | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         preferences.migrationSortingDirection().asFlow() | ||||
|             .onEach { state.sortingDirection = it } | ||||
|             .launchIn(presenterScope) | ||||
|  | ||||
|         preferences.migrationSortingMode().asFlow() | ||||
|             .onEach { state.sortingMode = it } | ||||
|             .launchIn(presenterScope) | ||||
|     } | ||||
|  | ||||
|     fun setAlphabeticalSorting(isAscending: Boolean) { | ||||
|         setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending) | ||||
|     fun toggleSortingMode() { | ||||
|         val newMode = when (state.sortingMode) { | ||||
|             SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL | ||||
|             SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL | ||||
|         } | ||||
|  | ||||
|         setMigrateSorting.await(newMode, state.sortingDirection) | ||||
|     } | ||||
|  | ||||
|     fun setTotalSorting(isAscending: Boolean) { | ||||
|         setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending) | ||||
|     fun toggleSortingDirection() { | ||||
|         val newDirection = when (state.sortingDirection) { | ||||
|             SetMigrateSorting.Direction.ASCENDING -> SetMigrateSorting.Direction.DESCENDING | ||||
|             SetMigrateSorting.Direction.DESCENDING -> SetMigrateSorting.Direction.ASCENDING | ||||
|         } | ||||
|  | ||||
|         setMigrateSorting.await(state.sortingMode, newDirection) | ||||
|     } | ||||
|  | ||||
|     sealed class Event { | ||||
|   | ||||
| @@ -1,106 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.source | ||||
|  | ||||
| import android.Manifest.permission.WRITE_EXTERNAL_STORAGE | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.browse.SourcesScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe | ||||
| import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SourcesController : SearchableComposeController<SourcesPresenter>() { | ||||
|  | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     override fun getTitle() = resources?.getString(R.string.label_sources) | ||||
|  | ||||
|     override fun createPresenter() = SourcesPresenter() | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { | ||||
|         SourcesScreen( | ||||
|             nestedScrollInterop = nestedScrollInterop, | ||||
|             presenter = presenter, | ||||
|             onClickItem = { source -> | ||||
|                 openSource(source, BrowseSourceController(source)) | ||||
|             }, | ||||
|             onClickDisable = { source -> | ||||
|                 presenter.toggleSource(source) | ||||
|             }, | ||||
|             onClickLatest = { source -> | ||||
|                 openSource(source, LatestUpdatesController(source)) | ||||
|             }, | ||||
|             onClickPin = { source -> | ||||
|                 presenter.togglePin(source) | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         LaunchedEffect(Unit) { | ||||
|             (activity as? MainActivity)?.ready = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|         requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Opens a catalogue with the given controller. | ||||
|      */ | ||||
|     private fun openSource(source: Source, controller: BrowseSourceController) { | ||||
|         if (!preferences.incognitoMode().get()) { | ||||
|             preferences.lastUsedSource().set(source.id) | ||||
|         } | ||||
|         parentController!!.router.pushController(controller) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when an option menu item has been selected by the user. | ||||
|      * | ||||
|      * @param item The selected item. | ||||
|      * @return True if this event has been consumed, false if it has not. | ||||
|      */ | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         return when (item.itemId) { | ||||
|             // Initialize option to open catalogue settings. | ||||
|             R.id.action_settings -> { | ||||
|                 parentController!!.router.pushController(SourceFilterController()) | ||||
|                 true | ||||
|             } | ||||
|             else -> super.onOptionsItemSelected(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         createOptionsMenu( | ||||
|             menu, | ||||
|             inflater, | ||||
|             R.menu.browse_sources, | ||||
|             R.id.action_search, | ||||
|             R.string.action_global_search_hint, | ||||
|             false, // GlobalSearch handles the searching here | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onSearchViewQueryTextSubmit(query: String?) { | ||||
|         parentController!!.router.pushController(GlobalSearchController(query)) | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.source | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.domain.source.interactor.GetEnabledSources | ||||
| import eu.kanade.domain.source.interactor.ToggleSource | ||||
| import eu.kanade.domain.source.interactor.ToggleSourcePin | ||||
| @@ -9,9 +8,10 @@ import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.browse.SourceUiModel | ||||
| import eu.kanade.presentation.browse.SourcesState | ||||
| import eu.kanade.presentation.browse.SourcesStateImpl | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| @@ -22,17 +22,18 @@ import uy.kohesive.injekt.api.get | ||||
| import java.util.TreeMap | ||||
|  | ||||
| class SourcesPresenter( | ||||
|     private val presenterScope: CoroutineScope, | ||||
|     private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl, | ||||
|     private val preferences: PreferencesHelper = Injekt.get(), | ||||
|     private val getEnabledSources: GetEnabledSources = Injekt.get(), | ||||
|     private val toggleSource: ToggleSource = Injekt.get(), | ||||
|     private val toggleSourcePin: ToggleSourcePin = Injekt.get(), | ||||
| ) : BasePresenter<SourcesController>(), SourcesState by state { | ||||
| ) : SourcesState by state { | ||||
|  | ||||
|     private val _events = Channel<Event>(Int.MAX_VALUE) | ||||
|     val events = _events.receiveAsFlow() | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|     fun onCreate() { | ||||
|         presenterScope.launchIO { | ||||
|             getEnabledSources.subscribe() | ||||
|                 .catch { exception -> | ||||
| @@ -76,6 +77,12 @@ class SourcesPresenter( | ||||
|         state.items = uiModels | ||||
|     } | ||||
|  | ||||
|     fun onOpenSource(source: Source) { | ||||
|         if (!preferences.incognitoMode().get()) { | ||||
|             preferences.lastUsedSource().set(source.id) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun toggleSource(source: Source) { | ||||
|         toggleSource.await(source) | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,55 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.source | ||||
|  | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.FilterList | ||||
| import androidx.compose.material.icons.outlined.TravelExplore | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import eu.kanade.presentation.browse.BrowseTab | ||||
| import eu.kanade.presentation.browse.SourcesScreen | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController | ||||
|  | ||||
| @Composable | ||||
| fun sourcesTab( | ||||
|     router: Router?, | ||||
|     presenter: SourcesPresenter, | ||||
| ) = BrowseTab( | ||||
|     titleRes = R.string.label_sources, | ||||
|     actions = listOf( | ||||
|         AppBar.Action( | ||||
|             title = stringResource(R.string.action_global_search), | ||||
|             icon = Icons.Outlined.TravelExplore, | ||||
|             onClick = { router?.pushController(GlobalSearchController()) }, | ||||
|         ), | ||||
|         AppBar.Action( | ||||
|             title = stringResource(R.string.action_filter), | ||||
|             icon = Icons.Outlined.FilterList, | ||||
|             onClick = { router?.pushController(SourceFilterController()) }, | ||||
|         ), | ||||
|     ), | ||||
|     content = { | ||||
|         SourcesScreen( | ||||
|             presenter = presenter, | ||||
|             onClickItem = { source -> | ||||
|                 presenter.onOpenSource(source) | ||||
|                 router?.pushController(BrowseSourceController(source)) | ||||
|             }, | ||||
|             onClickDisable = { source -> | ||||
|                 presenter.toggleSource(source) | ||||
|             }, | ||||
|             onClickLatest = { source -> | ||||
|                 presenter.onOpenSource(source) | ||||
|                 router?.pushController(LatestUpdatesController(source)) | ||||
|             }, | ||||
|             onClickPin = { source -> | ||||
|                 presenter.togglePin(source) | ||||
|             }, | ||||
|         ) | ||||
|     }, | ||||
| ) | ||||
| @@ -45,7 +45,6 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FabController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.TabbedController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.setRoot | ||||
| import eu.kanade.tachiyomi.ui.browse.BrowseController | ||||
| @@ -162,7 +161,7 @@ class MainActivity : BaseActivity() { | ||||
|                     R.id.nav_library -> router.setRoot(LibraryController(), id) | ||||
|                     R.id.nav_updates -> router.setRoot(UpdatesController(), id) | ||||
|                     R.id.nav_history -> router.setRoot(HistoryController(), id) | ||||
|                     R.id.nav_browse -> router.setRoot(BrowseController(), id) | ||||
|                     R.id.nav_browse -> router.setRoot(BrowseController(toExtensions = false), id) | ||||
|                     R.id.nav_more -> router.setRoot(MoreController(), id) | ||||
|                 } | ||||
|             } else if (!isHandlingShortcut) { | ||||
| @@ -590,17 +589,6 @@ class MainActivity : BaseActivity() { | ||||
|             showNav(true) | ||||
|         } | ||||
|  | ||||
|         if (from is TabbedController) { | ||||
|             from.cleanupTabs(binding.tabs) | ||||
|         } | ||||
|         if (internalTo is TabbedController) { | ||||
|             if (internalTo.configureTabs(binding.tabs)) { | ||||
|                 binding.tabs.isVisible = true | ||||
|             } | ||||
|         } else { | ||||
|             binding.tabs.isVisible = false | ||||
|         } | ||||
|  | ||||
|         if (from is FabController) { | ||||
|             from.cleanupFab(binding.fabLayout.rootFab) | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user