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
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@ -9,10 +8,10 @@ class GetShowLatest(
private val preferences: UiPreferences,
) {
fun subscribe(mode: SourcesController.Mode): Flow<Boolean> {
fun subscribe(hasSmartSearchConfig: Boolean): Flow<Boolean> {
return preferences.useNewSourceNavigation().changes()
.map {
mode == SourcesController.Mode.CATALOGUE && !it
!hasSmartSearchConfig && !it
}
}
}

View File

@ -1,6 +1,9 @@
package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
@ -9,6 +12,7 @@ import eu.kanade.presentation.components.TabContent
@Composable
fun BrowseTabWrapper(tab: TabContent) {
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@ -19,7 +23,8 @@ fun BrowseTabWrapper(tab: TabContent) {
scrollBehavior = scrollBehavior,
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
tab.content(paddingValues)
tab.content(paddingValues, snackbarHostState)
}
}

View File

@ -53,13 +53,13 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
import eu.kanade.tachiyomi.util.system.LocaleHelper
import exh.source.anyIs
@Composable
fun ExtensionScreen(
presenter: ExtensionsPresenter,
state: ExtensionsState,
contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
@ -72,19 +72,19 @@ fun ExtensionScreen(
onRefresh: () -> Unit,
) {
SwipeRefresh(
refreshing = presenter.isRefreshing,
refreshing = state.isRefreshing,
onRefresh = onRefresh,
enabled = !presenter.isLoading,
enabled = !state.isLoading,
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
ExtensionContent(
state = presenter,
state = state,
contentPadding = contentPadding,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,

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.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenState
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import eu.kanade.domain.manga.model.MangaCover as MangaCoverData
@ -69,81 +69,21 @@ data class FeedItemUI(
@Composable
fun FeedScreen(
presenter: FeedPresenter,
state: FeedScreenState,
contentPadding: PaddingValues,
onClickAdd: (CatalogueSource) -> Unit,
onClickCreate: (CatalogueSource, SavedSearch?) -> Unit,
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
) {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen()
state.isEmpty -> EmptyScreen(
textResource = R.string.feed_tab_empty,
modifier = Modifier.padding(contentPadding),
)
else -> {
FeedList(
state = presenter,
contentPadding = contentPadding,
getMangaState = { item, source -> presenter.getManga(item, source) },
onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource,
onClickDelete = onClickDelete,
onClickManga = onClickManga,
)
}
}
when (val dialog = presenter.dialog) {
is FeedPresenter.Dialog.AddFeed -> {
FeedAddDialog(
sources = dialog.options,
onDismiss = { presenter.dialog = null },
onClickAdd = {
presenter.dialog = null
onClickAdd(it ?: return@FeedAddDialog)
},
)
}
is FeedPresenter.Dialog.AddFeedSearch -> {
FeedAddSearchDialog(
source = dialog.source,
savedSearches = dialog.options,
onDismiss = { presenter.dialog = null },
onClickAdd = { source, savedSearch ->
presenter.dialog = null
onClickCreate(source, savedSearch)
},
)
}
is FeedPresenter.Dialog.DeleteFeed -> {
FeedDeleteConfirmDialog(
feed = dialog.feed,
onDismiss = { presenter.dialog = null },
onClickDeleteConfirm = {
presenter.dialog = null
onClickDeleteConfirm(it)
},
)
}
null -> Unit
}
}
@Composable
fun FeedList(
state: FeedState,
contentPadding: PaddingValues,
getMangaState: @Composable ((Manga, CatalogueSource?) -> State<Manga>),
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
@ -162,6 +102,8 @@ fun FeedList(
)
}
}
}
}
}
@Composable

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.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable
fun MigrateSourceScreen(
presenter: MigrationSourcesPresenter,
state: MigrateSourceState,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
onToggleSortingDirection: () -> Unit,
onToggleSortingMode: () -> Unit,
// SY -->
onClickAll: (Source) -> Unit,
// SY <--
) {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding),
)
else ->
MigrateSourceList(
list = presenter.items,
list = state.items,
contentPadding = contentPadding,
onClickItem = onClickItem,
onLongClickItem = { source ->
val sourceId = source.id.toString()
context.copyToClipboard(sourceId, sourceId)
},
sortingMode = presenter.sortingMode,
onToggleSortingMode = { presenter.toggleSortingMode() },
sortingDirection = presenter.sortingDirection,
onToggleSortingDirection = { presenter.toggleSortingDirection() },
sortingMode = state.sortingMode,
onToggleSortingMode = onToggleSortingMode,
sortingDirection = state.sortingDirection,
onToggleSortingDirection = onToggleSortingDirection,
// SY -->
onClickAll = onClickAll,
// SY <--

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.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -40,68 +39,24 @@ import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter.Dialog
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesScreen(
presenter: SourcesPresenter,
state: SourcesState,
contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit,
// SY -->
onClickSetCategories: (Source, List<String>) -> Unit,
onClickToggleDataSaver: (Source) -> Unit,
// SY <--
onLongClickItem: (Source) -> Unit,
) {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
SourceList(
state = presenter,
contentPadding = contentPadding,
onClickItem = onClickItem,
onClickDisable = onClickDisable,
onClickPin = onClickPin,
// SY -->
onClickSetCategories = onClickSetCategories,
onClickToggleDataSaver = onClickToggleDataSaver,
// SY <--
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesPresenter.Event.FailedFetchingSources -> {
context.toast(R.string.internal_error)
}
}
}
}
}
@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,
) {
@ -138,53 +93,14 @@ private fun SourceList(
showPin = state.showPin,
// SY <--
onClickItem = onClickItem,
// SY -->
onLongClickItem = { state.dialog = Dialog.SourceLongClick(it) },
// SY <--
onLongClickItem = onLongClickItem,
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
@ -268,7 +184,7 @@ private fun SourcePinButton(
}
@Composable
private fun SourceOptionsDialog(
fun SourceOptionsDialog(
source: Source,
onClickPin: () -> Unit,
onClickDisable: () -> Unit,
@ -338,7 +254,7 @@ sealed class SourceUiModel {
fun SourceCategoriesDialog(
source: Source?,
categories: List<String>,
onClickCategories: (Source, List<String>) -> Unit,
onClickCategories: (List<String>) -> Unit,
onDismiss: () -> Unit,
) {
source ?: return
@ -379,7 +295,7 @@ fun SourceCategoriesDialog(
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickCategories(source, newCategories.toList()) }) {
TextButton(onClick = { onClickCategories(newCategories.toList()) }) {
Text(text = stringResource(android.R.string.ok))
}
},

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.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -32,6 +35,7 @@ fun TabbedScreen(
) {
val scope = rememberCoroutineScope()
val state = rememberPagerState()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(startIndex) {
if (startIndex != null) {
@ -52,6 +56,7 @@ fun TabbedScreen(
actions = { AppBarActions(tab.actions) },
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { contentPadding ->
Column(
modifier = Modifier.padding(
@ -86,6 +91,7 @@ fun TabbedScreen(
TachiyomiBottomNavigationView.withBottomNavPadding(
PaddingValues(bottom = contentPadding.calculateBottomPadding()),
),
snackbarHostState,
)
}
}
@ -97,5 +103,5 @@ data class TabContent(
val badgeNumber: Int? = null,
val searchEnabled: Boolean = false,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable (contentPadding: PaddingValues) -> Unit,
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
)

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@ -22,7 +21,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -37,7 +35,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.google.accompanist.permissions.rememberPermissionState
import com.hippo.unifile.UniFile
import eu.kanade.domain.backup.service.BackupPreferences
import eu.kanade.presentation.components.Divider
@ -52,6 +49,7 @@ import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
@ -70,7 +68,7 @@ object SettingsBackupScreen : SearchableSettings {
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
RequestStoragePermission()
DiskUtil.RequestStoragePermission()
return listOf(
getCreateBackupPref(),
@ -79,14 +77,6 @@ object SettingsBackupScreen : SearchableSettings {
)
}
@Composable
private fun RequestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
@Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope()

View File

@ -1,25 +1,13 @@
package eu.kanade.tachiyomi.ui.browse
import android.Manifest
import android.os.Bundle
import android.view.View
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 eu.kanade.presentation.components.TabbedScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
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")
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)
override fun createPresenter() = BrowsePresenter()
@Composable
override fun ComposeContent() {
val query by presenter.extensionsPresenter.query.collectAsState()
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)
Navigator(screen = BrowseScreen(toExtensions = toExtensions))
}
}

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 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.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.extension.ExtensionManager
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.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -23,26 +20,23 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionsPresenter(
private val presenterScope: CoroutineScope,
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
private val preferences: SourcePreferences = Injekt.get(),
class ExtensionsScreenModel(
preferences: SourcePreferences = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(),
) : ExtensionsState by state {
) : StateScreenModel<ExtensionsState>(ExtensionsState()) {
private val _query: MutableStateFlow<String?> = MutableStateFlow(null)
val query: StateFlow<String?> = _query.asStateFlow()
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
fun onCreate() {
init {
val context = Injekt.get<Application>()
val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map ->
{
@ -76,7 +70,7 @@ class ExtensionsPresenter(
}
}
presenterScope.launchIO {
coroutineScope.launchIO {
combine(
_query,
_currentDownloads,
@ -117,30 +111,34 @@ class ExtensionsPresenter(
items
}
.onStart { delay(500) } // Defer to avoid crashing on initial render
.collectLatest {
state.isLoading = false
state.items = it
mutableState.update { state ->
state.copy(
isLoading = false,
items = it,
)
}
}
}
presenterScope.launchIO { findAvailableExtensions() }
coroutineScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount().changes()
.onEach { state.updates = it }
.launchIn(presenterScope)
.onEach { mutableState.update { state -> state.copy(updates = it) } }
.launchIn(coroutineScope)
}
fun search(query: String?) {
presenterScope.launchIO {
coroutineScope.launchIO {
_query.emit(query)
}
}
fun updateAllExtensions() {
presenterScope.launchIO {
if (state.isEmpty) return@launchIO
state.items
coroutineScope.launchIO {
with(state.value) {
if (isEmpty) return@launchIO
items
.mapNotNull {
when {
it !is ExtensionUiModel.Item -> null
@ -152,6 +150,7 @@ class ExtensionsPresenter(
.forEach { updateExtension(it) }
}
}
}
fun installExtension(extension: Extension.Available) {
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
@ -195,11 +194,11 @@ class ExtensionsPresenter(
}
fun findAvailableExtensions() {
presenterScope.launchIO {
state.isRefreshing = true
mutableState.update { it.copy(isRefreshing = true) }
coroutineScope.launchIO {
extensionManager.findAvailableExtensions()
state.isRefreshing = false
}
mutableState.update { it.copy(isRefreshing = false) }
}
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 Header : ExtensionUiModel {
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.outlined.Translate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.pushController
@ -15,53 +18,41 @@ import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsControlle
@Composable
fun extensionsTab(
router: Router?,
presenter: ExtensionsPresenter,
) = TabContent(
extensionsScreenModel: ExtensionsScreenModel,
): TabContent {
val router = LocalRouter.currentOrThrow
val state by extensionsScreenModel.state.collectAsState()
return TabContent(
titleRes = R.string.label_extensions,
badgeNumber = presenter.updates.takeIf { it > 0 },
badgeNumber = state.updates.takeIf { it > 0 },
searchEnabled = true,
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.Translate,
onClick = { router?.pushController(ExtensionFilterController()) },
onClick = { router.pushController(ExtensionFilterController()) },
),
),
content = { contentPadding ->
content = { contentPadding, _ ->
ExtensionScreen(
presenter = presenter,
state = state,
contentPadding = contentPadding,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
else -> presenter.uninstallExtension(extension.pkgName)
is Extension.Available -> extensionsScreenModel.installExtension(extension)
else -> extensionsScreenModel.uninstallExtension(extension.pkgName)
}
},
onClickItemCancel = { extension ->
presenter.cancelInstallUpdateExtension(extension)
},
onClickUpdateAll = {
presenter.updateAllExtensions()
},
onInstallExtension = {
presenter.installExtension(it)
},
onOpenExtension = {
router?.pushController(ExtensionDetailsController(it.pkgName))
},
onTrustExtension = {
presenter.trustSignature(it.signatureHash)
},
onUninstallExtension = {
presenter.uninstallExtension(it.pkgName)
},
onUpdateExtension = {
presenter.updateExtension(it)
},
onRefresh = {
presenter.findAvailableExtensions()
},
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
onInstallExtension = extensionsScreenModel::installExtension,
onOpenExtension = { router.pushController(ExtensionDetailsController(it.pkgName)) },
onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) },
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it.pkgName) },
onUpdateExtension = extensionsScreenModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions,
)
},
)
)
}

View File

@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.feed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
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.NetworkToLocalManga
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.service.SourcePreferences
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.SourceManager
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 exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@ -51,9 +53,7 @@ import eu.kanade.domain.manga.model.Manga as DomainManga
/**
* Presenter of [feedTab]
*/
open class FeedPresenter(
private val presenterScope: CoroutineScope,
private val state: FeedStateImpl = FeedState() as FeedStateImpl,
open class FeedScreenModel(
val sourceManager: SourceManager = Injekt.get(),
val sourcePreferences: SourcePreferences = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
@ -65,14 +65,17 @@ open class FeedPresenter(
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
private val insertFeedSavedSearch: InsertFeedSavedSearch = 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.
*/
private var fetchSourcesSubscription: Subscription? = null
fun onCreate() {
init {
getFeedSavedSearchGlobal.subscribe()
.distinctUntilChanged()
.onEach {
@ -84,30 +87,46 @@ open class FeedPresenter(
results = null,
)
}
state.items = items
state.isEmpty = items.isEmpty()
state.isLoading = false
mutableState.update { state ->
state.copy(
items = items,
)
}
getFeed(items)
}
.launchIn(presenterScope)
}
fun onDestroy() {
fetchSourcesSubscription?.unsubscribe()
.launchIn(coroutineScope)
}
fun openAddDialog() {
presenterScope.launchIO {
coroutineScope.launchIO {
if (hasTooManyFeeds()) {
return@launchIO
}
dialog = Dialog.AddFeed(getEnabledSources())
mutableState.update { state ->
state.copy(
dialog = Dialog.AddFeed(getEnabledSources()),
)
}
}
}
fun openAddSearchDialog(source: CatalogueSource) {
presenterScope.launchIO {
dialog = Dialog.AddFeedSearch(source, (if (source.supportsLatest) listOf(null) else emptyList()) + getSourceSavedSearches(source.id))
coroutineScope.launchIO {
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?) {
presenterScope.launchNonCancellable {
coroutineScope.launchNonCancellable {
insertFeedSavedSearch.await(
FeedSavedSearch(
id = -1,
@ -147,7 +166,7 @@ open class FeedPresenter(
}
fun deleteFeed(feed: FeedSavedSearch) {
presenterScope.launchNonCancellable {
coroutineScope.launchNonCancellable {
deleteFeedSavedSearchById.await(feed.id)
}
}
@ -212,8 +231,10 @@ open class FeedPresenter(
.observeOn(AndroidSchedulers.mainThread())
// Update matching source with the obtained results
.doOnNext { result ->
synchronized(state) {
state.items = state.items?.map { if (it.feed.id == result.feed.id) result else it }
mutableState.update { state ->
state.copy(
items = state.items?.map { if (it.feed.id == result.feed.id) result else it },
)
}
}
.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 {
data class AddFeed(val options: List<CatalogueSource>) : Dialog()
data class AddFeedSearch(val source: CatalogueSource, val options: List<SavedSearch?>) : 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.outlined.Add
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 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.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.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Composable
fun feedTab(
router: Router?,
presenter: FeedPresenter,
) = TabContent(
fun Screen.feedTab(): TabContent {
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { FeedScreenModel() }
val state by screenModel.state.collectAsState()
return TabContent(
titleRes = R.string.feed,
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_add),
icon = Icons.Outlined.Add,
onClick = {
presenter.openAddDialog()
screenModel.openAddDialog()
},
),
),
content = { contentPadding ->
content = { contentPadding, snackbarHostState ->
FeedScreen(
presenter = presenter,
state = state,
contentPadding = contentPadding,
onClickAdd = {
presenter.openAddSearchDialog(it)
},
onClickCreate = { source, savedSearch ->
presenter.createFeed(source, savedSearch)
},
onClickSavedSearch = { savedSearch, source ->
presenter.sourcePreferences.lastUsedSource().set(savedSearch.source)
router?.pushController(BrowseSourceController(source, savedSearch = savedSearch.id))
},
onClickSource = { source ->
presenter.sourcePreferences.lastUsedSource().set(source.id)
router?.pushController(BrowseSourceController(source, GetRemoteManga.QUERY_LATEST))
},
onClickDelete = {
presenter.dialog = FeedPresenter.Dialog.DeleteFeed(it)
},
onClickDeleteConfirm = {
presenter.deleteFeed(it)
},
onClickManga = { manga ->
router?.pushController(MangaController(manga.id, true))
},
screenModel.sourcePreferences.lastUsedSource().set(savedSearch.source)
router.pushController(
BrowseSourceController(
source,
savedSearch = savedSearch.id,
),
)
},
)
onClickSource = { source ->
screenModel.sourcePreferences.lastUsedSource().set(source.id)
router.pushController(
BrowseSourceController(
source,
GetRemoteManga.QUERY_LATEST,
),
)
},
onClickDelete = screenModel::openDeleteDialog,
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.outlined.HelpOutline
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.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.manga.interactor.GetFavorites
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
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
@Composable
fun migrateSourcesTab(
router: Router?,
presenter: MigrationSourcesPresenter,
): TabContent {
fun Screen.migrateSourceTab(): TabContent {
val uriHandler = LocalUriHandler.current
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
val state by screenModel.state.collectAsState()
return TabContent(
titleRes = R.string.label_migration,
@ -38,18 +43,20 @@ fun migrateSourcesTab(
},
),
),
content = { contentPadding ->
content = { contentPadding, _ ->
MigrateSourceScreen(
presenter = presenter,
state = state,
contentPadding = contentPadding,
onClickItem = { source ->
router?.pushController(
router.pushController(
MigrationMangaController(
source.id,
source.name,
),
)
},
onToggleSortingDirection = screenModel::toggleSortingDirection,
onToggleSortingMode = screenModel::toggleSortingMode,
// SY -->
onClickAll = { source ->
// TODO: Jay wtf, need to clean this up sometime
@ -58,7 +65,6 @@ fun migrateSourcesTab(
val sourceMangas =
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
withUIContext {
if (router != null) {
PreMigrationController.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
router,
@ -66,7 +72,6 @@ fun migrateSourcesTab(
)
}
}
}
},
// SY <--
)

View File

@ -1,16 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Composable
import eu.kanade.presentation.browse.BrowseTabWrapper
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
class MigrationSourcesController : FullComposeController<MigrationSourcesPresenterWrapper>() {
override fun createPresenter() = MigrationSourcesPresenterWrapper()
class MigrationSourcesController : BasicFullComposeController() {
@Composable
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.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.compose.runtime.Composable
import eu.kanade.presentation.browse.BrowseTabWrapper
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.util.system.getParcelableCompat
import kotlinx.parcelize.Parcelize
import eu.kanade.tachiyomi.util.system.getSerializableCompat
import java.io.Serializable
class SourcesController(bundle: Bundle? = null) : FullComposeController<SourcesPresenterWrapper>(bundle) {
private val smartSearchConfig = args.getParcelableCompat<SmartSearchConfig>(SMART_SEARCH_CONFIG)
private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH
override fun createPresenter() = SourcesPresenterWrapper(controllerMode = mode, smartSearchConfig = smartSearchConfig)
class SourcesController(bundle: Bundle? = null) : BasicFullComposeController(bundle) {
private val smartSearchConfig = args.getSerializableCompat<SmartSearchConfig>(SMART_SEARCH_CONFIG)
@Composable
override fun ComposeContent() {
BrowseTabWrapper(sourcesTab(router, presenter = presenter.presenter))
Navigator(screen = SourcesScreen(smartSearchConfig))
}
override fun onViewCreated(view: View) {
@ -27,13 +23,7 @@ class SourcesController(bundle: Bundle? = null) : FullComposeController<SourcesP
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
}
@Parcelize
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
enum class Mode {
CATALOGUE,
SMART_SEARCH,
}
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Serializable
companion object {
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
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.source.interactor.GetEnabledSources
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.ui.UiPreferences
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.TreeMap
class SourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
class SourcesScreenModel(
private val preferences: BasePreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
private val getEnabledSources: GetEnabledSources = Injekt.get(),
@ -46,36 +43,35 @@ class SourcesPresenter(
private val getShowLatest: GetShowLatest = Injekt.get(),
private val toggleExcludeFromDataSaver: ToggleExcludeFromDataSaver = Injekt.get(),
private val setSourceCategories: SetSourceCategories = Injekt.get(),
val controllerMode: SourcesController.Mode,
val smartSearchConfig: SourcesController.SmartSearchConfig?,
// SY <--
) : SourcesState by state {
) : StateScreenModel<SourcesState>(SourcesState()) {
private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow()
val useNewSourceNavigation = uiPreferences.useNewSourceNavigation().get()
fun onCreate() {
init {
// SY -->
combine(
getEnabledSources.subscribe(),
getSourceCategories.subscribe(),
getShowLatest.subscribe(controllerMode),
flowOf(controllerMode == SourcesController.Mode.CATALOGUE),
getShowLatest.subscribe(smartSearchConfig != null),
flowOf(smartSearchConfig == null),
::collectLatestSources,
)
.catch {
logcat(LogPriority.ERROR, it)
_events.send(Event.FailedFetchingSources)
}
.onStart { delay(500) } // Defer to avoid crashing on initial render
.flowOn(Dispatchers.IO)
.launchIn(presenterScope)
.launchIn(coroutineScope)
// SY <--
}
private fun collectLatestSources(sources: List<Source>, categories: List<String>, showLatest: Boolean, showPin: Boolean) {
mutableState.update { state ->
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
// Sources without a lang defined will be placed at the end
when {
@ -103,21 +99,23 @@ class SourcesPresenter(
}
}
val uiModels = byLang.flatMap {
state.copy(
isLoading = false,
items = byLang.flatMap {
listOf(
SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null),
*it.value.map { source ->
SourceUiModel.Item(source)
}.toTypedArray(),
)
}
},
// SY -->
state.showPin = showPin
state.showLatest = showLatest
state.categories = categories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it })
categories = categories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it }),
showPin = showPin,
showLatest = showLatest,
// SY <--
state.isLoading = false
state.items = uiModels
)
}
}
fun onOpenSource(source: Source) {
@ -134,6 +132,7 @@ class SourcesPresenter(
toggleSourcePin.await(source)
}
// SY -->
fun toggleExcludeFromDataSaver(source: Source) {
toggleExcludeFromDataSaver.await(source)
}
@ -142,6 +141,19 @@ class SourcesPresenter(
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 {
object FailedFetchingSources : Event()
}
@ -160,3 +172,17 @@ class SourcesPresenter(
// 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.TravelExplore
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 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.presentation.browse.SourceCategoriesDialog
import eu.kanade.presentation.browse.SourceOptionsDialog
import eu.kanade.presentation.browse.SourcesScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
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.feed.SourceFeedController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import exh.ui.smartsearch.SmartSearchController
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Composable
fun sourcesTab(
router: Router?,
presenter: SourcesPresenter,
) = TabContent(
fun Screen.sourcesTab(
smartSearchConfig: SmartSearchConfig? = null,
): TabContent {
val router = LocalRouter.currentOrThrow
val screenModel = rememberScreenModel { SourcesScreenModel(smartSearchConfig = smartSearchConfig) }
val state by screenModel.state.collectAsState()
return TabContent(
// SY -->
titleRes = when (presenter.controllerMode) {
SourcesController.Mode.CATALOGUE -> R.string.label_sources
SourcesController.Mode.SMART_SEARCH -> R.string.find_in_another_source
titleRes = when (smartSearchConfig == null) {
true -> R.string.label_sources
false -> R.string.find_in_another_source
},
actions = if (presenter.controllerMode == SourcesController.Mode.CATALOGUE) {
actions = if (smartSearchConfig == null) {
listOf(
AppBar.Action(
title = stringResource(R.string.action_global_search),
icon = Icons.Outlined.TravelExplore,
onClick = { router?.pushController(GlobalSearchController()) },
onClick = { router.pushController(GlobalSearchController()) },
),
AppBar.Action(
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList,
onClick = { router?.pushController(SourceFilterController()) },
onClick = { router.pushController(SourceFilterController()) },
),
)
} else {
emptyList()
},
// SY <--
content = { contentPadding ->
content = { contentPadding, snackbarHostState ->
SourcesScreen(
presenter = presenter,
state = state,
contentPadding = contentPadding,
onClickItem = { source, query ->
// SY -->
val controller = when {
presenter.controllerMode == SourcesController.Mode.SMART_SEARCH ->
SmartSearchController(source.id, presenter.smartSearchConfig!!)
(query.isBlank() || query == QUERY_POPULAR) && presenter.useNewSourceNavigation -> SourceFeedController(source.id)
smartSearchConfig != null -> SmartSearchController(source.id, smartSearchConfig)
(query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedController(source.id)
else -> BrowseSourceController(source, query)
}
presenter.onOpenSource(source)
router?.pushController(controller)
screenModel.onOpenSource(source)
router.pushController(controller)
// SY <--
},
onClickDisable = { source ->
presenter.toggleSource(source)
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()
},
onClickPin = { source ->
presenter.togglePin(source)
onClickDisable = {
screenModel.toggleSource(source)
screenModel.closeDialog()
},
// SY -->
onClickSetCategories = { source, categories ->
presenter.setSourceCategories(source, categories)
onClickSetCategories = {
screenModel.showSourceCategoriesDialog(source)
screenModel.closeDialog()
},
onClickToggleDataSaver = { source ->
presenter.toggleExcludeFromDataSaver(source)
onClickToggleDataSaver = {
screenModel.toggleExcludeFromDataSaver(source)
screenModel.closeDialog()
},
// SY <--
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,
)
}
}
}
val internalErrString = stringResource(R.string.internal_error)
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
when (event) {
SourcesScreenModel.Event.FailedFetchingSources -> {
launch { snackbarHostState.showSnackbar(internalErrString) }
}
}
}
}
},
)
}

View File

@ -52,7 +52,7 @@ open class BrowseSourceController(bundle: Bundle) :
// SY -->
if (smartSearchConfig != null) {
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
putSerializable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
}
if (savedSearch != null) {

View File

@ -1,11 +1,15 @@
package eu.kanade.tachiyomi.util.storage
import android.Manifest
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.os.StatFs
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.content.ContextCompat
import com.google.accompanist.permissions.rememberPermissionState
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
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"
}

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.util.system
import android.content.Context
import androidx.core.os.LocaleListCompat
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
/**
@ -21,8 +21,8 @@ object LocaleHelper {
}
// SY <--
return when (lang) {
SourcesPresenter.LAST_USED_KEY -> context.getString(R.string.last_used_source)
SourcesPresenter.PINNED_KEY -> context.getString(R.string.pinned_sources)
SourcesScreenModel.LAST_USED_KEY -> context.getString(R.string.last_used_source)
SourcesScreenModel.PINNED_KEY -> context.getString(R.string.pinned_sources)
"other" -> context.getString(R.string.other_source)
"all" -> context.getString(R.string.multi_lang)
else -> getDisplayName(lang)