Initial conversion of browse tabs to full Compose

TODO:
- Global search should launch a controller with the search textfield focused. This is pending a Compose rewrite of that screen.
- Better migrate sort UI
- Extensions search

(cherry picked from commit 92e83f702c775d5ab5a0b4248995308155150811)

# 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/library/components/LibraryTabs.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt
This commit is contained in:
arkon 2022-08-29 17:18:06 -04:00 committed by Jobobby04
parent aaa2a961ae
commit 8c182df784
41 changed files with 879 additions and 920 deletions

View File

@ -6,10 +6,9 @@ class SetMigrateSorting(
private val preferences: PreferencesHelper, private val preferences: PreferencesHelper,
) { ) {
fun await(mode: Mode, isAscending: Boolean) { fun await(mode: Mode, direction: Direction) {
val direction = if (isAscending) Direction.ASCENDING else Direction.DESCENDING
preferences.migrationSortingDirection().set(direction)
preferences.migrationSortingMode().set(mode) preferences.migrationSortingMode().set(mode)
preferences.migrationSortingDirection().set(direction)
} }
enum class Mode { enum class Mode {

View File

@ -0,0 +1,84 @@
package eu.kanade.presentation.browse
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
@Composable
fun BrowseScreen(
startIndex: Int? = null,
tabs: List<BrowseTab>,
) {
val scope = rememberCoroutineScope()
val state = rememberPagerState()
LaunchedEffect(startIndex) {
if (startIndex != null) {
state.scrollToPage(startIndex)
}
}
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.browse),
actions = {
AppBarActions(tabs[state.currentPage].actions)
},
)
},
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
TabRow(
selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage]) },
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
TabText(stringResource(tab.titleRes), tab.badgeNumber, state.currentPage == index)
},
)
}
}
HorizontalPager(
count = tabs.size,
modifier = Modifier.fillMaxSize(),
state = state,
verticalAlignment = Alignment.Top,
) { page ->
tabs[page].content()
}
}
}
}
data class BrowseTab(
@StringRes val titleRes: Int,
val badgeNumber: Int? = null,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable () -> Unit,
)

View File

@ -0,0 +1,30 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.Scaffold
@Composable
fun BrowseTabWrapper(tab: BrowseTab) {
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(tab.titleRes),
actions = {
AppBarActions(tab.actions)
},
)
},
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
tab.content()
}
}
}

View File

@ -23,15 +23,12 @@ 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.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -60,7 +57,6 @@ import exh.source.anyIs
@Composable @Composable
fun ExtensionScreen( fun ExtensionScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionsPresenter, presenter: ExtensionsPresenter,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
@ -71,10 +67,8 @@ fun ExtensionScreen(
onOpenExtension: (Extension.Installed) -> Unit, onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onLaunched: () -> Unit,
) { ) {
SwipeRefresh( SwipeRefresh(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = rememberSwipeRefreshState(presenter.isRefreshing), state = rememberSwipeRefreshState(presenter.isRefreshing),
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) }, indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
onRefresh = onRefresh, onRefresh = onRefresh,
@ -93,7 +87,6 @@ fun ExtensionScreen(
onTrustExtension = onTrustExtension, onTrustExtension = onTrustExtension,
onOpenExtension = onOpenExtension, onOpenExtension = onOpenExtension,
onClickUpdateAll = onClickUpdateAll, onClickUpdateAll = onClickUpdateAll,
onLaunched = onLaunched,
) )
} }
} }
@ -111,7 +104,6 @@ fun ExtensionContent(
onTrustExtension: (Extension.Untrusted) -> Unit, onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit, onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
onLaunched: () -> Unit,
) { ) {
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) } var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
@ -190,9 +182,6 @@ fun ExtensionContent(
} }
}, },
) )
LaunchedEffect(Unit) {
onLaunched()
}
} }
} }
} }

View File

@ -10,6 +10,7 @@ interface ExtensionsState {
val isLoading: Boolean val isLoading: Boolean
val isRefreshing: Boolean val isRefreshing: Boolean
val items: List<ExtensionUiModel> val items: List<ExtensionUiModel>
val updates: Int
val isEmpty: Boolean val isEmpty: Boolean
} }
@ -21,5 +22,6 @@ class ExtensionsStateImpl : ExtensionsState {
override var isLoading: Boolean by mutableStateOf(true) override var isLoading: Boolean by mutableStateOf(true)
override var isRefreshing: Boolean by mutableStateOf(false) override var isRefreshing: Boolean by mutableStateOf(false)
override var items: List<ExtensionUiModel> by mutableStateOf(emptyList()) override var items: List<ExtensionUiModel> by mutableStateOf(emptyList())
override var updates: Int by mutableStateOf(0)
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
} }

View File

