Kaynağa Gözat

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
arkon 2 yıl önce
ebeveyn
işleme
92e83f702c
32 değiştirilmiş dosya ile 459 ekleme ve 702 silme
  1. 2 3
      app/src/main/java/eu/kanade/domain/source/interactor/SetMigrateSorting.kt
  2. 84 0
      app/src/main/java/eu/kanade/presentation/browse/BrowseScreen.kt
  3. 0 11
      app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt
  4. 2 0
      app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt
  5. 21 6
      app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt
  6. 5 0
      app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt
  7. 1 8
      app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt
  8. 50 0
      app/src/main/java/eu/kanade/presentation/components/Tabs.kt
  9. 4 33
      app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt
  10. 2 50
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt
  11. 0 13
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt
  12. 31 127
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt
  13. 23 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt
  14. 0 120
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsController.kt
  15. 12 6
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt
  16. 75 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt
  17. 48 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt
  18. 0 65
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt
  19. 30 10
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt
  20. 0 106
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt
  21. 12 5
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt
  22. 55 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt
  23. 1 13
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  24. 0 10
      app/src/main/res/drawable/ic_sort_24dp.xml
  25. 0 9
      app/src/main/res/drawable/ic_translate_24dp.xml
  26. 0 9
      app/src/main/res/drawable/ic_travel_explore_24dp.xml
  27. 0 5
      app/src/main/res/layout-sw720dp/main_activity.xml
  28. 0 6
      app/src/main/res/layout/main_activity.xml
  29. 0 19
      app/src/main/res/menu/browse_extensions.xml
  30. 0 47
      app/src/main/res/menu/browse_migrate.xml
  31. 0 19
      app/src/main/res/menu/browse_sources.xml
  32. 1 2
      gradle/libs.versions.toml

+ 2 - 3
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 {

+ 84 - 0
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<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,
+)

+ 0 - 11
app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt

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

+ 2 - 0
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<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() }
 }

+ 21 - 6
app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt

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

+ 5 - 0
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<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)
 }

+ 1 - 8
app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt

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

+ 50 - 0
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,
+            )
+        }
+    }
+}

+ 4 - 33
app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt

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

+ 2 - 50
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<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)
-}

+ 0 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt

@@ -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) {}
-}

+ 31 - 127
app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt

@@ -1,149 +1,53 @@
 package eu.kanade.tachiyomi.ui.browse
 
+import android.Manifest
 import android.os.Bundle
-import android.view.LayoutInflater
 import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.core.os.bundleOf
-import com.bluelinelabs.conductor.Controller
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.ControllerChangeType
-import com.bluelinelabs.conductor.Router
-import com.bluelinelabs.conductor.RouterTransaction
-import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
-import com.google.android.material.badge.BadgeDrawable
-import com.google.android.material.tabs.TabLayout
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.databinding.PagerControllerBinding
+import eu.kanade.presentation.browse.BrowseScreen
+import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
-import eu.kanade.tachiyomi.ui.base.controller.RxController
-import eu.kanade.tachiyomi.ui.base.controller.TabbedController
-import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsController
-import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
-import eu.kanade.tachiyomi.ui.browse.source.SourcesController
+import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
+import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
+import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourcesTab
+import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import uy.kohesive.injekt.injectLazy
 
-class BrowseController :
-    RxController<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()
-
-    private var adapter: BrowseAdapter? = null
-
-    override fun getTitle(): String? {
-        return resources!!.getString(R.string.browse)
-    }
-
-    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,
+    override fun createPresenter() = BrowsePresenter()
+
+    @Composable
+    override fun ComposeContent() {
+        BrowseScreen(
+            startIndex = 1.takeIf { toExtensions },
+            tabs = listOf(
+                sourcesTab(router, presenter.sourcesPresenter),
+                extensionsTab(router, presenter.extensionsPresenter),
+                migrateSourcesTab(router, presenter.migrationSourcesPresenter),
+            ),
         )
-            .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]
+        LaunchedEffect(Unit) {
+            (activity as? MainActivity)?.ready = true
         }
     }
 
-    companion object {
-        const val TO_EXTENSIONS_EXTRA = "to_extensions"
-
-        const val SOURCES_CONTROLLER = 0
-        const val EXTENSIONS_CONTROLLER = 1
-        const val MIGRATION_CONTROLLER = 2
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+        requestPermissionsSafe(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 301)
     }
 }
+
+private const val TO_EXTENSIONS_EXTRA = "to_extensions"

+ 23 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt

@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.ui.browse
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
+import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
+import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
+import uy.kohesive.injekt.api.get
+
+class BrowsePresenter : BasePresenter<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()
+    }
+}

+ 0 - 120
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsController.kt

@@ -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)
-    }
-}

+ 12 - 6
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<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) {

+ 75 - 0
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()
+            },
+        )
+    },
+)

+ 48 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourcesTab.kt

@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.ui.browse.migration.sources
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.HelpOutline
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import com.bluelinelabs.conductor.Router
+import eu.kanade.presentation.browse.BrowseTab
+import eu.kanade.presentation.browse.MigrateSourceScreen
+import eu.kanade.presentation.components.AppBar
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.pushController
+import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
+
+@Composable
+fun migrateSourcesTab(
+    router: Router?,
+    presenter: MigrationSourcesPresenter,
+): BrowseTab {
+    val uriHandler = LocalUriHandler.current
+
+    return BrowseTab(
+        titleRes = R.string.label_migration,
+        actions = listOf(
+            AppBar.Action(
+                title = stringResource(R.string.migration_help_guide),
+                icon = Icons.Outlined.HelpOutline,
+                onClick = {
+                    uriHandler.openUri("https://tachiyomi.org/help/guides/source-migration/")
+                },
+            ),
+        ),
+        content = {
+            MigrateSourceScreen(
+                presenter = presenter,
+                onClickItem = { source ->
+                    router?.pushController(
+                        MigrationMangaController(
+                            source.id,
+                            source.name,
+                        ),
+                    )
+                },
+            )
+        },
+    )
+}

