Use Voyager on Browse tab (#8605)
(cherry picked from commit f4ac754d02242f33e78a15f98959d6e59bd967c9) # 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:
parent
0b9b6612fd
commit
bf9b2ca2ff
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
@ -9,10 +8,10 @@ class GetShowLatest(
|
|||||||
private val preferences: UiPreferences,
|
private val preferences: UiPreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(mode: SourcesController.Mode): Flow<Boolean> {
|
fun subscribe(hasSmartSearchConfig: Boolean): Flow<Boolean> {
|
||||||
return preferences.useNewSourceNavigation().changes()
|
return preferences.useNewSourceNavigation().changes()
|
||||||
.map {
|
.map {
|
||||||
mode == SourcesController.Mode.CATALOGUE && !it
|
!hasSmartSearchConfig && !it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package eu.kanade.presentation.browse
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
@ -9,6 +12,7 @@ import eu.kanade.presentation.components.TabContent
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseTabWrapper(tab: TabContent) {
|
fun BrowseTabWrapper(tab: TabContent) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
@ -19,7 +23,8 @@ fun BrowseTabWrapper(tab: TabContent) {
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { 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.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
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 eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import exh.source.anyIs
|
import exh.source.anyIs
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionScreen(
|
fun ExtensionScreen(
|
||||||
presenter: ExtensionsPresenter,
|
state: ExtensionsState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
@ -72,19 +72,19 @@ fun ExtensionScreen(
|
|||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
SwipeRefresh(
|
SwipeRefresh(
|
||||||
refreshing = presenter.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
enabled = !presenter.isLoading,
|
enabled = !state.isLoading,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.empty_screen,
|
textResource = R.string.empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
ExtensionContent(
|
ExtensionContent(
|
||||||
state = presenter,
|
state = state,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemCancel = onClickItemCancel,
|
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.presentation.util.topSmallPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.FeedSavedSearch
|
||||||
import exh.savedsearches.models.SavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
import eu.kanade.domain.manga.model.MangaCover as MangaCoverData
|
import eu.kanade.domain.manga.model.MangaCover as MangaCoverData
|
||||||
@ -69,97 +69,39 @@ data class FeedItemUI(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedScreen(
|
fun FeedScreen(
|
||||||
presenter: FeedPresenter,
|
state: FeedScreenState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickAdd: (CatalogueSource) -> Unit,
|
|
||||||
onClickCreate: (CatalogueSource, SavedSearch?) -> Unit,
|
|
||||||
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
|
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickDelete: (FeedSavedSearch) -> Unit,
|
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||||
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
|
|
||||||
onClickManga: (Manga) -> Unit,
|
onClickManga: (Manga) -> Unit,
|
||||||
|
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen()
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.feed_tab_empty,
|
textResource = R.string.feed_tab_empty,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
FeedList(
|
ScrollbarLazyColumn(
|
||||||
state = presenter,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
contentPadding = contentPadding,
|
) {
|
||||||
getMangaState = { item, source -> presenter.getManga(item, source) },
|
items(
|
||||||
onClickSavedSearch = onClickSavedSearch,
|
state.items.orEmpty(),
|
||||||
onClickSource = onClickSource,
|
key = { it.feed.id },
|
||||||
onClickDelete = onClickDelete,
|
) { item ->
|
||||||
onClickManga = onClickManga,
|
FeedItem(
|
||||||
)
|
modifier = Modifier.animateItemPlacement(),
|
||||||
}
|
item = item,
|
||||||
}
|
getMangaState = { getMangaState(it, item.source) },
|
||||||
|
onClickSavedSearch = onClickSavedSearch,
|
||||||
when (val dialog = presenter.dialog) {
|
onClickSource = onClickSource,
|
||||||
is FeedPresenter.Dialog.AddFeed -> {
|
onClickDelete = onClickDelete,
|
||||||
FeedAddDialog(
|
onClickManga = onClickManga,
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.secondaryItemAlpha
|
||||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
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
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSourceScreen(
|
fun MigrateSourceScreen(
|
||||||
presenter: MigrationSourcesPresenter,
|
state: MigrateSourceState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source) -> Unit,
|
onClickItem: (Source) -> Unit,
|
||||||
|
onToggleSortingDirection: () -> Unit,
|
||||||
|
onToggleSortingMode: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickAll: (Source) -> Unit,
|
onClickAll: (Source) -> Unit,
|
||||||
// SY <--
|
// SY <--
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.information_empty_library,
|
textResource = R.string.information_empty_library,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else ->
|
else ->
|
||||||
MigrateSourceList(
|
MigrateSourceList(
|
||||||
list = presenter.items,
|
list = state.items,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = { source ->
|
onLongClickItem = { source ->
|
||||||
val sourceId = source.id.toString()
|
val sourceId = source.id.toString()
|
||||||
context.copyToClipboard(sourceId, sourceId)
|
context.copyToClipboard(sourceId, sourceId)
|
||||||
},
|
},
|
||||||
sortingMode = presenter.sortingMode,
|
sortingMode = state.sortingMode,
|
||||||
onToggleSortingMode = { presenter.toggleSortingMode() },
|
onToggleSortingMode = onToggleSortingMode,
|
||||||
sortingDirection = presenter.sortingDirection,
|
sortingDirection = state.sortingDirection,
|
||||||
onToggleSortingDirection = { presenter.toggleSortingDirection() },
|
onToggleSortingDirection = onToggleSortingDirection,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickAll = onClickAll,
|
onClickAll = onClickAll,
|
||||||
// SY <--
|
// 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.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -40,153 +39,70 @@ import eu.kanade.presentation.util.plus
|
|||||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter.Dialog
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesScreen(
|
fun SourcesScreen(
|
||||||
presenter: SourcesPresenter,
|
state: SourcesState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source, String) -> Unit,
|
onClickItem: (Source, String) -> Unit,
|
||||||
onClickDisable: (Source) -> Unit,
|
|
||||||
onClickPin: (Source) -> Unit,
|
onClickPin: (Source) -> Unit,
|
||||||
// SY -->
|
onLongClickItem: (Source) -> Unit,
|
||||||
onClickSetCategories: (Source, List<String>) -> Unit,
|
|
||||||
onClickToggleDataSaver: (Source) -> Unit,
|
|
||||||
// SY <--
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.source_empty_screen,
|
textResource = R.string.source_empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
SourceList(
|
ScrollbarLazyColumn(
|
||||||
state = presenter,
|
contentPadding = contentPadding + topSmallPaddingValues,
|
||||||
contentPadding = contentPadding,
|
) {
|
||||||
onClickItem = onClickItem,
|
items(
|
||||||
onClickDisable = onClickDisable,
|
items = state.items,
|
||||||
onClickPin = onClickPin,
|
contentType = {
|
||||||
// SY -->
|
when (it) {
|
||||||
onClickSetCategories = onClickSetCategories,
|
is SourceUiModel.Header -> "header"
|
||||||
onClickToggleDataSaver = onClickToggleDataSaver,
|
is SourceUiModel.Item -> "item"
|
||||||
// SY <--
|
}
|
||||||
)
|
},
|
||||||
}
|
key = {
|
||||||
}
|
when (it) {
|
||||||
LaunchedEffect(Unit) {
|
is SourceUiModel.Header -> it.hashCode()
|
||||||
presenter.events.collectLatest { event ->
|
is SourceUiModel.Item -> "source-${it.source.key()}"
|
||||||
when (event) {
|
}
|
||||||
SourcesPresenter.Event.FailedFetchingSources -> {
|
},
|
||||||
context.toast(R.string.internal_error)
|
) { 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
|
@Composable
|
||||||
private fun SourceHeader(
|
private fun SourceHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@ -268,7 +184,7 @@ private fun SourcePinButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SourceOptionsDialog(
|
fun SourceOptionsDialog(
|
||||||
source: Source,
|
source: Source,
|
||||||
onClickPin: () -> Unit,
|
onClickPin: () -> Unit,
|
||||||
onClickDisable: () -> Unit,
|
onClickDisable: () -> Unit,
|
||||||
@ -338,7 +254,7 @@ sealed class SourceUiModel {
|
|||||||
fun SourceCategoriesDialog(
|
fun SourceCategoriesDialog(
|
||||||
source: Source?,
|
source: Source?,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
onClickCategories: (Source, List<String>) -> Unit,
|
onClickCategories: (List<String>) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
source ?: return
|
source ?: return
|
||||||
@ -379,7 +295,7 @@ fun SourceCategoriesDialog(
|
|||||||
},
|
},
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { onClickCategories(source, newCategories.toList()) }) {
|
TextButton(onClick = { onClickCategories(newCategories.toList()) }) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -32,6 +35,7 @@ fun TabbedScreen(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state = rememberPagerState()
|
val state = rememberPagerState()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
LaunchedEffect(startIndex) {
|
LaunchedEffect(startIndex) {
|
||||||
if (startIndex != null) {
|
if (startIndex != null) {
|
||||||
@ -52,6 +56,7 @@ fun TabbedScreen(
|
|||||||
actions = { AppBarActions(tab.actions) },
|
actions = { AppBarActions(tab.actions) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
@ -86,6 +91,7 @@ fun TabbedScreen(
|
|||||||
TachiyomiBottomNavigationView.withBottomNavPadding(
|
TachiyomiBottomNavigationView.withBottomNavPadding(
|
||||||
PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||||
),
|
),
|
||||||
|
snackbarHostState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,5 +103,5 @@ data class TabContent(
|
|||||||
val badgeNumber: Int? = null,
|
val badgeNumber: Int? = null,
|
||||||
val searchEnabled: Boolean = false,
|
val searchEnabled: Boolean = false,
|
||||||
val actions: List<AppBar.Action> = emptyList(),
|
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
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -22,7 +21,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.domain.backup.service.BackupPreferences
|
import eu.kanade.domain.backup.service.BackupPreferences
|
||||||
import eu.kanade.presentation.components.Divider
|
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.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
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.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@ -70,7 +68,7 @@ object SettingsBackupScreen : SearchableSettings {
|
|||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||||
|
|
||||||
RequestStoragePermission()
|
DiskUtil.RequestStoragePermission()
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
getCreateBackupPref(),
|
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
|
@Composable
|
||||||
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
@ -1,25 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import eu.kanade.presentation.components.TabbedScreen
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.feed.feedTab
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourcesTab
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
|
|
||||||
class BrowseController : FullComposeController<BrowsePresenter>, RootController {
|
class BrowseController : BasicFullComposeController, RootController {
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false)
|
constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false)
|
||||||
@ -30,47 +18,9 @@ class BrowseController : FullComposeController<BrowsePresenter>, RootController
|
|||||||
|
|
||||||
private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false)
|
private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false)
|
||||||
|
|
||||||
override fun createPresenter() = BrowsePresenter()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
val query by presenter.extensionsPresenter.query.collectAsState()
|
Navigator(screen = BrowseScreen(toExtensions = toExtensions))
|
||||||
|
|
||||||
TabbedScreen(
|
|
||||||
titleRes = R.string.browse,
|
|
||||||
// SY -->
|
|
||||||
tabs = (
|
|
||||||
if (presenter.feedTabInFront) {
|
|
||||||
listOf(
|
|
||||||
feedTab(router, presenter.feedPresenter),
|
|
||||||
sourcesTab(router, presenter.sourcesPresenter),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
sourcesTab(router, presenter.sourcesPresenter),
|
|
||||||
feedTab(router, presenter.feedPresenter),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) + listOf(
|
|
||||||
extensionsTab(router, presenter.extensionsPresenter),
|
|
||||||
migrateSourcesTab(router, presenter.migrationSourcesPresenter),
|
|
||||||
),
|
|
||||||
startIndex = 2.takeIf { toExtensions },
|
|
||||||
// SY <--
|
|
||||||
searchQuery = query,
|
|
||||||
onChangeSearchQuery = { presenter.extensionsPresenter.search(it) },
|
|
||||||
incognitoMode = presenter.isIncognitoMode,
|
|
||||||
downloadedOnlyMode = presenter.isDownloadOnly,
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
(activity as? MainActivity)?.ready = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
requestPermissionsSafe(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 301)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class BrowsePresenter(
|
|
||||||
preferences: BasePreferences = Injekt.get(),
|
|
||||||
// SY -->
|
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
|
||||||
// SY <--
|
|
||||||
) : BasePresenter<BrowseController>() {
|
|
||||||
|
|
||||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
|
||||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
val feedTabInFront = uiPreferences.feedTabInFront().get()
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
val sourcesPresenter = SourcesPresenter(presenterScope, controllerMode = SourcesController.Mode.CATALOGUE, smartSearchConfig = null)
|
|
||||||
val feedPresenter = FeedPresenter(presenterScope)
|
|
||||||
|
|
||||||
// SY <--
|
|
||||||
val extensionsPresenter = ExtensionsPresenter(presenterScope)
|
|
||||||
val migrationSourcesPresenter = MigrationSourcesPresenter(presenterScope)
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
sourcesPresenter.onCreate()
|
|
||||||
// SY -->
|
|
||||||
feedPresenter.onCreate()
|
|
||||||
// SY <--
|
|
||||||
extensionsPresenter.onCreate()
|
|
||||||
migrationSourcesPresenter.onCreate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
feedPresenter.onDestroy()
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
|
@ -0,0 +1,87 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import eu.kanade.core.prefs.asState
|
||||||
|
import eu.kanade.domain.base.BasePreferences
|
||||||
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
|
import eu.kanade.presentation.components.TabbedScreen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.feed.feedTab
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourceTab
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
data class BrowseScreen(
|
||||||
|
private val toExtensions: Boolean,
|
||||||
|
) : Screen {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val screenModel = rememberScreenModel { BrowseScreenModel() }
|
||||||
|
|
||||||
|
// Hoisted for extensions tab's search bar
|
||||||
|
val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() }
|
||||||
|
val extensionsQuery by extensionsScreenModel.query.collectAsState()
|
||||||
|
|
||||||
|
TabbedScreen(
|
||||||
|
titleRes = R.string.browse,
|
||||||
|
// SY -->
|
||||||
|
tabs = if (screenModel.feedTabInFront) {
|
||||||
|
listOf(
|
||||||
|
feedTab(),
|
||||||
|
sourcesTab(),
|
||||||
|
extensionsTab(extensionsScreenModel),
|
||||||
|
migrateSourceTab(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
sourcesTab(),
|
||||||
|
feedTab(),
|
||||||
|
extensionsTab(extensionsScreenModel),
|
||||||
|
migrateSourceTab(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
startIndex = 2.takeIf { toExtensions },
|
||||||
|
// SY <--
|
||||||
|
searchQuery = extensionsQuery,
|
||||||
|
onChangeSearchQuery = extensionsScreenModel::search,
|
||||||
|
incognitoMode = screenModel.isIncognitoMode,
|
||||||
|
downloadedOnlyMode = screenModel.isDownloadOnly,
|
||||||
|
)
|
||||||
|
|
||||||
|
// For local source
|
||||||
|
DiskUtil.RequestStoragePermission()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
(context as? MainActivity)?.ready = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BrowseScreenModel(
|
||||||
|
preferences: BasePreferences = Injekt.get(),
|
||||||
|
// SY -->
|
||||||
|
uiPreferences: UiPreferences = Injekt.get(),
|
||||||
|
// SY <--
|
||||||
|
) : ScreenModel {
|
||||||
|
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
|
||||||
|
val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
val feedTabInFront = uiPreferences.feedTabInFront().get()
|
||||||
|
// SY <--
|
||||||
|
}
|
@ -2,11 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
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.R
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
@ -14,8 +13,6 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@ -23,26 +20,23 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class ExtensionsPresenter(
|
class ExtensionsScreenModel(
|
||||||
private val presenterScope: CoroutineScope,
|
preferences: SourcePreferences = Injekt.get(),
|
||||||
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
|
|
||||||
private val preferences: SourcePreferences = Injekt.get(),
|
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
||||||
) : ExtensionsState by state {
|
) : StateScreenModel<ExtensionsState>(ExtensionsState()) {
|
||||||
|
|
||||||
private val _query: MutableStateFlow<String?> = MutableStateFlow(null)
|
private val _query: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||||
val query: StateFlow<String?> = _query.asStateFlow()
|
val query: StateFlow<String?> = _query.asStateFlow()
|
||||||
|
|
||||||
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
|
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
|
||||||
|
|
||||||
fun onCreate() {
|
init {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map ->
|
val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map ->
|
||||||
{
|
{
|
||||||
@ -76,7 +70,7 @@ class ExtensionsPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
combine(
|
combine(
|
||||||
_query,
|
_query,
|
||||||
_currentDownloads,
|
_currentDownloads,
|
||||||
@ -117,39 +111,44 @@ class ExtensionsPresenter(
|
|||||||
|
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
.onStart { delay(500) } // Defer to avoid crashing on initial render
|
|
||||||
.collectLatest {
|
.collectLatest {
|
||||||
state.isLoading = false
|
mutableState.update { state ->
|
||||||
state.items = it
|
state.copy(
|
||||||
|
isLoading = false,
|
||||||
|
items = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
presenterScope.launchIO { findAvailableExtensions() }
|
coroutineScope.launchIO { findAvailableExtensions() }
|
||||||
|
|
||||||
preferences.extensionUpdatesCount().changes()
|
preferences.extensionUpdatesCount().changes()
|
||||||
.onEach { state.updates = it }
|
.onEach { mutableState.update { state -> state.copy(updates = it) } }
|
||||||
.launchIn(presenterScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
_query.emit(query)
|
_query.emit(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAllExtensions() {
|
fun updateAllExtensions() {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
if (state.isEmpty) return@launchIO
|
with(state.value) {
|
||||||
state.items
|
if (isEmpty) return@launchIO
|
||||||
.mapNotNull {
|
items
|
||||||
when {
|
.mapNotNull {
|
||||||
it !is ExtensionUiModel.Item -> null
|
when {
|
||||||
it.extension !is Extension.Installed -> null
|
it !is ExtensionUiModel.Item -> null
|
||||||
!it.extension.hasUpdate -> null
|
it.extension !is Extension.Installed -> null
|
||||||
else -> it.extension
|
!it.extension.hasUpdate -> null
|
||||||
|
else -> it.extension
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.forEach { updateExtension(it) }
|
||||||
.forEach { updateExtension(it) }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,11 +194,11 @@ class ExtensionsPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findAvailableExtensions() {
|
fun findAvailableExtensions() {
|
||||||
presenterScope.launchIO {
|
mutableState.update { it.copy(isRefreshing = true) }
|
||||||
state.isRefreshing = true
|
coroutineScope.launchIO {
|
||||||
extensionManager.findAvailableExtensions()
|
extensionManager.findAvailableExtensions()
|
||||||
state.isRefreshing = false
|
|
||||||
}
|
}
|
||||||
|
mutableState.update { it.copy(isRefreshing = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trustSignature(signatureHash: String) {
|
fun trustSignature(signatureHash: String) {
|
||||||
@ -207,6 +206,15 @@ class ExtensionsPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ExtensionsState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val isRefreshing: Boolean = false,
|
||||||
|
val items: List<ExtensionUiModel> = emptyList(),
|
||||||
|
val updates: Int = 0,
|
||||||
|
) {
|
||||||
|
val isEmpty = items.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface ExtensionUiModel {
|
sealed interface ExtensionUiModel {
|
||||||
sealed interface Header : ExtensionUiModel {
|
sealed interface Header : ExtensionUiModel {
|
||||||
data class Resource(@StringRes val textRes: Int) : Header
|
data class Resource(@StringRes val textRes: Int) : Header
|
@ -3,11 +3,14 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Translate
|
import androidx.compose.material.icons.outlined.Translate
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bluelinelabs.conductor.Router
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.browse.ExtensionScreen
|
import eu.kanade.presentation.browse.ExtensionScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
@ -15,53 +18,41 @@ import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsControlle
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun extensionsTab(
|
fun extensionsTab(
|
||||||
router: Router?,
|
extensionsScreenModel: ExtensionsScreenModel,
|
||||||
presenter: ExtensionsPresenter,
|
): TabContent {
|
||||||
) = TabContent(
|
val router = LocalRouter.currentOrThrow
|
||||||
titleRes = R.string.label_extensions,
|
val state by extensionsScreenModel.state.collectAsState()
|
||||||
badgeNumber = presenter.updates.takeIf { it > 0 },
|
|
||||||
searchEnabled = true,
|
return TabContent(
|
||||||
actions = listOf(
|
titleRes = R.string.label_extensions,
|
||||||
AppBar.Action(
|
badgeNumber = state.updates.takeIf { it > 0 },
|
||||||
title = stringResource(R.string.action_filter),
|
searchEnabled = true,
|
||||||
icon = Icons.Outlined.Translate,
|
actions = listOf(
|
||||||
onClick = { router?.pushController(ExtensionFilterController()) },
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_filter),
|
||||||
|
icon = Icons.Outlined.Translate,
|
||||||
|
onClick = { router.pushController(ExtensionFilterController()) },
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
content = { contentPadding, _ ->
|
||||||
content = { contentPadding ->
|
ExtensionScreen(
|
||||||
ExtensionScreen(
|
state = state,
|
||||||
presenter = presenter,
|
contentPadding = contentPadding,
|
||||||
contentPadding = contentPadding,
|
onLongClickItem = { extension ->
|
||||||
onLongClickItem = { extension ->
|
when (extension) {
|
||||||
when (extension) {
|
is Extension.Available -> extensionsScreenModel.installExtension(extension)
|
||||||
is Extension.Available -> presenter.installExtension(extension)
|
else -> extensionsScreenModel.uninstallExtension(extension.pkgName)
|
||||||
else -> presenter.uninstallExtension(extension.pkgName)
|
}
|
||||||
}
|
},
|
||||||
},
|
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
|
||||||
onClickItemCancel = { extension ->
|
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
|
||||||
presenter.cancelInstallUpdateExtension(extension)
|
onInstallExtension = extensionsScreenModel::installExtension,
|
||||||
},
|
onOpenExtension = { router.pushController(ExtensionDetailsController(it.pkgName)) },
|
||||||
onClickUpdateAll = {
|
onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) },
|
||||||
presenter.updateAllExtensions()
|
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it.pkgName) },
|
||||||
},
|
onUpdateExtension = extensionsScreenModel::updateExtension,
|
||||||
onInstallExtension = {
|
onRefresh = extensionsScreenModel::findAvailableExtensions,
|
||||||
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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.feed
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
import eu.kanade.domain.manga.interactor.GetManga
|
import eu.kanade.domain.manga.interactor.GetManga
|
||||||
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
|
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
@ -16,8 +18,6 @@ import eu.kanade.domain.source.interactor.GetSavedSearchGlobalFeed
|
|||||||
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.browse.FeedItemUI
|
import eu.kanade.presentation.browse.FeedItemUI
|
||||||
import eu.kanade.presentation.browse.FeedState
|
|
||||||
import eu.kanade.presentation.browse.FeedStateImpl
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
@ -30,11 +30,13 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import exh.savedsearches.models.FeedSavedSearch
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
import exh.savedsearches.models.SavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -51,9 +53,7 @@ import eu.kanade.domain.manga.model.Manga as DomainManga
|
|||||||
/**
|
/**
|
||||||
* Presenter of [feedTab]
|
* Presenter of [feedTab]
|
||||||
*/
|
*/
|
||||||
open class FeedPresenter(
|
open class FeedScreenModel(
|
||||||
private val presenterScope: CoroutineScope,
|
|
||||||
private val state: FeedStateImpl = FeedState() as FeedStateImpl,
|
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val sourcePreferences: SourcePreferences = Injekt.get(),
|
val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
@ -65,14 +65,17 @@ open class FeedPresenter(
|
|||||||
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
|
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
|
||||||
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
||||||
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
||||||
) : FeedState by state {
|
) : StateScreenModel<FeedScreenState>(FeedScreenState()) {
|
||||||
|
|
||||||
|
private val _events = Channel<Event>(Int.MAX_VALUE)
|
||||||
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the different sources by user settings.
|
* Fetches the different sources by user settings.
|
||||||
*/
|
*/
|
||||||
private var fetchSourcesSubscription: Subscription? = null
|
private var fetchSourcesSubscription: Subscription? = null
|
||||||
|
|
||||||
fun onCreate() {
|
init {
|
||||||
getFeedSavedSearchGlobal.subscribe()
|
getFeedSavedSearchGlobal.subscribe()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach {
|
.onEach {
|
||||||
@ -84,30 +87,46 @@ open class FeedPresenter(
|
|||||||
results = null,
|
results = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
state.items = items
|
mutableState.update { state ->
|
||||||
state.isEmpty = items.isEmpty()
|
state.copy(
|
||||||
state.isLoading = false
|
items = items,
|
||||||
|
)
|
||||||
|
}
|
||||||
getFeed(items)
|
getFeed(items)
|
||||||
}
|
}
|
||||||
.launchIn(presenterScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
|
||||||
|
|
||||||
fun onDestroy() {
|
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openAddDialog() {
|
fun openAddDialog() {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
if (hasTooManyFeeds()) {
|
if (hasTooManyFeeds()) {
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
dialog = Dialog.AddFeed(getEnabledSources())
|
mutableState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
dialog = Dialog.AddFeed(getEnabledSources()),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openAddSearchDialog(source: CatalogueSource) {
|
fun openAddSearchDialog(source: CatalogueSource) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
dialog = Dialog.AddFeedSearch(source, (if (source.supportsLatest) listOf(null) else emptyList()) + getSourceSavedSearches(source.id))
|
mutableState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
dialog = Dialog.AddFeedSearch(source, (if (source.supportsLatest) listOf(null) else emptyList()) + getSourceSavedSearches(source.id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDeleteDialog(feed: FeedSavedSearch) {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
mutableState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
dialog = Dialog.DeleteFeed(feed),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +153,7 @@ open class FeedPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
|
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
|
||||||
presenterScope.launchNonCancellable {
|
coroutineScope.launchNonCancellable {
|
||||||
insertFeedSavedSearch.await(
|
insertFeedSavedSearch.await(
|
||||||
FeedSavedSearch(
|
FeedSavedSearch(
|
||||||
id = -1,
|
id = -1,
|
||||||
@ -147,7 +166,7 @@ open class FeedPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFeed(feed: FeedSavedSearch) {
|
fun deleteFeed(feed: FeedSavedSearch) {
|
||||||
presenterScope.launchNonCancellable {
|
coroutineScope.launchNonCancellable {
|
||||||
deleteFeedSavedSearchById.await(feed.id)
|
deleteFeedSavedSearchById.await(feed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,8 +231,10 @@ open class FeedPresenter(
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Update matching source with the obtained results
|
// Update matching source with the obtained results
|
||||||
.doOnNext { result ->
|
.doOnNext { result ->
|
||||||
synchronized(state) {
|
mutableState.update { state ->
|
||||||
state.items = state.items?.map { if (it.feed.id == result.feed.id) result else it }
|
state.copy(
|
||||||
|
items = state.items?.map { if (it.feed.id == result.feed.id) result else it },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribe(
|
.subscribe(
|
||||||
@ -272,9 +293,34 @@ open class FeedPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDispose() {
|
||||||
|
super.onDispose()
|
||||||
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Dialog {
|
sealed class Dialog {
|
||||||
data class AddFeed(val options: List<CatalogueSource>) : Dialog()
|
data class AddFeed(val options: List<CatalogueSource>) : Dialog()
|
||||||
data class AddFeedSearch(val source: CatalogueSource, val options: List<SavedSearch?>) : Dialog()
|
data class AddFeedSearch(val source: CatalogueSource, val options: List<SavedSearch?>) : Dialog()
|
||||||
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
|
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
object FailedFetchingSources : Event()
|
||||||
|
object TooManyFeeds : Event()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FeedScreenState(
|
||||||
|
val dialog: FeedScreenModel.Dialog? = null,
|
||||||
|
val items: List<FeedItemUI>? = null,
|
||||||
|
) {
|
||||||
|
val isLoading
|
||||||
|
get() = items == null
|
||||||
|
|
||||||
|
val isEmpty
|
||||||
|
get() = items.isNullOrEmpty()
|
||||||
}
|
}
|
@ -3,59 +3,126 @@ package eu.kanade.tachiyomi.ui.browse.feed
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bluelinelabs.conductor.Router
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||||
|
import eu.kanade.presentation.browse.FeedAddDialog
|
||||||
|
import eu.kanade.presentation.browse.FeedAddSearchDialog
|
||||||
|
import eu.kanade.presentation.browse.FeedDeleteConfirmDialog
|
||||||
import eu.kanade.presentation.browse.FeedScreen
|
import eu.kanade.presentation.browse.FeedScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun feedTab(
|
fun Screen.feedTab(): TabContent {
|
||||||
router: Router?,
|
val router = LocalRouter.currentOrThrow
|
||||||
presenter: FeedPresenter,
|
val screenModel = rememberScreenModel { FeedScreenModel() }
|
||||||
) = TabContent(
|
val state by screenModel.state.collectAsState()
|
||||||
titleRes = R.string.feed,
|
|
||||||
actions = listOf(
|
return TabContent(
|
||||||
AppBar.Action(
|
titleRes = R.string.feed,
|
||||||
title = stringResource(R.string.action_add),
|
actions = listOf(
|
||||||
icon = Icons.Outlined.Add,
|
AppBar.Action(
|
||||||
onClick = {
|
title = stringResource(R.string.action_add),
|
||||||
presenter.openAddDialog()
|
icon = Icons.Outlined.Add,
|
||||||
},
|
onClick = {
|
||||||
|
screenModel.openAddDialog()
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
content = { contentPadding, snackbarHostState ->
|
||||||
content = { contentPadding ->
|
FeedScreen(
|
||||||
FeedScreen(
|
state = state,
|
||||||
presenter = presenter,
|
contentPadding = contentPadding,
|
||||||
contentPadding = contentPadding,
|
onClickSavedSearch = { savedSearch, source ->
|
||||||
onClickAdd = {
|
screenModel.sourcePreferences.lastUsedSource().set(savedSearch.source)
|
||||||
presenter.openAddSearchDialog(it)
|
router.pushController(
|
||||||
},
|
BrowseSourceController(
|
||||||
onClickCreate = { source, savedSearch ->
|
source,
|
||||||
presenter.createFeed(source, savedSearch)
|
savedSearch = savedSearch.id,
|
||||||
},
|
),
|
||||||
onClickSavedSearch = { savedSearch, source ->
|
)
|
||||||
presenter.sourcePreferences.lastUsedSource().set(savedSearch.source)
|
},
|
||||||
router?.pushController(BrowseSourceController(source, savedSearch = savedSearch.id))
|
onClickSource = { source ->
|
||||||
},
|
screenModel.sourcePreferences.lastUsedSource().set(source.id)
|
||||||
onClickSource = { source ->
|
router.pushController(
|
||||||
presenter.sourcePreferences.lastUsedSource().set(source.id)
|
BrowseSourceController(
|
||||||
router?.pushController(BrowseSourceController(source, GetRemoteManga.QUERY_LATEST))
|
source,
|
||||||
},
|
GetRemoteManga.QUERY_LATEST,
|
||||||
onClickDelete = {
|
),
|
||||||
presenter.dialog = FeedPresenter.Dialog.DeleteFeed(it)
|
)
|
||||||
},
|
},
|
||||||
onClickDeleteConfirm = {
|
onClickDelete = screenModel::openDeleteDialog,
|
||||||
presenter.deleteFeed(it)
|
onClickManga = { manga ->
|
||||||
},
|
router.pushController(MangaController(manga.id, true))
|
||||||
onClickManga = { manga ->
|
},
|
||||||
router?.pushController(MangaController(manga.id, true))
|
getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) },
|
||||||
},
|
)
|
||||||
)
|
|
||||||
},
|
state.dialog?.let { dialog ->
|
||||||
)
|
when (dialog) {
|
||||||
|
is FeedScreenModel.Dialog.AddFeed -> {
|
||||||
|
FeedAddDialog(
|
||||||
|
sources = dialog.options,
|
||||||
|
onDismiss = screenModel::dismissDialog,
|
||||||
|
onClickAdd = {
|
||||||
|
if (it != null) {
|
||||||
|
screenModel.openAddSearchDialog(it)
|
||||||
|
}
|
||||||
|
screenModel.dismissDialog()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FeedScreenModel.Dialog.AddFeedSearch -> {
|
||||||
|
FeedAddSearchDialog(
|
||||||
|
source = dialog.source,
|
||||||
|
savedSearches = dialog.options,
|
||||||
|
onDismiss = screenModel::dismissDialog,
|
||||||
|
onClickAdd = { source, savedSearch ->
|
||||||
|
screenModel.createFeed(source, savedSearch)
|
||||||
|
screenModel.dismissDialog()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FeedScreenModel.Dialog.DeleteFeed -> {
|
||||||
|
FeedDeleteConfirmDialog(
|
||||||
|
feed = dialog.feed,
|
||||||
|
onDismiss = screenModel::dismissDialog,
|
||||||
|
onClickDeleteConfirm = {
|
||||||
|
screenModel.deleteFeed(it)
|
||||||
|
screenModel.dismissDialog()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val internalErrString = stringResource(R.string.internal_error)
|
||||||
|
val tooManyFeedsString = stringResource(R.string.too_many_in_feed)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
screenModel.events.collectLatest { event ->
|
||||||
|
when (event) {
|
||||||
|
FeedScreenModel.Event.FailedFetchingSources -> {
|
||||||
|
launch { snackbarHostState.showSnackbar(internalErrString) }
|
||||||
|
}
|
||||||
|
FeedScreenModel.Event.TooManyFeeds -> {
|
||||||
|
launch { snackbarHostState.showSnackbar(tooManyFeedsString) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||||
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
|
import eu.kanade.domain.source.model.Source
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
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 kotlinx.coroutines.flow.update
|
||||||
|
import logcat.LogPriority
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MigrateSourceScreenModel(
|
||||||
|
preferences: SourcePreferences = Injekt.get(),
|
||||||
|
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
|
||||||
|
private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
|
||||||
|
) : StateScreenModel<MigrateSourceState>(MigrateSourceState()) {
|
||||||
|
|
||||||
|
private val _channel = Channel<Event>(Int.MAX_VALUE)
|
||||||
|
val channel = _channel.receiveAsFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
getSourcesWithFavoriteCount.subscribe()
|
||||||
|
.catch {
|
||||||
|
logcat(LogPriority.ERROR, it)
|
||||||
|
_channel.send(Event.FailedFetchingSourcesWithCount)
|
||||||
|
}
|
||||||
|
.collectLatest { sources ->
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
items = sources,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.migrationSortingDirection().changes()
|
||||||
|
.onEach { mutableState.update { state -> state.copy(sortingDirection = it) } }
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
|
preferences.migrationSortingMode().changes()
|
||||||
|
.onEach { mutableState.update { state -> state.copy(sortingMode = it) } }
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSortingMode() {
|
||||||
|
with(state.value) {
|
||||||
|
val newMode = when (sortingMode) {
|
||||||
|
SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL
|
||||||
|
SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
setMigrateSorting.await(newMode, sortingDirection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSortingDirection() {
|
||||||
|
with(state.value) {
|
||||||
|
val newDirection = when (sortingDirection) {
|
||||||
|
SetMigrateSorting.Direction.ASCENDING -> SetMigrateSorting.Direction.DESCENDING
|
||||||
|
SetMigrateSorting.Direction.DESCENDING -> SetMigrateSorting.Direction.ASCENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
setMigrateSorting.await(sortingMode, newDirection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
object FailedFetchingSourcesWithCount : Event()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MigrateSourceState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val items: List<Pair<Source, Long>> = emptyList(),
|
||||||
|
val sortingMode: SetMigrateSorting.Mode = SetMigrateSorting.Mode.ALPHABETICAL,
|
||||||
|
val sortingDirection: SetMigrateSorting.Direction = SetMigrateSorting.Direction.ASCENDING,
|
||||||
|
) {
|
||||||
|
val isEmpty = items.isEmpty()
|
||||||
|
}
|
@ -3,14 +3,19 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bluelinelabs.conductor.Router
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.UnsortedPreferences
|
import eu.kanade.domain.UnsortedPreferences
|
||||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||||
import eu.kanade.presentation.browse.MigrateSourceScreen
|
import eu.kanade.presentation.browse.MigrateSourceScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||||
@ -21,11 +26,11 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun migrateSourcesTab(
|
fun Screen.migrateSourceTab(): TabContent {
|
||||||
router: Router?,
|
|
||||||
presenter: MigrationSourcesPresenter,
|
|
||||||
): TabContent {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val router = LocalRouter.currentOrThrow
|
||||||
|
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
return TabContent(
|
return TabContent(
|
||||||
titleRes = R.string.label_migration,
|
titleRes = R.string.label_migration,
|
||||||
@ -38,18 +43,20 @@ fun migrateSourcesTab(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content = { contentPadding ->
|
content = { contentPadding, _ ->
|
||||||
MigrateSourceScreen(
|
MigrateSourceScreen(
|
||||||
presenter = presenter,
|
state = state,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onClickItem = { source ->
|
onClickItem = { source ->
|
||||||
router?.pushController(
|
router.pushController(
|
||||||
MigrationMangaController(
|
MigrationMangaController(
|
||||||
source.id,
|
source.id,
|
||||||
source.name,
|
source.name,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onToggleSortingDirection = screenModel::toggleSortingDirection,
|
||||||
|
onToggleSortingMode = screenModel::toggleSortingMode,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickAll = { source ->
|
onClickAll = { source ->
|
||||||
// TODO: Jay wtf, need to clean this up sometime
|
// TODO: Jay wtf, need to clean this up sometime
|
||||||
@ -58,13 +65,11 @@ fun migrateSourcesTab(
|
|||||||
val sourceMangas =
|
val sourceMangas =
|
||||||
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
|
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
|
||||||
withUIContext {
|
withUIContext {
|
||||||
if (router != null) {
|
PreMigrationController.navigateToMigration(
|
||||||
PreMigrationController.navigateToMigration(
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
router,
|
||||||
router,
|
sourceMangas,
|
||||||
sourceMangas,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
@ -1,16 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import eu.kanade.presentation.browse.BrowseTabWrapper
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
|
|
||||||
class MigrationSourcesController : FullComposeController<MigrationSourcesPresenterWrapper>() {
|
class MigrationSourcesController : BasicFullComposeController() {
|
||||||
|
|
||||||
override fun createPresenter() = MigrationSourcesPresenterWrapper()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
BrowseTabWrapper(migrateSourcesTab(router, presenter = presenter.presenter))
|
Navigator(screen = MigrationSourcesScreen())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
|
||||||
|
|
||||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.presentation.browse.MigrateSourceState
|
|
||||||
import eu.kanade.presentation.browse.MigrateSourceStateImpl
|
|
||||||
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: SourcePreferences = Injekt.get(),
|
|
||||||
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
|
|
||||||
private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
|
|
||||||
) : MigrateSourceState by state {
|
|
||||||
|
|
||||||
private val _channel = Channel<Event>(Int.MAX_VALUE)
|
|
||||||
val channel = _channel.receiveAsFlow()
|
|
||||||
|
|
||||||
fun onCreate() {
|
|
||||||
presenterScope.launchIO {
|
|
||||||
getSourcesWithFavoriteCount.subscribe()
|
|
||||||
.catch {
|
|
||||||
logcat(LogPriority.ERROR, it)
|
|
||||||
_channel.send(Event.FailedFetchingSourcesWithCount)
|
|
||||||
}
|
|
||||||
.collectLatest { sources ->
|
|
||||||
state.items = sources
|
|
||||||
state.isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences.migrationSortingDirection().changes()
|
|
||||||
.onEach { state.sortingDirection = it }
|
|
||||||
.launchIn(presenterScope)
|
|
||||||
|
|
||||||
preferences.migrationSortingMode().changes()
|
|
||||||
.onEach { state.sortingMode = it }
|
|
||||||
.launchIn(presenterScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 {
|
|
||||||
object FailedFetchingSourcesWithCount : Event()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
|
|
||||||
class MigrationSourcesPresenterWrapper() : BasePresenter<MigrationSourcesController>() {
|
|
||||||
|
|
||||||
val presenter = MigrationSourcesPresenter(presenterScope)
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
presenter.onCreate()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import eu.kanade.presentation.browse.BrowseTabWrapper
|
||||||
|
|
||||||
|
class MigrationSourcesScreen : Screen {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
BrowseTabWrapper(migrateSourceTab())
|
||||||
|
}
|
||||||
|
}
|
@ -2,24 +2,20 @@ package eu.kanade.tachiyomi.ui.browse.source
|
|||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import eu.kanade.presentation.browse.BrowseTabWrapper
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.util.system.getParcelableCompat
|
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||||
import kotlinx.parcelize.Parcelize
|
import java.io.Serializable
|
||||||
|
|
||||||
class SourcesController(bundle: Bundle? = null) : FullComposeController<SourcesPresenterWrapper>(bundle) {
|
class SourcesController(bundle: Bundle? = null) : BasicFullComposeController(bundle) {
|
||||||
private val smartSearchConfig = args.getParcelableCompat<SmartSearchConfig>(SMART_SEARCH_CONFIG)
|
private val smartSearchConfig = args.getSerializableCompat<SmartSearchConfig>(SMART_SEARCH_CONFIG)
|
||||||
private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH
|
|
||||||
|
|
||||||
override fun createPresenter() = SourcesPresenterWrapper(controllerMode = mode, smartSearchConfig = smartSearchConfig)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
BrowseTabWrapper(sourcesTab(router, presenter = presenter.presenter))
|
Navigator(screen = SourcesScreen(smartSearchConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
@ -27,13 +23,7 @@ class SourcesController(bundle: Bundle? = null) : FullComposeController<SourcesP
|
|||||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Serializable
|
||||||
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
|
|
||||||
|
|
||||||
enum class Mode {
|
|
||||||
CATALOGUE,
|
|
||||||
SMART_SEARCH,
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
|
|
||||||
class SourcesPresenterWrapper(controllerMode: SourcesController.Mode, smartSearchConfig: SourcesController.SmartSearchConfig?) : BasePresenter<SourcesPresenter>() {
|
|
||||||
|
|
||||||
val presenter = SourcesPresenter(presenterScope, controllerMode = controllerMode, smartSearchConfig = smartSearchConfig)
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
presenter.onCreate()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import eu.kanade.presentation.browse.BrowseTabWrapper
|
||||||
|
|
||||||
|
class SourcesScreen(private val smartSearchConfig: SourcesController.SmartSearchConfig?) : Screen {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
BrowseTabWrapper(sourcesTab(smartSearchConfig))
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||||
import eu.kanade.domain.source.interactor.GetShowLatest
|
import eu.kanade.domain.source.interactor.GetShowLatest
|
||||||
@ -13,28 +16,22 @@ import eu.kanade.domain.source.model.Source
|
|||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.browse.SourceUiModel
|
import eu.kanade.presentation.browse.SourceUiModel
|
||||||
import eu.kanade.presentation.browse.SourcesState
|
|
||||||
import eu.kanade.presentation.browse.SourcesStateImpl
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.TreeMap
|
import java.util.TreeMap
|
||||||
|
|
||||||
class SourcesPresenter(
|
class SourcesScreenModel(
|
||||||
private val presenterScope: CoroutineScope,
|
|
||||||
private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
|
|
||||||
private val preferences: BasePreferences = Injekt.get(),
|
private val preferences: BasePreferences = Injekt.get(),
|
||||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||||
private val getEnabledSources: GetEnabledSources = Injekt.get(),
|
private val getEnabledSources: GetEnabledSources = Injekt.get(),
|
||||||
@ -46,78 +43,79 @@ class SourcesPresenter(
|
|||||||
private val getShowLatest: GetShowLatest = Injekt.get(),
|
private val getShowLatest: GetShowLatest = Injekt.get(),
|
||||||
private val toggleExcludeFromDataSaver: ToggleExcludeFromDataSaver = Injekt.get(),
|
private val toggleExcludeFromDataSaver: ToggleExcludeFromDataSaver = Injekt.get(),
|
||||||
private val setSourceCategories: SetSourceCategories = Injekt.get(),
|
private val setSourceCategories: SetSourceCategories = Injekt.get(),
|
||||||
val controllerMode: SourcesController.Mode,
|
|
||||||
val smartSearchConfig: SourcesController.SmartSearchConfig?,
|
val smartSearchConfig: SourcesController.SmartSearchConfig?,
|
||||||
// SY <--
|
// SY <--
|
||||||
) : SourcesState by state {
|
) : StateScreenModel<SourcesState>(SourcesState()) {
|
||||||
|
|
||||||
private val _events = Channel<Event>(Int.MAX_VALUE)
|
private val _events = Channel<Event>(Int.MAX_VALUE)
|
||||||
val events = _events.receiveAsFlow()
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
val useNewSourceNavigation = uiPreferences.useNewSourceNavigation().get()
|
val useNewSourceNavigation = uiPreferences.useNewSourceNavigation().get()
|
||||||
|
|
||||||
fun onCreate() {
|
init {
|
||||||
// SY -->
|
// SY -->
|
||||||
combine(
|
combine(
|
||||||
getEnabledSources.subscribe(),
|
getEnabledSources.subscribe(),
|
||||||
getSourceCategories.subscribe(),
|
getSourceCategories.subscribe(),
|
||||||
getShowLatest.subscribe(controllerMode),
|
getShowLatest.subscribe(smartSearchConfig != null),
|
||||||
flowOf(controllerMode == SourcesController.Mode.CATALOGUE),
|
flowOf(smartSearchConfig == null),
|
||||||
::collectLatestSources,
|
::collectLatestSources,
|
||||||
)
|
)
|
||||||
.catch {
|
.catch {
|
||||||
logcat(LogPriority.ERROR, it)
|
logcat(LogPriority.ERROR, it)
|
||||||
_events.send(Event.FailedFetchingSources)
|
_events.send(Event.FailedFetchingSources)
|
||||||
}
|
}
|
||||||
.onStart { delay(500) } // Defer to avoid crashing on initial render
|
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.launchIn(presenterScope)
|
.launchIn(coroutineScope)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun collectLatestSources(sources: List<Source>, categories: List<String>, showLatest: Boolean, showPin: Boolean) {
|
private fun collectLatestSources(sources: List<Source>, categories: List<String>, showLatest: Boolean, showPin: Boolean) {
|
||||||
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
|
mutableState.update { state ->
|
||||||
// Sources without a lang defined will be placed at the end
|
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
|
||||||
when {
|
// Sources without a lang defined will be placed at the end
|
||||||
d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1
|
when {
|
||||||
d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1
|
d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1
|
||||||
d1 == PINNED_KEY && d2 != PINNED_KEY -> -1
|
d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1
|
||||||
d2 == PINNED_KEY && d1 != PINNED_KEY -> 1
|
d1 == PINNED_KEY && d2 != PINNED_KEY -> -1
|
||||||
// SY -->
|
d2 == PINNED_KEY && d1 != PINNED_KEY -> 1
|
||||||
d1.startsWith(CATEGORY_KEY_PREFIX) && !d2.startsWith(CATEGORY_KEY_PREFIX) -> -1
|
// SY -->
|
||||||
d2.startsWith(CATEGORY_KEY_PREFIX) && !d1.startsWith(CATEGORY_KEY_PREFIX) -> 1
|
d1.startsWith(CATEGORY_KEY_PREFIX) && !d2.startsWith(CATEGORY_KEY_PREFIX) -> -1
|
||||||
// SY <--
|
d2.startsWith(CATEGORY_KEY_PREFIX) && !d1.startsWith(CATEGORY_KEY_PREFIX) -> 1
|
||||||
d1 == "" && d2 != "" -> 1
|
// SY <--
|
||||||
d2 == "" && d1 != "" -> -1
|
d1 == "" && d2 != "" -> 1
|
||||||
else -> d1.compareTo(d2)
|
d2 == "" && d1 != "" -> -1
|
||||||
|
else -> d1.compareTo(d2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
val byLang = sources.groupByTo(map) {
|
||||||
val byLang = sources.groupByTo(map) {
|
when {
|
||||||
when {
|
// SY -->
|
||||||
// SY -->
|
it.category != null -> "$CATEGORY_KEY_PREFIX${it.category}"
|
||||||
it.category != null -> "$CATEGORY_KEY_PREFIX${it.category}"
|
// SY <--
|
||||||
// SY <--
|
it.isUsedLast -> LAST_USED_KEY
|
||||||
it.isUsedLast -> LAST_USED_KEY
|
Pin.Actual in it.pin -> PINNED_KEY
|
||||||
Pin.Actual in it.pin -> PINNED_KEY
|
else -> it.lang
|
||||||
else -> it.lang
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val uiModels = byLang.flatMap {
|
state.copy(
|
||||||
listOf(
|
isLoading = false,
|
||||||
SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null),
|
items = byLang.flatMap {
|
||||||
*it.value.map { source ->
|
listOf(
|
||||||
SourceUiModel.Item(source)
|
SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null),
|
||||||
}.toTypedArray(),
|
*it.value.map { source ->
|
||||||
|
SourceUiModel.Item(source)
|
||||||
|
}.toTypedArray(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// SY -->
|
||||||
|
categories = categories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it }),
|
||||||
|
showPin = showPin,
|
||||||
|
showLatest = showLatest,
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// SY -->
|
|
||||||
state.showPin = showPin
|
|
||||||
state.showLatest = showLatest
|
|
||||||
state.categories = categories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it })
|
|
||||||
// SY <--
|
|
||||||
state.isLoading = false
|
|
||||||
state.items = uiModels
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOpenSource(source: Source) {
|
fun onOpenSource(source: Source) {
|
||||||
@ -134,6 +132,7 @@ class SourcesPresenter(
|
|||||||
toggleSourcePin.await(source)
|
toggleSourcePin.await(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
fun toggleExcludeFromDataSaver(source: Source) {
|
fun toggleExcludeFromDataSaver(source: Source) {
|
||||||
toggleExcludeFromDataSaver.await(source)
|
toggleExcludeFromDataSaver.await(source)
|
||||||
}
|
}
|
||||||
@ -142,6 +141,19 @@ class SourcesPresenter(
|
|||||||
setSourceCategories.await(source, categories)
|
setSourceCategories.await(source, categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSourceCategoriesDialog(source: Source) {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.SourceCategories(source)) }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
fun showSourceDialog(source: Source) {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.SourceLongClick(source)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
object FailedFetchingSources : Event()
|
object FailedFetchingSources : Event()
|
||||||
}
|
}
|
||||||
@ -160,3 +172,17 @@ class SourcesPresenter(
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class SourcesState(
|
||||||
|
val dialog: SourcesScreenModel.Dialog? = null,
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val items: List<SourceUiModel> = emptyList(),
|
||||||
|
// SY -->
|
||||||
|
val categories: List<String> = emptyList(),
|
||||||
|
val showPin: Boolean = true,
|
||||||
|
val showLatest: Boolean = false,
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
val isEmpty = items.isEmpty()
|
||||||
|
}
|
@ -4,76 +4,131 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material.icons.outlined.TravelExplore
|
import androidx.compose.material.icons.outlined.TravelExplore
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bluelinelabs.conductor.Router
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.source.interactor.GetRemoteManga.Companion.QUERY_POPULAR
|
import eu.kanade.domain.source.interactor.GetRemoteManga.Companion.QUERY_POPULAR
|
||||||
|
import eu.kanade.presentation.browse.SourceCategoriesDialog
|
||||||
|
import eu.kanade.presentation.browse.SourceOptionsDialog
|
||||||
import eu.kanade.presentation.browse.SourcesScreen
|
import eu.kanade.presentation.browse.SourcesScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesController.SmartSearchConfig
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
|
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import exh.ui.smartsearch.SmartSearchController
|
import exh.ui.smartsearch.SmartSearchController
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun sourcesTab(
|
fun Screen.sourcesTab(
|
||||||
router: Router?,
|
smartSearchConfig: SmartSearchConfig? = null,
|
||||||
presenter: SourcesPresenter,
|
): TabContent {
|
||||||
) = TabContent(
|
val router = LocalRouter.currentOrThrow
|
||||||
// SY -->
|
val screenModel = rememberScreenModel { SourcesScreenModel(smartSearchConfig = smartSearchConfig) }
|
||||||
titleRes = when (presenter.controllerMode) {
|
val state by screenModel.state.collectAsState()
|
||||||
SourcesController.Mode.CATALOGUE -> R.string.label_sources
|
|
||||||
SourcesController.Mode.SMART_SEARCH -> R.string.find_in_another_source
|
return TabContent(
|
||||||
},
|
// SY -->
|
||||||
actions = if (presenter.controllerMode == SourcesController.Mode.CATALOGUE) {
|
titleRes = when (smartSearchConfig == null) {
|
||||||
listOf(
|
true -> R.string.label_sources
|
||||||
AppBar.Action(
|
false -> R.string.find_in_another_source
|
||||||
title = stringResource(R.string.action_global_search),
|
},
|
||||||
icon = Icons.Outlined.TravelExplore,
|
actions = if (smartSearchConfig == null) {
|
||||||
onClick = { router?.pushController(GlobalSearchController()) },
|
listOf(
|
||||||
),
|
AppBar.Action(
|
||||||
AppBar.Action(
|
title = stringResource(R.string.action_global_search),
|
||||||
title = stringResource(R.string.action_filter),
|
icon = Icons.Outlined.TravelExplore,
|
||||||
icon = Icons.Outlined.FilterList,
|
onClick = { router.pushController(GlobalSearchController()) },
|
||||||
onClick = { router?.pushController(SourceFilterController()) },
|
),
|
||||||
),
|
AppBar.Action(
|
||||||
)
|
title = stringResource(R.string.action_filter),
|
||||||
} else {
|
icon = Icons.Outlined.FilterList,
|
||||||
emptyList()
|
onClick = { router.pushController(SourceFilterController()) },
|
||||||
},
|
),
|
||||||
// SY <--
|
)
|
||||||
content = { contentPadding ->
|
} else {
|
||||||
SourcesScreen(
|
emptyList()
|
||||||
presenter = presenter,
|
},
|
||||||
contentPadding = contentPadding,
|
// SY <--
|
||||||
onClickItem = { source, query ->
|
content = { contentPadding, snackbarHostState ->
|
||||||
// SY -->
|
SourcesScreen(
|
||||||
val controller = when {
|
state = state,
|
||||||
presenter.controllerMode == SourcesController.Mode.SMART_SEARCH ->
|
contentPadding = contentPadding,
|
||||||
SmartSearchController(source.id, presenter.smartSearchConfig!!)
|
onClickItem = { source, query ->
|
||||||
(query.isBlank() || query == QUERY_POPULAR) && presenter.useNewSourceNavigation -> SourceFeedController(source.id)
|
// SY -->
|
||||||
else -> BrowseSourceController(source, query)
|
val controller = when {
|
||||||
|
smartSearchConfig != null -> SmartSearchController(source.id, smartSearchConfig)
|
||||||
|
(query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedController(source.id)
|
||||||
|
else -> BrowseSourceController(source, query)
|
||||||
|
}
|
||||||
|
screenModel.onOpenSource(source)
|
||||||
|
router.pushController(controller)
|
||||||
|
// SY <--
|
||||||
|
},
|
||||||
|
onClickPin = screenModel::togglePin,
|
||||||
|
onLongClickItem = screenModel::showSourceDialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
state.dialog?.let { dialog ->
|
||||||
|
when (dialog) {
|
||||||
|
is SourcesScreenModel.Dialog.SourceCategories -> {
|
||||||
|
val source = dialog.source
|
||||||
|
SourceOptionsDialog(
|
||||||
|
source = source,
|
||||||
|
onClickPin = {
|
||||||
|
screenModel.togglePin(source)
|
||||||
|
screenModel.closeDialog()
|
||||||
|
},
|
||||||
|
onClickDisable = {
|
||||||
|
screenModel.toggleSource(source)
|
||||||
|
screenModel.closeDialog()
|
||||||
|
},
|
||||||
|
// SY -->
|
||||||
|
onClickSetCategories = {
|
||||||
|
screenModel.showSourceCategoriesDialog(source)
|
||||||
|
screenModel.closeDialog()
|
||||||
|
},
|
||||||
|
onClickToggleDataSaver = {
|
||||||
|
screenModel.toggleExcludeFromDataSaver(source)
|
||||||
|
screenModel.closeDialog()
|
||||||
|
},
|
||||||
|
onDismiss = screenModel::closeDialog,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SourcesScreenModel.Dialog.SourceLongClick -> {
|
||||||
|
val source = dialog.source
|
||||||
|
SourceCategoriesDialog(
|
||||||
|
source = source,
|
||||||
|
categories = state.categories,
|
||||||
|
onClickCategories = { categories ->
|
||||||
|
screenModel.setSourceCategories(source, categories)
|
||||||
|
screenModel.closeDialog()
|
||||||
|
},
|
||||||
|
onDismiss = screenModel::closeDialog,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
presenter.onOpenSource(source)
|
}
|
||||||
router?.pushController(controller)
|
|
||||||
// SY <--
|
val internalErrString = stringResource(R.string.internal_error)
|
||||||
},
|
LaunchedEffect(Unit) {
|
||||||
onClickDisable = { source ->
|
screenModel.events.collectLatest { event ->
|
||||||
presenter.toggleSource(source)
|
when (event) {
|
||||||
},
|
SourcesScreenModel.Event.FailedFetchingSources -> {
|
||||||
onClickPin = { source ->
|
launch { snackbarHostState.showSnackbar(internalErrString) }
|
||||||
presenter.togglePin(source)
|
}
|
||||||
},
|
}
|
||||||
// SY -->
|
}
|
||||||
onClickSetCategories = { source, categories ->
|
}
|
||||||
presenter.setSourceCategories(source, categories)
|
},
|
||||||
},
|
)
|
||||||
onClickToggleDataSaver = { source ->
|
}
|
||||||
presenter.toggleExcludeFromDataSaver(source)
|
|
||||||
},
|
|
||||||
// SY <--
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@ -52,7 +52,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (smartSearchConfig != null) {
|
if (smartSearchConfig != null) {
|
||||||
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
|
putSerializable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedSearch != null) {
|
if (savedSearch != null) {
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.util.storage
|
package eu.kanade.tachiyomi.util.storage
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -113,5 +117,16 @@ object DiskUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RequestStoragePermission() {
|
||||||
|
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
permissionState.launchPermissionRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const val NOMEDIA_FILE = ".nomedia"
|
const val NOMEDIA_FILE = ".nomedia"
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.util.system
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,8 +21,8 @@ object LocaleHelper {
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
return when (lang) {
|
return when (lang) {
|
||||||
SourcesPresenter.LAST_USED_KEY -> context.getString(R.string.last_used_source)
|
SourcesScreenModel.LAST_USED_KEY -> context.getString(R.string.last_used_source)
|
||||||
SourcesPresenter.PINNED_KEY -> context.getString(R.string.pinned_sources)
|
SourcesScreenModel.PINNED_KEY -> context.getString(R.string.pinned_sources)
|
||||||
"other" -> context.getString(R.string.other_source)
|
"other" -> context.getString(R.string.other_source)
|
||||||
"all" -> context.getString(R.string.multi_lang)
|
"all" -> context.getString(R.string.multi_lang)
|
||||||
else -> getDisplayName(lang)
|
else -> getDisplayName(lang)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user