@ -7,29 +7,39 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -62,11 +72,13 @@ data class FeedItemUI(
@Composable @Composable
fun FeedScreen( fun FeedScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: FeedPresenter, presenter: FeedPresenter,
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,
) { ) {
when { when {
@ -74,11 +86,13 @@ fun FeedScreen(
presenter.isEmpty -> EmptyScreen(R.string.feed_tab_empty) presenter.isEmpty -> EmptyScreen(R.string.feed_tab_empty)
else -> { else -> {
FeedList( FeedList(
nestedScrollConnection = nestedScrollInterop,
state = presenter, state = presenter,
onClickAdd = onClickAdd,
onClickCreate = onClickCreate,
onClickSavedSearch = onClickSavedSearch, onClickSavedSearch = onClickSavedSearch,
onClickSource = onClickSource, onClickSource = onClickSource,
onClickDelete = onClickDelete, onClickDelete = onClickDelete,
onClickDeleteConfirm = onClickDeleteConfirm,
onClickManga = onClickManga, onClickManga = onClickManga,
) )
} }
@ -87,15 +101,16 @@ fun FeedScreen(
@Composable @Composable
fun FeedList( fun FeedList(
nestedScrollConnection: NestedScrollConnection,
state: FeedState, state: FeedState,
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,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
items( items(
@ -112,6 +127,41 @@ fun FeedList(
) )
} }
} }
when (val dialog = state.dialog) {
is FeedPresenter.Dialog.AddFeed -> {
FeedAddDialog(
sources = dialog.options,
onDismiss = { state.dialog = null },
onClickAdd = {
state.dialog = null
onClickAdd(it ?: return@FeedAddDialog)
},
)
}
is FeedPresenter.Dialog.AddFeedSearch -> {
FeedAddSearchDialog(
source = dialog.source,
savedSearches = dialog.options,
onDismiss = { state.dialog = null },
onClickAdd = { source, savedSearch ->
state.dialog = null
onClickCreate(source, savedSearch)
},
)
}
is FeedPresenter.Dialog.DeleteFeed -> {
FeedDeleteConfirmDialog(
feed = dialog.feed,
onDismiss = { state.dialog = null },
onClickDeleteConfirm = {
state.dialog = null
onClickDeleteConfirm(it)
},
)
}
null -> Unit
}
} }
@Composable @Composable
@ -207,7 +257,8 @@ fun FeedCardItem(
.aspectRatio(MangaCover.Book.ratio), .aspectRatio(MangaCover.Book.ratio),
) { ) {
MangaCover.Book( MangaCover.Book(
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.alpha( .alpha(
if (manga.favorite) 0.3f else 1.0f, if (manga.favorite) 0.3f else 1.0f,
), ),
@ -239,3 +290,110 @@ fun FeedCardItem(
) )
} }
} }
@Composable
fun FeedAddDialog(
sources: List<CatalogueSource>,
onDismiss: () -> Unit,
onClickAdd: (CatalogueSource?) -> Unit,
) {
var selected by remember { mutableStateOf<Int?>(null) }
AlertDialog(
title = {
Text(text = stringResource(R.string.feed))
},
text = {
RadioSelector(options = sources, selected = selected) {
selected = it
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickAdd(selected?.let { sources[it] }) }) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
@Composable
fun FeedAddSearchDialog(
source: CatalogueSource,
savedSearches: List<SavedSearch?>,
onDismiss: () -> Unit,
onClickAdd: (CatalogueSource, SavedSearch?) -> Unit,
) {
var selected by remember { mutableStateOf<Int?>(null) }
AlertDialog(
title = {
Text(text = source.name)
},
text = {
val context = LocalContext.current
val savedSearchStrings = remember {
savedSearches.map {
it?.name ?: context.getString(R.string.latest)
}
}
RadioSelector(
options = savedSearches,
optionStrings = savedSearchStrings,
selected = selected,
) {
selected = it
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickAdd(source, selected?.let { savedSearches[it] }) }) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
@Composable
fun <T> RadioSelector(
options: List<T>,
optionStrings: List<String> = remember { options.map { it.toString() } },
selected: Int?,
onSelectOption: (Int) -> Unit,
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
optionStrings.forEachIndexed { index, option ->
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.clickable { onSelectOption(index) },
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected == index, onClick = null)
Spacer(Modifier.width(4.dp))
Text(option, maxLines = 1)
}
}
}
}
@Composable
fun FeedDeleteConfirmDialog(
feed: FeedSavedSearch,
onDismiss: () -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(R.string.feed))
},
text = {
Text(text = stringResource(R.string.feed_delete))
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickDeleteConfirm(feed) }) {
Text(text = stringResource(R.string.action_delete))
}
},
)
}

View File

