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:
Ivan Iskandar 2022-11-24 10:28:25 +07:00 committed by Jobobby04
parent 0b9b6612fd
commit bf9b2ca2ff
33 changed files with 821 additions and 884 deletions

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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