+ 0 - 65
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt

@@ -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/"

+ 30 - 10
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<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 {

+ 0 - 106
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt

@@ -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))
-    }
-}

+ 12 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt

@@ -1,6 +1,5 @@
 package eu.kanade.tachiyomi.ui.browse.source
 
-import android.os.Bundle
 import eu.kanade.domain.source.interactor.GetEnabledSources
 import eu.kanade.domain.source.interactor.ToggleSource
 import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -9,9 +8,10 @@ import eu.kanade.domain.source.model.Source
 import eu.kanade.presentation.browse.SourceUiModel
 import eu.kanade.presentation.browse.SourcesState
 import eu.kanade.presentation.browse.SourcesStateImpl
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.system.logcat
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collectLatest
@@ -22,17 +22,18 @@ import uy.kohesive.injekt.api.get
 import java.util.TreeMap
 
 class SourcesPresenter(
+    private val presenterScope: CoroutineScope,
     private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
+    private val preferences: PreferencesHelper = Injekt.get(),
     private val getEnabledSources: GetEnabledSources = Injekt.get(),
     private val toggleSource: ToggleSource = Injekt.get(),
     private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
-) : BasePresenter<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)
     }

+ 55 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt

@@ -0,0 +1,55 @@
+package eu.kanade.tachiyomi.ui.browse.source
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.FilterList
+import androidx.compose.material.icons.outlined.TravelExplore
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.bluelinelabs.conductor.Router
+import eu.kanade.presentation.browse.BrowseTab
+import eu.kanade.presentation.browse.SourcesScreen
+import eu.kanade.presentation.components.AppBar
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.pushController
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
+import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
+
+@Composable
+fun sourcesTab(
+    router: Router?,
+    presenter: SourcesPresenter,
+) = BrowseTab(
+    titleRes = R.string.label_sources,
+    actions = listOf(
+        AppBar.Action(
+            title = stringResource(R.string.action_global_search),
+            icon = Icons.Outlined.TravelExplore,
+            onClick = { router?.pushController(GlobalSearchController()) },
+        ),
+        AppBar.Action(
+            title = stringResource(R.string.action_filter),
+            icon = Icons.Outlined.FilterList,
+            onClick = { router?.pushController(SourceFilterController()) },
+        ),
+    ),
+    content = {
+        SourcesScreen(
+            presenter = presenter,
+            onClickItem = { source ->
+                presenter.onOpenSource(source)
+                router?.pushController(BrowseSourceController(source))
+            },
+            onClickDisable = { source ->
+                presenter.toggleSource(source)
+            },
+            onClickLatest = { source ->
+                presenter.onOpenSource(source)
+                router?.pushController(LatestUpdatesController(source))
+            },
+            onClickPin = { source ->
+                presenter.togglePin(source)
+            },
+        )
+    },
+)

+ 1 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -45,7 +45,6 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.ui.base.controller.FabController
 import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
-import eu.kanade.tachiyomi.ui.base.controller.TabbedController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.base.controller.setRoot
 import eu.kanade.tachiyomi.ui.browse.BrowseController
@@ -162,7 +161,7 @@ class MainActivity : BaseActivity() {
                     R.id.nav_library -> router.setRoot(LibraryController(), id)
                     R.id.nav_updates -> router.setRoot(UpdatesController(), id)
                     R.id.nav_history -> router.setRoot(HistoryController(), id)
-                    R.id.nav_browse -> router.setRoot(BrowseController(), id)
+                    R.id.nav_browse -> router.setRoot(BrowseController(toExtensions = false), id)
                     R.id.nav_more -> router.setRoot(MoreController(), id)
                 }
             } else if (!isHandlingShortcut) {
@@ -590,17 +589,6 @@ class MainActivity : BaseActivity() {
             showNav(true)
         }
 
-        if (from is TabbedController) {
-            from.cleanupTabs(binding.tabs)
-        }
-        if (internalTo is TabbedController) {
-            if (internalTo.configureTabs(binding.tabs)) {
-                binding.tabs.isVisible = true
-            }
-        } else {
-            binding.tabs.isVisible = false
-        }
-
         if (from is FabController) {
             from.cleanupFab(binding.fabLayout.rootFab)
         }

+ 0 - 10
app/src/main/res/drawable/ic_sort_24dp.xml

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

+ 0 - 9
app/src/main/res/drawable/ic_translate_24dp.xml

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

+ 0 - 9
app/src/main/res/drawable/ic_travel_explore_24dp.xml

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

+ 0 - 5
app/src/main/res/layout-sw720dp/main_activity.xml

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

+ 0 - 6
app/src/main/res/layout/main_activity.xml

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

+ 0 - 19
app/src/main/res/menu/browse_extensions.xml

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

+ 0 - 47
app/src/main/res/menu/browse_migrate.xml

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

+ 0 - 19
app/src/main/res/menu/browse_sources.xml

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

+ 1 - 2
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]