@ -4,9 +4,11 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.feed.FeedPresenter
@Stable @Stable
interface FeedState { interface FeedState {
var dialog: FeedPresenter.Dialog?
val isLoading: Boolean val isLoading: Boolean
val isEmpty: Boolean val isEmpty: Boolean
val items: List<FeedItemUI>? val items: List<FeedItemUI>?
@ -17,6 +19,7 @@ fun FeedState(): FeedState {
} }
class FeedStateImpl : FeedState { class FeedStateImpl : FeedState {
override var dialog: FeedPresenter.Dialog? by mutableStateOf(null)
override var isLoading: Boolean by mutableStateOf(true) override var isLoading: Boolean by mutableStateOf(true)
override var isEmpty: Boolean by mutableStateOf(false) override var isEmpty: Boolean by mutableStateOf(false)
override var items: List<FeedItemUI>? by mutableStateOf(null) override var items: List<FeedItemUI>? by mutableStateOf(null)

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -15,12 +16,11 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
@ -41,7 +41,6 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable @Composable
fun MigrateSourceScreen( fun MigrateSourceScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: MigrationSourcesPresenter, presenter: MigrationSourcesPresenter,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
// SY --> // SY -->
@ -54,13 +53,16 @@ fun MigrateSourceScreen(
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library) presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library)
else -> else ->
MigrateSourceList( MigrateSourceList(
nestedScrollInterop = nestedScrollInterop,
list = presenter.items, list = presenter.items,
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,
onToggleSortingMode = { presenter.toggleSortingMode() },
sortingDirection = presenter.sortingDirection,
onToggleSortingDirection = { presenter.toggleSortingDirection() },
// SY --> // SY -->
onClickAll = onClickAll, onClickAll = onClickAll,
// SY <-- // SY <--
@ -70,18 +72,31 @@ fun MigrateSourceScreen(
@Composable @Composable
fun MigrateSourceList( fun MigrateSourceList(
nestedScrollInterop: NestedScrollConnection,
list: List<Pair<Source, Long>>, list: List<Pair<Source, Long>>,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
sortingMode: SetMigrateSorting.Mode,
onToggleSortingMode: () -> Unit,
sortingDirection: SetMigrateSorting.Direction,
onToggleSortingDirection: () -> Unit,
// SY --> // SY -->
onClickAll: (Source) -> Unit, onClickAll: (Source) -> Unit,
// SY <-- // SY <--
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
stickyHeader {
Row {
Button(onClick = onToggleSortingMode) {
Text(sortingMode.toString())
}
Button(onClick = onToggleSortingDirection) {
Text(sortingDirection.toString())
}
}
}
item(key = "title") { item(key = "title") {
Text( Text(
text = stringResource(R.string.migration_selection_prompt), text = stringResource(R.string.migration_selection_prompt),

View File

@ -4,12 +4,15 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
interface MigrateSourceState { interface MigrateSourceState {
val isLoading: Boolean val isLoading: Boolean
val items: List<Pair<Source, Long>> val items: List<Pair<Source, Long>>
val isEmpty: Boolean val isEmpty: Boolean
val sortingMode: SetMigrateSorting.Mode
val sortingDirection: SetMigrateSorting.Direction
} }
fun MigrateSourceState(): MigrateSourceState { fun MigrateSourceState(): MigrateSourceState {
@ -20,4 +23,6 @@ class MigrateSourceStateImpl : MigrateSourceState {
override var isLoading: Boolean by mutableStateOf(true) override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList()) override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } 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

@ -24,8 +24,6 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.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
@ -51,7 +49,6 @@ import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesScreen( fun SourcesScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: SourcesPresenter, presenter: SourcesPresenter,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
@ -66,7 +63,6 @@ fun SourcesScreen(
presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen) presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
else -> { else -> {
SourceList( SourceList(
nestedScrollConnection = nestedScrollInterop,
state = presenter, state = presenter,
onClickItem = onClickItem, onClickItem = onClickItem,
onClickDisable = onClickDisable, onClickDisable = onClickDisable,
@ -90,7 +86,6 @@ fun SourcesScreen(
@Composable @Composable
fun SourceList( fun SourceList(
nestedScrollConnection: NestedScrollConnection,
state: SourcesState, state: SourcesState,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
@ -100,7 +95,6 @@ fun SourceList(
onClickToggleDataSaver: (Source) -> Unit, onClickToggleDataSaver: (Source) -> Unit,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
items( items(

View File

@ -0,0 +1,50 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun TabIndicator(currentTabPosition: TabPosition) {
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(currentTabPosition)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
)
}
@Composable
fun TabText(
text: String,
badgeCount: Int? = null,
isCurrentPage: Boolean,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = text,
color = if (isCurrentPage) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
)
if (badgeCount != null) {
Pill(
text = "$badgeCount",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
}

View File

@ -2,31 +2,22 @@ package eu.kanade.presentation.library.components
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.DownloadedOnlyModeBanner import eu.kanade.presentation.components.DownloadedOnlyModeBanner
import eu.kanade.presentation.components.IncognitoModeBanner import eu.kanade.presentation.components.IncognitoModeBanner
import eu.kanade.presentation.components.Pill import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -49,13 +40,7 @@ fun LibraryTabs(
ScrollableTabRow( ScrollableTabRow(
selectedTabIndex = state.currentPage.coerceAtMost(categories.lastIndex), selectedTabIndex = state.currentPage.coerceAtMost(categories.lastIndex),
edgePadding = 0.dp, edgePadding = 0.dp,
indicator = { tabPositions -> indicator = { TabIndicator(it[state.currentPage.coerceAtMost(categories.lastIndex)]) },
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[state.currentPage.coerceAtMost(categories.lastIndex)])
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
)
},
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
val count by if (showMangaCount) { val count by if (showMangaCount) {
@ -67,23 +52,13 @@ fun LibraryTabs(
selected = state.currentPage == index, selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } }, onClick = { scope.launch { state.animateScrollToPage(index) } },
text = { text = {
Row( TabText(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
// SY --> // SY -->
text = getCategoryName(category, category.visualName), text = getCategoryName(category, category.visualName),
// SY <-- // SY <--
color = if (state.currentPage == index) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground, count,
state.currentPage == index,
) )
if (count != null) {
Pill(
text = "$count",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
}, },
) )
} }

View File

@ -4,10 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import nucleus.presenter.Presenter import nucleus.presenter.Presenter
@ -29,33 +26,11 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
} }
} }
/**
* Compose controller with a Nucleus presenter.
*/
abstract class ComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
NucleusController<ComposeControllerBinding, P>(bundle),
ComposeContentController {
override fun createBinding(inflater: LayoutInflater) =
ComposeControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.root.apply {
setComposeContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
ComposeContent(nestedScrollInterop)
}
}
}
}
/** /**
* Basic Compose controller without a presenter. * Basic Compose controller without a presenter.
*/ */
abstract class BasicFullComposeController : abstract class BasicFullComposeController(bundle: Bundle? = null) :
BaseController<ComposeControllerBinding>(), BaseController<ComposeControllerBinding>(bundle),
FullComposeContentController { FullComposeContentController {
override fun createBinding(inflater: LayoutInflater) = override fun createBinding(inflater: LayoutInflater) =
@ -72,29 +47,6 @@ abstract class BasicFullComposeController :
} }
} }
abstract class SearchableComposeController<P : BasePresenter<*>>(bundle: Bundle? = null) :
SearchableNucleusController<ComposeControllerBinding, P>(bundle),
ComposeContentController {
override fun createBinding(inflater: LayoutInflater) =
ComposeControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.root.apply {
setComposeContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
ComposeContent(nestedScrollInterop)
}
}
}
}
interface FullComposeContentController { interface FullComposeContentController {
@Composable fun ComposeContent() @Composable fun ComposeContent()
} }
interface ComposeContentController {
@Composable fun ComposeContent(nestedScrollInterop: NestedScrollConnection)
}

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.ui.base.controller
import com.google.android.material.tabs.TabLayout
interface TabbedController {
/**
* @return true to let activity updates tabs visibility (to visible)
*/
fun configureTabs(tabs: TabLayout): Boolean = true
fun cleanupTabs(tabs: TabLayout) {}
}

View File

@ -1,174 +1,63 @@
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.LayoutInflater
import android.view.View import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.Controller import eu.kanade.presentation.browse.BrowseScreen
import com.bluelinelabs.conductor.ControllerChangeHandler import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.PagerControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.RxController import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsController import eu.kanade.tachiyomi.ui.browse.feed.feedTab
import eu.kanade.tachiyomi.ui.browse.feed.FeedController import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourcesTab
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import uy.kohesive.injekt.injectLazy
class BrowseController : class BrowseController : FullComposeController<BrowsePresenter>, RootController {
RxController<PagerControllerBinding>,
RootController, @Suppress("unused")
TabbedController { constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false)
constructor(toExtensions: Boolean = false) : super( constructor(toExtensions: Boolean = false) : super(
bundleOf(TO_EXTENSIONS_EXTRA to toExtensions), bundleOf(TO_EXTENSIONS_EXTRA to toExtensions),
) )
@Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getBoolean(TO_EXTENSIONS_EXTRA))
private val preferences: PreferencesHelper by injectLazy()
private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false) private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false)
val extensionListUpdateRelay: PublishRelay<Boolean> = PublishRelay.create() override fun createPresenter() = BrowsePresenter()
private var adapter: BrowseAdapter? = null @Composable
override fun ComposeContent() {
BrowseScreen(
// SY -->
startIndex = 2.takeIf { toExtensions },
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),
),
// SY <--
)
override fun getTitle(): String? { LaunchedEffect(Unit) {
return resources!!.getString(R.string.browse) (activity as? MainActivity)?.ready = true
}
} }
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
requestPermissionsSafe(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 301)
adapter = BrowseAdapter()
binding.pager.adapter = adapter
if (toExtensions) {
binding.pager.currentItem = EXTENSIONS_CONTROLLER
} }
} }
override fun onDestroyView(view: View) { private const val TO_EXTENSIONS_EXTRA = "to_extensions"
super.onDestroyView(view)
adapter = null
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
(activity as? MainActivity)?.binding?.tabs?.apply {
setupWithViewPager(binding.pager)
// Show badge on tab for extension updates
setExtensionUpdateBadge()
}
}
}
override fun configureTabs(tabs: TabLayout): Boolean {
with(tabs) {
tabGravity = TabLayout.GRAVITY_FILL
tabMode = TabLayout.MODE_FIXED
}
return true
}
override fun cleanupTabs(tabs: TabLayout) {
// Remove extension update badge
tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
}
fun setExtensionUpdateBadge() {
/* It's possible to switch to the Library controller by the time setExtensionUpdateBadge
is called, resulting in a badge being put on the category tabs (if enabled).
This check prevents that from happening */
if (router.backstack.lastOrNull()?.controller !is BrowseController) return
(activity as? MainActivity)?.binding?.tabs?.apply {
val updates = preferences.extensionUpdatesCount().get()
if (updates > 0) {
// SY -->
val badge: BadgeDrawable? = getTabAt(EXTENSIONS_CONTROLLER)?.orCreateBadge
// SY <--
badge?.isVisible = true
} else {
getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
}
}
}
private inner class BrowseAdapter : RouterPagerAdapter(this@BrowseController) {
// SY -->
private val tabTitles = (
if (preferences.feedTabInFront().get()) {
listOf(
R.string.feed,
R.string.label_sources,
R.string.label_extensions,
R.string.label_migration,
)
} else {
listOf(
R.string.label_sources,
R.string.feed,
R.string.label_extensions,
R.string.label_migration,
)
}
)
// SY <--
.map { resources!!.getString(it) }
override fun getCount(): Int {
return tabTitles.size
}
override fun configureRouter(router: Router, position: Int) {
if (!router.hasRootController()) {
val controller: Controller = when (position) {
// SY -->
SOURCES_CONTROLLER -> if (preferences.feedTabInFront().get()) FeedController() else SourcesController()
FEED_CONTROLLER -> if (!preferences.feedTabInFront().get()) FeedController() else SourcesController()
// SY <--
EXTENSIONS_CONTROLLER -> ExtensionsController()
MIGRATION_CONTROLLER -> MigrationSourcesController()
else -> error("Wrong position $position")
}
router.setRoot(RouterTransaction.with(controller))
}
}
override fun getPageTitle(position: Int): CharSequence {
return tabTitles[position]
}
}
companion object {
const val TO_EXTENSIONS_EXTRA = "to_extensions"
const val SOURCES_CONTROLLER = 0
// SY -->
const val FEED_CONTROLLER = 1
const val EXTENSIONS_CONTROLLER = 2
const val MIGRATION_CONTROLLER = 3
// SY <--
}
}

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.ui.browse
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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(
// SY -->
private val preferences: PreferencesHelper = Injekt.get(),
// SY <--
) : BasePresenter<BrowseController>() {
// SY -->
val feedTabInFront = preferences.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

@ -1,120 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.presentation.browse.ExtensionScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.queryTextChanges
class ExtensionsController : ComposeController<ExtensionsPresenter>() {
private var query = ""
init {
setHasOptionsMenu(true)
}
override fun getTitle() = applicationContext?.getString(R.string.label_extensions)
override fun createPresenter() = ExtensionsPresenter()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
ExtensionScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
else -> presenter.uninstallExtension(extension.pkgName)
}
},
onClickItemCancel = { extension ->
presenter.cancelInstallUpdateExtension(extension)
},
onClickUpdateAll = {
presenter.updateAllExtensions()
},
onLaunched = {
val ctrl = parentController as BrowseController
ctrl.setExtensionUpdateBadge()
ctrl.extensionListUpdateRelay.call(true)
},
onInstallExtension = {
presenter.installExtension(it)
},
onOpenExtension = {
val controller = ExtensionDetailsController(it.pkgName)
parentController!!.router.pushController(controller)
},
onTrustExtension = {
presenter.trustSignature(it.signatureHash)
},
onUninstallExtension = {
presenter.uninstallExtension(it.pkgName)
},
onUpdateExtension = {
presenter.updateExtension(it)
},
onRefresh = {
presenter.findAvailableExtensions()
},
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_search -> expandActionViewFromInteraction = true
R.id.action_settings -> {
parentController!!.router.pushController(ExtensionFilterController())
}
}
return super.onOptionsItemSelected(item)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isPush) {
presenter.findAvailableExtensions()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.browse_extensions, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextChanges()
.filter { router.backstack.lastOrNull()?.controller == this }
.onEach {
query = it.toString()
presenter.search(query)
}
.launchIn(viewScope)
}
}

View File

@ -1,41 +1,43 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import android.os.Bundle
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.domain.extension.interactor.GetExtensionsByType import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.presentation.browse.ExtensionState import eu.kanade.presentation.browse.ExtensionState
import eu.kanade.presentation.browse.ExtensionsState import eu.kanade.presentation.browse.ExtensionsState
import eu.kanade.presentation.browse.ExtensionsStateImpl import eu.kanade.presentation.browse.ExtensionsStateImpl
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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
import eu.kanade.tachiyomi.extension.model.InstallStep 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.ui.base.presenter.BasePresenter
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.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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 ExtensionsPresenter(
private val presenterScope: CoroutineScope,
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl, private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
private val preferences: PreferencesHelper = 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(),
) : BasePresenter<ExtensionsController>(), ExtensionsState by state { ) : ExtensionsState by state {
private val _query: MutableStateFlow<String> = MutableStateFlow("") private val _query: MutableStateFlow<String> = MutableStateFlow("")
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
override fun onCreate(savedState: Bundle?) { fun onCreate() {
super.onCreate(savedState)
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 ->
{ {
@ -114,6 +116,10 @@ class ExtensionsPresenter(
} }
presenterScope.launchIO { findAvailableExtensions() } presenterScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount().asFlow()
.onEach { state.updates = it }
.launchIn(presenterScope)
} }
fun search(query: String) { fun search(query: String) {

View File

@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.ExtensionScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
@Composable
fun extensionsTab(
router: Router?,
presenter: ExtensionsPresenter,
) = BrowseTab(
titleRes = R.string.label_extensions,
badgeNumber = presenter.updates.takeIf { it > 0 },
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = {
// TODO: extensions search
// presenter.search(query)
},
),
AppBar.Action(
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList,
onClick = { router?.pushController(ExtensionFilterController()) },
),
),
content = {
ExtensionScreen(
presenter = presenter,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
else -> presenter.uninstallExtension(extension.pkgName)
}
},
onClickItemCancel = { extension ->
presenter.cancelInstallUpdateExtension(extension)
},
onClickUpdateAll = {
presenter.updateAllExtensions()
},
onInstallExtension = {
presenter.installExtension(it)
},
onOpenExtension = {
router?.pushController(ExtensionDetailsController(it.pkgName))
},
onTrustExtension = {
presenter.trustSignature(it.signatureHash)
},
onUninstallExtension = {
presenter.uninstallExtension(it.pkgName)
},
onUpdateExtension = {
presenter.updateExtension(it)
},
onRefresh = {
presenter.findAvailableExtensions()
},
)
},
)

View File

@ -1,144 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.feed
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.presentation.browse.FeedScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
/**
* This controller shows and manages the different search result in global search.
* This controller should only handle UI actions, IO actions should be done by [FeedPresenter]
* [FeedCardAdapter.OnMangaClickListener] called when manga is clicked in global search
*/
class FeedController : ComposeController<FeedPresenter>() {
init {
setHasOptionsMenu(true)
}
override fun getTitle(): String? {
return applicationContext?.getString(R.string.feed)
}
/**
* Create the [FeedPresenter] used in controller.
*
* @return instance of [FeedPresenter]
*/
override fun createPresenter(): FeedPresenter {
return FeedPresenter()
}
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
FeedScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickSavedSearch = ::onSavedSearchClick,
onClickSource = ::onSourceClick,
onClickDelete = ::onRemoveClick,
onClickManga = ::onMangaClick,
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.feed, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_add_feed -> addFeed()
}
return super.onOptionsItemSelected(item)
}
private fun addFeed() {
viewScope.launchUI {
if (presenter.hasTooManyFeeds()) {
activity?.toast(R.string.too_many_in_feed)
return@launchUI
}
val items = presenter.getEnabledSources()
val itemsStrings = items.map { it.toString() }
var selectedIndex = 0
MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.feed)
.setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton(android.R.string.ok) { _, _ ->
addFeedSearch(items[selectedIndex])
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
private fun addFeedSearch(source: CatalogueSource) {
viewScope.launchUI {
val items = presenter.getSourceSavedSearches(source.id)
val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name }
var selectedIndex = 0
MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.feed)
.setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton(android.R.string.ok) { _, _ ->
presenter.createFeed(source, items.getOrNull(selectedIndex - 1))
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
/**
* Called when manga in global search is clicked, opens manga.
*
* @param manga clicked item containing manga information.
*/
private fun onMangaClick(manga: eu.kanade.domain.manga.model.Manga) {
// Open MangaController.
parentController?.router?.pushController(MangaController(manga.id, true))
}
/**
* Opens a catalogue with the given search.
*/
private fun onSourceClick(source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(source.id)
parentController?.router?.pushController(LatestUpdatesController(source))
}
private fun onSavedSearchClick(savedSearch: SavedSearch, source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(savedSearch.source)
parentController?.router?.pushController(BrowseSourceController(source, savedSearch = savedSearch.id))
}
private fun onRemoveClick(feedSavedSearch: FeedSavedSearch) {
MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.feed)
.setMessage(R.string.feed_delete)
.setPositiveButton(R.string.action_delete) { _, _ ->
presenter.deleteFeed(feedSavedSearch)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.browse.feed package eu.kanade.tachiyomi.ui.browse.feed
import android.os.Bundle
import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.InsertManga import eu.kanade.domain.manga.interactor.InsertManga
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
@ -24,13 +23,13 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.system.LocaleHelper 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.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
@ -55,6 +54,7 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class FeedPresenter( open class FeedPresenter(
private val presenterScope: CoroutineScope,
private val state: FeedStateImpl = FeedState() as FeedStateImpl, private val state: FeedStateImpl = FeedState() as FeedStateImpl,
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
@ -67,7 +67,7 @@ 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(),
) : BasePresenter<FeedController>(), FeedState by state { ) : FeedState by state {
/** /**
* Fetches the different sources by user settings. * Fetches the different sources by user settings.
@ -84,9 +84,7 @@ open class FeedPresenter(
*/ */
private var fetchImageSubscription: Subscription? = null private var fetchImageSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) { fun onCreate() {
super.onCreate(savedState)
getFeedSavedSearchGlobal.subscribe() getFeedSavedSearchGlobal.subscribe()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach {
@ -106,10 +104,24 @@ open class FeedPresenter(
.launchIn(presenterScope) .launchIn(presenterScope)
} }
override fun onDestroy() { fun onDestroy() {
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe() fetchImageSubscription?.unsubscribe()
super.onDestroy() }
fun openAddDialog() {
presenterScope.launchIO {
if (hasTooManyFeeds()) {
return@launchIO
}
dialog = Dialog.AddFeed(getEnabledSources())
}
}
fun openAddSearchDialog(source: CatalogueSource) {
presenterScope.launchIO {
dialog = Dialog.AddFeedSearch(source, (if (source.supportsLatest) listOf(null) else emptyList()) + getSourceSavedSearches(source.id))
}
} }
suspend fun hasTooManyFeeds(): Boolean { suspend fun hasTooManyFeeds(): Boolean {
@ -330,4 +342,10 @@ open class FeedPresenter(
} }
return localManga?.toDbManga()!! return localManga?.toDbManga()!!
} }
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()
}
} }

View File

@ -0,0 +1,60 @@
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.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.FeedScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaController
@Composable
fun feedTab(
router: Router?,
presenter: FeedPresenter,
) = BrowseTab(
titleRes = R.string.feed,
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_add),
icon = Icons.Outlined.Add,
onClick = {
presenter.openAddDialog()
},
),
),
content = {
FeedScreen(
presenter = presenter,
onClickAdd = {
presenter.openAddSearchDialog(it)
},
onClickCreate = { source, savedSearch ->
presenter.createFeed(source, savedSearch)
},
onClickSavedSearch = { savedSearch, source ->
presenter.preferences.lastUsedSource().set(savedSearch.source)
router?.pushController(BrowseSourceController(source, savedSearch = savedSearch.id))
},
onClickSource = { source ->
presenter.preferences.lastUsedSource().set(source.id)
router?.pushController(LatestUpdatesController(source))
},
onClickDelete = {
presenter.dialog = FeedPresenter.Dialog.DeleteFeed(it)
},
onClickDeleteConfirm = {
presenter.deleteFeed(it)
},
onClickManga = { manga ->
router?.pushController(MangaController(manga.id, true))
},
)
},
)

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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.manga.MigrationMangaController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun migrateSourcesTab(
router: Router?,
presenter: MigrationSourcesPresenter,
): BrowseTab {
val uriHandler = LocalUriHandler.current
return BrowseTab(
titleRes = R.string.label_migration,
actions = listOf(
AppBar.Action(
title = stringResource(R.string.migration_help_guide),
icon = Icons.Outlined.HelpOutline,
onClick = {
uriHandler.openUri("https://tachiyomi.org/help/guides/source-migration/")
},
),
),
content = {
MigrateSourceScreen(
presenter = presenter,
onClickItem = { source ->
router?.pushController(
MigrationMangaController(
source.id,
source.name,
),
)
},
// SY -->
onClickAll = { source ->
// TODO: Jay wtf, need to clean this up sometime
launchIO {
val manga = Injekt.get<GetFavorites>().await()
val sourceMangas =
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
withUIContext {
if (router != null) {
PreMigrationController.navigateToMigration(
Injekt.get<PreferencesHelper>().skipPreMigration().get(),
router,
sourceMangas,
)
}
}
}
},
// SY <--
)
},
)
}

View File

@ -1,99 +1,16 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import eu.kanade.presentation.browse.BrowseTabWrapper
import eu.kanade.domain.manga.interactor.GetFavorites import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.openInBrowser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() { class MigrationSourcesController : FullComposeController<MigrationSourcesPresenterWrapper>() {
init { override fun createPresenter() = MigrationSourcesPresenterWrapper()
setHasOptionsMenu(true)
}
override fun createPresenter() = MigrationSourcesPresenter()
@Composable @Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { override fun ComposeContent() {
MigrateSourceScreen( BrowseTabWrapper(migrateSourcesTab(router, presenter = presenter.presenter))
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickItem = { source ->
val parentController = parentController
if (parentController is BrowseController) {
parentController.router
} else {
router
}.pushController(
MigrationMangaController(
source.id,
source.name,
),
)
},
onClickAll = { source ->
// TODO: Jay wtf, need to clean this up sometime
launchIO {
val manga = Injekt.get<GetFavorites>().await()
val sourceMangas =
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
withUIContext {
PreMigrationController.navigateToMigration(
Injekt.get<PreferencesHelper>().skipPreMigration().get(),
run {
val parentController = parentController
if (parentController is BrowseController) {
parentController.router
} else {
router
}
},
sourceMangas,
)
}
}
},
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
inflater.inflate(R.menu.browse_migrate, menu)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (val itemId = item.itemId) {
R.id.action_source_migration_help -> {
activity?.openInBrowser(HELP_URL)
true
}
R.id.asc_alphabetical,
R.id.desc_alphabetical,
-> {
presenter.setAlphabeticalSorting(itemId == R.id.asc_alphabetical)
true
}
R.id.asc_count,
R.id.desc_count,
-> {
presenter.setTotalSorting(itemId == R.id.asc_count)
true
}
else -> super.onOptionsItemSelected(item)
}
} }
} }

View File

@ -1,33 +1,35 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.presentation.browse.MigrateSourceState import eu.kanade.presentation.browse.MigrateSourceState
import eu.kanade.presentation.browse.MigrateSourceStateImpl import eu.kanade.presentation.browse.MigrateSourceStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
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
class MigrationSourcesPresenter( class MigrationSourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl, private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state { ) : MigrateSourceState by state {
private val _channel = Channel<Event>(Int.MAX_VALUE) private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow() val channel = _channel.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { fun onCreate() {
super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getSourcesWithFavoriteCount.subscribe() getSourcesWithFavoriteCount.subscribe()
.catch { exception -> .catch { exception ->
@ -39,14 +41,32 @@ class MigrationSourcesPresenter(
state.isLoading = false state.isLoading = false
} }
} }
preferences.migrationSortingDirection().asFlow()
.onEach { state.sortingDirection = it }
.launchIn(presenterScope)
preferences.migrationSortingMode().asFlow()
.onEach { state.sortingMode = it }
.launchIn(presenterScope)
} }
fun setAlphabeticalSorting(isAscending: Boolean) { fun toggleSortingMode() {
setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending) val newMode = when (state.sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> SetMigrateSorting.Mode.TOTAL
SetMigrateSorting.Mode.TOTAL -> SetMigrateSorting.Mode.ALPHABETICAL
} }
fun setTotalSorting(isAscending: Boolean) { setMigrateSorting.await(newMode, state.sortingDirection)
setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending) }
fun toggleSortingDirection() {
val newDirection = when (state.sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> SetMigrateSorting.Direction.DESCENDING
SetMigrateSorting.Direction.DESCENDING -> SetMigrateSorting.Direction.ASCENDING
}
setMigrateSorting.await(state.sortingMode, newDirection)
} }
sealed class Event { sealed class Event {

View File

@ -0,0 +1,14 @@
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

@ -3,46 +3,18 @@ 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.os.Parcelable
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import eu.kanade.presentation.browse.BrowseTabWrapper
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import com.bluelinelabs.conductor.Controller
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourcesScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
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 eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.getParcelableCompat import eu.kanade.tachiyomi.util.system.getParcelableCompat
import exh.ui.smartsearch.SmartSearchController
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import uy.kohesive.injekt.injectLazy
class SourcesController(bundle: Bundle? = null) : SearchableComposeController<SourcesPresenter>(bundle) { class SourcesController(bundle: Bundle? = null) : FullComposeController<SourcesPresenterWrapper>(bundle) {
private val preferences: PreferencesHelper by injectLazy()
// EXH -->
private val smartSearchConfig = args.getParcelableCompat<SmartSearchConfig>(SMART_SEARCH_CONFIG) private val smartSearchConfig = args.getParcelableCompat<SmartSearchConfig>(SMART_SEARCH_CONFIG)
private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH
// EXH <--
init {
// SY -->
setHasOptionsMenu(mode == Mode.CATALOGUE)
// SY <--
}
override fun getTitle(): String? { override fun getTitle(): String? {
// SY --> // SY -->
@ -53,40 +25,11 @@ class SourcesController(bundle: Bundle? = null) : SearchableComposeController<So
// SY <-- // SY <--
} }
override fun createPresenter() = SourcesPresenter(/* SY --> */ controllerMode = mode /* SY <-- */) override fun createPresenter() = SourcesPresenterWrapper(controllerMode = mode, smartSearchConfig = smartSearchConfig)
@Composable @Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { override fun ComposeContent() {
SourcesScreen( BrowseTabWrapper(sourcesTab(router, presenter = presenter.presenter))
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickItem = { source ->
when {
mode == Mode.SMART_SEARCH -> router.pushController(SmartSearchController(source.id, smartSearchConfig!!))
preferences.useNewSourceNavigation().get() -> openSource(source, SourceFeedController(source.id))
else -> openSource(source, BrowseSourceController(source))
}
},
onClickDisable = { source ->
presenter.toggleSource(source)
},
onClickLatest = { source ->
openSource(source, LatestUpdatesController(source))
},
onClickPin = { source ->
presenter.togglePin(source)
},
onClickSetCategories = { source, categories ->
presenter.setSourceCategories(source, categories)
},
onClickToggleDataSaver = { source ->
presenter.toggleExcludeFromDataSaver(source)
},
)
LaunchedEffect(Unit) {
(activity as? MainActivity)?.ready = true
}
} }
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
@ -94,57 +37,6 @@ class SourcesController(bundle: Bundle? = null) : SearchableComposeController<So
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
} }
/**
* Opens a catalogue with the given controller.
*/
private fun openSource(source: Source, controller: Controller) {
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
parentController!!.router.pushController(controller)
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
parentController!!.router.pushController(SourceFilterController())
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (mode == Mode.CATALOGUE) {
createOptionsMenu(
menu,
inflater,
R.menu.browse_sources,
R.id.action_search,
R.string.action_global_search_hint,
false, // GlobalSearch handles the searching here
)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
// SY -->
if (mode == Mode.CATALOGUE) {
parentController!!.router.pushController(
GlobalSearchController(query),
)
}
// SY <--
}
// SY -->
@Parcelize @Parcelize
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
@ -157,5 +49,4 @@ class SourcesController(bundle: Bundle? = null) : SearchableComposeController<So
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG" const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
const val SMART_SEARCH_SOURCE_TAG = "smart_search_source_tag" const val SMART_SEARCH_SOURCE_TAG = "smart_search_source_tag"
} }
// SY <--
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import android.os.Bundle
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
import eu.kanade.domain.source.interactor.GetSourceCategories import eu.kanade.domain.source.interactor.GetSourceCategories
@ -13,8 +12,9 @@ import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourceUiModel import eu.kanade.presentation.browse.SourceUiModel
import eu.kanade.presentation.browse.SourcesState import eu.kanade.presentation.browse.SourcesState
import eu.kanade.presentation.browse.SourcesStateImpl import eu.kanade.presentation.browse.SourcesStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.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.flow.catch import kotlinx.coroutines.flow.catch
@ -29,7 +29,9 @@ import uy.kohesive.injekt.api.get
import java.util.TreeMap import java.util.TreeMap
class SourcesPresenter( class SourcesPresenter(
private val presenterScope: CoroutineScope,
private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl, private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
private val preferences: PreferencesHelper = Injekt.get(),
private val getEnabledSources: GetEnabledSources = Injekt.get(), private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
@ -38,15 +40,17 @@ 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(),
private val controllerMode: SourcesController.Mode, val controllerMode: SourcesController.Mode,
val smartSearchConfig: SourcesController.SmartSearchConfig?,
// SY <-- // SY <--
) : BasePresenter<SourcesController>(), SourcesState by state { ) : SourcesState by state {
private val _events = Channel<Event>(Int.MAX_VALUE) private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { val useNewSourceNavigation = preferences.useNewSourceNavigation().get()
super.onCreate(savedState)
fun onCreate() {
// SY --> // SY -->
combine( combine(
getEnabledSources.subscribe(), getEnabledSources.subscribe(),
@ -105,6 +109,12 @@ class SourcesPresenter(
state.items = uiModels state.items = uiModels
} }
fun onOpenSource(source: Source) {
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
}
fun toggleSource(source: Source) { fun toggleSource(source: Source) {
toggleSource.await(source) toggleSource.await(source)
} }

View File

@ -0,0 +1,14 @@
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,80 @@
package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.TravelExplore
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.bluelinelabs.conductor.Router
import eu.kanade.presentation.browse.BrowseTab
import eu.kanade.presentation.browse.SourcesScreen
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import exh.ui.smartsearch.SmartSearchController
@Composable
fun sourcesTab(
router: Router?,
presenter: SourcesPresenter,
) = BrowseTab(
// SY -->
titleRes = when (presenter.controllerMode) {
SourcesController.Mode.CATALOGUE -> R.string.label_sources
SourcesController.Mode.SMART_SEARCH -> R.string.find_in_another_source
},
actions = if (presenter.controllerMode == SourcesController.Mode.CATALOGUE) {
listOf(
AppBar.Action(
title = stringResource(R.string.action_global_search),
icon = Icons.Outlined.TravelExplore,
onClick = { router?.pushController(GlobalSearchController()) },
),
AppBar.Action(
title = stringResource(R.string.action_filter),
icon = Icons.Outlined.FilterList,
onClick = { router?.pushController(SourceFilterController()) },
),
)
} else emptyList(),
// SY <--
content = {
SourcesScreen(
presenter = presenter,
onClickItem = { source ->
// SY -->
val controller = when {
presenter.controllerMode == SourcesController.Mode.SMART_SEARCH ->
SmartSearchController(source.id, presenter.smartSearchConfig!!)
presenter.useNewSourceNavigation -> SourceFeedController(source.id)
else -> BrowseSourceController(source)
}
presenter.onOpenSource(source)
router?.pushController(controller)
// SY <--
},
onClickDisable = { source ->
presenter.toggleSource(source)
},
onClickLatest = { source ->
presenter.onOpenSource(source)
router?.pushController(LatestUpdatesController(source))
},
onClickPin = { source ->
presenter.togglePin(source)
},
// SY -->
onClickSetCategories = { source, categories ->
presenter.setSourceCategories(source, categories)
},
onClickToggleDataSaver = { source ->
presenter.toggleExcludeFromDataSaver(source)
},
// SY <--
)
},
)

View File

@ -46,7 +46,6 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController import eu.kanade.tachiyomi.ui.base.controller.FullComposeContentController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.setRoot import eu.kanade.tachiyomi.ui.base.controller.setRoot
import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.BrowseController
@ -189,7 +188,7 @@ class MainActivity : BaseActivity() {
R.id.nav_library -> router.setRoot(LibraryController(), id) R.id.nav_library -> router.setRoot(LibraryController(), id)
R.id.nav_updates -> router.setRoot(UpdatesController(), id) R.id.nav_updates -> router.setRoot(UpdatesController(), id)
R.id.nav_history -> router.setRoot(HistoryController(), id) R.id.nav_history -> router.setRoot(HistoryController(), id)
R.id.nav_browse -> router.setRoot(BrowseController(), id) R.id.nav_browse -> router.setRoot(BrowseController(toExtensions = false), id)
R.id.nav_more -> router.setRoot(MoreController(), id) R.id.nav_more -> router.setRoot(MoreController(), id)
} }
} else if (!isHandlingShortcut) { } else if (!isHandlingShortcut) {
@ -644,17 +643,6 @@ class MainActivity : BaseActivity() {
showNav(true) showNav(true)
} }
if (from is TabbedController) {
from.cleanupTabs(binding.tabs)
}
if (internalTo is TabbedController) {
if (internalTo.configureTabs(binding.tabs)) {
binding.tabs.isVisible = true
}
} else {
binding.tabs.isVisible = false
}
if (from is FabController) { if (from is FabController) {
from.cleanupFab(binding.fabLayout.rootFab) from.cleanupFab(binding.fabLayout.rootFab)
} }

View File

@ -114,7 +114,7 @@ class SettingsDebugController : BasicFullComposeController() {
fun FunctionList( fun FunctionList(
paddingValues: PaddingValues, paddingValues: PaddingValues,
functions: List<Pair<KFunction<*>, String>>, functions: List<Pair<KFunction<*>, String>>,
toggles: List<DebugToggle> toggles: List<DebugToggle>,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M19.3,16.9c0.4,-0.7 0.7,-1.5 0.7,-2.4c0,-2.5 -2,-4.5 -4.5,-4.5S11,12 11,14.5s2,4.5 4.5,4.5c0.9,0 1.7,-0.3 2.4,-0.7l3.2,3.2l1.4,-1.4L19.3,16.9zM15.5,17c-1.4,0 -2.5,-1.1 -2.5,-2.5s1.1,-2.5 2.5,-2.5s2.5,1.1 2.5,2.5S16.9,17 15.5,17zM12,20v2C6.48,22 2,17.52 2,12C2,6.48 6.48,2 12,2c4.84,0 8.87,3.44 9.8,8h-2.07c-0.64,-2.46 -2.4,-4.47 -4.73,-5.41V5c0,1.1 -0.9,2 -2,2h-2v2c0,0.55 -0.45,1 -1,1H8v2h2v3H9l-4.79,-4.79C4.08,10.79 4,11.38 4,12C4,16.41 7.59,20 12,20z"/>
</vector>

View File

@ -26,11 +26,6 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" /> android:theme="?attr/actionBarTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView <TextView
android:id="@+id/downloaded_only" android:id="@+id/downloaded_only"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -25,12 +25,6 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" /> android:theme="?attr/actionBarTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/downloaded_only" android:id="@+id/downloaded_only"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,19 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_translate_24dp"
android:title="@string/action_filter"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,47 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_sort"
android:icon="@drawable/ic_sort_24dp"
android:title="@string/action_sort"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" >
<menu>
<item
android:id="@+id/action_sort_alphabetical"
android:title="@string/action_sort_alpha"
app:showAsAction="never">
<menu>
<item
android:id="@+id/asc_alphabetical"
android:title="@string/action_asc" />
<item
android:id="@+id/desc_alphabetical"
android:title="@string/action_desc" />
</menu>
</item>
<item
android:id="@+id/action_sort_count"
android:title="@string/action_sort_count"
app:showAsAction="never">
<menu>
<item
android:id="@+id/asc_count"
android:title="@string/action_asc" />
<item
android:id="@+id/desc_count"
android:title="@string/action_desc" />
</menu>
</item>
</menu>
</item>
<item
android:id="@+id/action_source_migration_help"
android:icon="@drawable/ic_help_24dp"
android:title="@string/migration_help_guide"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,19 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_travel_explore_24dp"
android:title="@string/action_global_search"
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_filter_list_24dp"
android:title="@string/action_filter"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View File

@ -64,7 +64,6 @@ directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1" insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" } conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" }
conductor-viewpager = { module = "com.bluelinelabs:conductor-viewpager", version.ref = "conductor_version" }
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" }
flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding_version" } flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding_version" }
@ -99,7 +98,7 @@ sqlite = ["sqlitektx", "sqlite-android"]
nucleus = ["nucleus-core", "nucleus-supportv7"] nucleus = ["nucleus-core", "nucleus-supportv7"]
coil = ["coil-core", "coil-gif", "coil-compose"] coil = ["coil-core", "coil-gif", "coil-compose"]
flowbinding = ["flowbinding-android", "flowbinding-appcompat"] flowbinding = ["flowbinding-android", "flowbinding-appcompat"]
conductor = ["conductor-core", "conductor-viewpager", "conductor-support-preference"] conductor = ["conductor-core", "conductor-support-preference"]
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
[plugins] [plugins]