Use Voyager on Browse tab (#8605)

(cherry picked from commit f4ac754d02)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt
#	app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt
#	app/src/main/java/eu/kanade/presentation/browse/SourcesState.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowsePresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt
This commit is contained in:
Ivan Iskandar
2022-11-24 10:28:25 +07:00
committed by Jobobby04
parent 0b9b6612fd
commit bf9b2ca2ff
33 changed files with 821 additions and 884 deletions
@@ -1,6 +1,9 @@
package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
@@ -9,6 +12,7 @@ import eu.kanade.presentation.components.TabContent
@Composable
fun BrowseTabWrapper(tab: TabContent) {
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@@ -19,7 +23,8 @@ fun BrowseTabWrapper(tab: TabContent) {
scrollBehavior = scrollBehavior,
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
tab.content(paddingValues)
tab.content(paddingValues, snackbarHostState)
}
}
@@ -53,13 +53,13 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
import eu.kanade.tachiyomi.util.system.LocaleHelper
import exh.source.anyIs
@Composable
fun ExtensionScreen(
presenter: ExtensionsPresenter,
state: ExtensionsState,
contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
@@ -72,19 +72,19 @@ fun ExtensionScreen(
onRefresh: () -> Unit,
) {
SwipeRefresh(
refreshing = presenter.isRefreshing,
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !presenter.isLoading,
enabled = !state.isLoading,
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
ExtensionContent(
state = presenter,
state = state,
contentPadding = contentPadding,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
@@ -1,27 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
interface ExtensionsState {
val isLoading: Boolean
val isRefreshing: Boolean
val items: List<ExtensionUiModel>
val updates: Int
val isEmpty: Boolean
}
fun ExtensionState(): ExtensionsState {
return ExtensionsStateImpl()
}
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() }
}
@@ -53,7 +53,7 @@ import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenState
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import eu.kanade.domain.manga.model.MangaCover as MangaCoverData
@@ -69,97 +69,39 @@ data class FeedItemUI(
@Composable
fun FeedScreen(
presenter: FeedPresenter,
state: FeedScreenState,
contentPadding: PaddingValues,
onClickAdd: (CatalogueSource) -> Unit,
onClickCreate: (CatalogueSource, SavedSearch?) -> Unit,
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen()
state.isEmpty -> EmptyScreen(
textResource = R.string.feed_tab_empty,
modifier = Modifier.padding(contentPadding),
)
else -> {
FeedList(
state = presenter,
contentPadding = contentPadding,
getMangaState = { item, source -> presenter.getManga(item, source) },
onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource,
onClickDelete = onClickDelete,
onClickManga = onClickManga,
)
}
}
when (val dialog = presenter.dialog) {
is FeedPresenter.Dialog.AddFeed -> {
FeedAddDialog(
sources = dialog.options,
onDismiss = { presenter.dialog = null },
onClickAdd = {
presenter.dialog = null
onClickAdd(it ?: return@FeedAddDialog)
},
)
}
is FeedPresenter.Dialog.AddFeedSearch -> {
FeedAddSearchDialog(
source = dialog.source,
savedSearches = dialog.options,
onDismiss = { presenter.dialog = null },
onClickAdd = { source, savedSearch ->
presenter.dialog = null
onClickCreate(source, savedSearch)
},
)
}
is FeedPresenter.Dialog.DeleteFeed -> {
FeedDeleteConfirmDialog(
feed = dialog.feed,
onDismiss = { presenter.dialog = null },
onClickDeleteConfirm = {
presenter.dialog = null
onClickDeleteConfirm(it)
},
)
}
null -> Unit
}
}
@Composable
fun FeedList(
state: FeedState,
contentPadding: PaddingValues,
getMangaState: @Composable ((Manga, CatalogueSource?) -> State<Manga>),
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
items(
state.items.orEmpty(),
key = { it.feed.id },
) { item ->
FeedItem(
modifier = Modifier.animateItemPlacement(),
item = item,
getMangaState = { getMangaState(it, item.source) },
onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource,
onClickDelete = onClickDelete,
onClickManga = onClickManga,
)
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
items(
state.items.orEmpty(),
key = { it.feed.id },
) { item ->
FeedItem(
modifier = Modifier.animateItemPlacement(),
item = item,
getMangaState = { getMangaState(it, item.source) },
onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource,
onClickDelete = onClickDelete,
onClickManga = onClickManga,
)
}
}
}
}
}
@@ -1,26 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter
@Stable
interface FeedState {
var dialog: FeedPresenter.Dialog?
val isLoading: Boolean
val isEmpty: Boolean
val items: List<FeedItemUI>?
}
fun FeedState(): FeedState {
return FeedStateImpl()
}
class FeedStateImpl : FeedState {
override var dialog: FeedPresenter.Dialog? by mutableStateOf(null)
override var isLoading: Boolean by mutableStateOf(true)
override var isEmpty: Boolean by mutableStateOf(false)
override var items: List<FeedItemUI>? by mutableStateOf(null)
}
@@ -41,38 +41,40 @@ import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable
fun MigrateSourceScreen(
presenter: MigrationSourcesPresenter,
state: MigrateSourceState,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
onToggleSortingDirection: () -> Unit,
onToggleSortingMode: () -> Unit,
// SY -->
onClickAll: (Source) -> Unit,
// SY <--
) {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding),
)
else ->
MigrateSourceList(
list = presenter.items,
list = state.items,
contentPadding = contentPadding,
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() },
sortingMode = state.sortingMode,
onToggleSortingMode = onToggleSortingMode,
sortingDirection = state.sortingDirection,
onToggleSortingDirection = onToggleSortingDirection,
// SY -->
onClickAll = onClickAll,
// SY <--
@@ -1,28 +0,0 @@
package eu.kanade.presentation.browse
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 {
return MigrateSourceStateImpl()
}
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)
}
@@ -19,7 +19,6 @@ 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.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -40,153 +39,70 @@ import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
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.ui.browse.source.SourcesState
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesScreen(
presenter: SourcesPresenter,
state: SourcesState,
contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit,
// SY -->
onClickSetCategories: (Source, List<String>) -> Unit,
onClickToggleDataSaver: (Source) -> Unit,
// SY <--
onLongClickItem: (Source) -> Unit,
) {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
SourceList(
state = presenter,
contentPadding = contentPadding,
onClickItem = onClickItem,
onClickDisable = onClickDisable,
onClickPin = onClickPin,
// SY -->
onClickSetCategories = onClickSetCategories,
onClickToggleDataSaver = onClickToggleDataSaver,
// SY <--
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesPresenter.Event.FailedFetchingSources -> {
context.toast(R.string.internal_error)
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
items(
items = state.items,
contentType = {
when (it) {
is SourceUiModel.Header -> "header"
is SourceUiModel.Item -> "item"
}
},
key = {
when (it) {
is SourceUiModel.Header -> it.hashCode()
is SourceUiModel.Item -> "source-${it.source.key()}"
}
},
) { model ->
when (model) {
is SourceUiModel.Header -> {
SourceHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
// SY -->
isCategory = model.isCategory,
// SY <--
)
}
is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
// SY -->
showLatest = state.showLatest,
showPin = state.showPin,
// SY <--
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onClickPin = onClickPin,
)
}
}
}
}
}
}
@Composable
private fun SourceList(
state: SourcesState,
contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit,
// SY -->
onClickSetCategories: (Source, List<String>) -> Unit,
onClickToggleDataSaver: (Source) -> Unit,
// SY <--
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
items(
items = state.items,
contentType = {
when (it) {
is SourceUiModel.Header -> "header"
is SourceUiModel.Item -> "item"
}
},
key = {
when (it) {
is SourceUiModel.Header -> it.hashCode()
is SourceUiModel.Item -> "source-${it.source.key()}"
}
},
) { model ->
when (model) {
is SourceUiModel.Header -> {
SourceHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
// SY -->
isCategory = model.isCategory,
// SY <--
)
}
is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
// SY -->
showLatest = state.showLatest,
showPin = state.showPin,
// SY <--
onClickItem = onClickItem,
// SY -->
onLongClickItem = { state.dialog = Dialog.SourceLongClick(it) },
// SY <--
onClickPin = onClickPin,
)
}
}
}
// SY -->
when (val dialog = state.dialog) {
is Dialog.SourceCategories -> {
SourceCategoriesDialog(
source = dialog.source,
categories = state.categories,
onClickCategories = { source, newCategories ->
onClickSetCategories(source, newCategories)
state.dialog = null
},
onDismiss = { state.dialog = null },
)
}
is Dialog.SourceLongClick -> {
val source = dialog.source
SourceOptionsDialog(
source = source,
onClickPin = {
onClickPin(source)
state.dialog = null
},
onClickDisable = {
onClickDisable(source)
state.dialog = null
},
onClickSetCategories = {
state.dialog = Dialog.SourceCategories(source)
},
onClickToggleDataSaver = {
onClickToggleDataSaver(source)
state.dialog = null
},
onDismiss = { state.dialog = null },
)
}
null -> Unit
}
// SY <--
}
@Composable
private fun SourceHeader(
modifier: Modifier = Modifier,
@@ -268,7 +184,7 @@ private fun SourcePinButton(
}
@Composable
private fun SourceOptionsDialog(
fun SourceOptionsDialog(
source: Source,
onClickPin: () -> Unit,
onClickDisable: () -> Unit,
@@ -338,7 +254,7 @@ sealed class SourceUiModel {
fun SourceCategoriesDialog(
source: Source?,
categories: List<String>,
onClickCategories: (Source, List<String>) -> Unit,
onClickCategories: (List<String>) -> Unit,
onDismiss: () -> Unit,
) {
source ?: return
@@ -379,7 +295,7 @@ fun SourceCategoriesDialog(
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickCategories(source, newCategories.toList()) }) {
TextButton(onClick = { onClickCategories(newCategories.toList()) }) {
Text(text = stringResource(android.R.string.ok))
}
},
@@ -1,39 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
@Stable
interface SourcesState {
var dialog: SourcesPresenter.Dialog?
val isLoading: Boolean
val items: List<SourceUiModel>
val isEmpty: Boolean
// SY -->
val showPin: Boolean
val showLatest: Boolean
val categories: List<String>
// SY <--
}
fun SourcesState(): SourcesState {
return SourcesStateImpl()
}
class SourcesStateImpl : SourcesState {
override var dialog: SourcesPresenter.Dialog? by mutableStateOf(null)
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<SourceUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
// SY -->
override var showLatest: Boolean by mutableStateOf(true)
override var showPin: Boolean by mutableStateOf(true)
override var categories: List<String> by mutableStateOf(emptyList())
// SY <--
}
@@ -8,10 +8,13 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,6 +35,7 @@ fun TabbedScreen(
) {
val scope = rememberCoroutineScope()
val state = rememberPagerState()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(startIndex) {
if (startIndex != null) {
@@ -52,6 +56,7 @@ fun TabbedScreen(
actions = { AppBarActions(tab.actions) },
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { contentPadding ->
Column(
modifier = Modifier.padding(
@@ -86,6 +91,7 @@ fun TabbedScreen(
TachiyomiBottomNavigationView.withBottomNavPadding(
PaddingValues(bottom = contentPadding.calculateBottomPadding()),
),
snackbarHostState,
)
}
}
@@ -97,5 +103,5 @@ data class TabContent(
val badgeNumber: Int? = null,
val searchEnabled: Boolean = false,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable (contentPadding: PaddingValues) -> Unit,
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
)
@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@@ -22,7 +21,6 @@ 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.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -37,7 +35,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.google.accompanist.permissions.rememberPermissionState
import com.hippo.unifile.UniFile
import eu.kanade.domain.backup.service.BackupPreferences
import eu.kanade.presentation.components.Divider
@@ -52,6 +49,7 @@ import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
@@ -70,7 +68,7 @@ object SettingsBackupScreen : SearchableSettings {
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
RequestStoragePermission()
DiskUtil.RequestStoragePermission()
return listOf(
getCreateBackupPref(),
@@ -79,14 +77,6 @@ object SettingsBackupScreen : SearchableSettings {
)
}
@Composable
private fun RequestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
@Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope()