mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +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