Misc cleanup

- Migrate sources filter screen to full compose
- Use standard "OK"/"Cancel" actions for delete category dialog
- Abstract some AppBar logic
- Remove some dead code
- Group related strings
This commit is contained in:
arkon 2022-07-18 18:32:25 -04:00
parent 00519e3b93
commit e2510c144a
13 changed files with 160 additions and 274 deletions

View File

@ -1,24 +1,27 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.res.stringResource
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter
@ -28,22 +31,32 @@ import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesFilterScreen(
nestedScrollInterop: NestedScrollConnection,
navigateUp: () -> Unit,
presenter: SourcesFilterPresenter,
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.source_filter_empty_screen)
else -> {
SourcesFilterContent(
nestedScrollInterop = nestedScrollInterop,
state = presenter,
onClickLang = onClickLang,
onClickSource = onClickSource,
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.label_sources),
navigateUp = navigateUp,
)
},
) { paddingValues ->
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.source_filter_empty_screen)
else -> {
SourcesFilterContent(
paddingValues = paddingValues,
state = presenter,
onClickLang = onClickLang,
onClickSource = onClickSource,
)
}
}
}
LaunchedEffect(Unit) {
@ -59,14 +72,13 @@ fun SourcesFilterScreen(
@Composable
fun SourcesFilterContent(
nestedScrollInterop: NestedScrollConnection,
paddingValues: PaddingValues,
state: SourcesFilterState,
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues(),
) {
items(
items = state.items,

View File

@ -118,16 +118,16 @@ fun CategoryDeleteDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.no))
}
},
dismissButton = {
TextButton(onClick = {
onDelete()
onDismissRequest()
},) {
Text(text = stringResource(R.string.yes))
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
title = {

View File

@ -2,8 +2,14 @@ package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
@ -11,11 +17,16 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -24,29 +35,82 @@ import eu.kanade.tachiyomi.R
@Composable
fun AppBar(
modifier: Modifier = Modifier,
// Text
title: String?,
subtitle: String? = null,
// Up button
navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack,
// Menu
actions: @Composable RowScope.() -> Unit = {},
// Action mode
actionModeCounter: Int = 0,
onCancelActionMode: () -> Unit = {},
actionModeActions: @Composable RowScope.() -> Unit = {},
// Banners
downloadedOnlyMode: Boolean = false,
incognitoMode: Boolean = false,
) {
SmallTopAppBar(
navigationIcon = {
navigateUp?.let {
IconButton(onClick = it) {
Icon(
imageVector = navigationIcon,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
val isActionMode by derivedStateOf { actionModeCounter > 0 }
val backgroundColor = if (isActionMode) {
TopAppBarDefaults.smallTopAppBarColors().containerColor(1f).value
} else {
MaterialTheme.colorScheme.surface
}
Column(
modifier = modifier.drawBehind { drawRect(backgroundColor) },
) {
SmallTopAppBar(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
navigationIcon = {
if (isActionMode) {
IconButton(onClick = onCancelActionMode) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(id = R.string.action_cancel),
)
}
} else {
navigateUp?.let {
IconButton(onClick = it) {
Icon(
imageVector = navigationIcon,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
}
}
}
},
title = {
AppBarTitle(title, subtitle)
// TODO: incognito/downloaded only banners
},
actions = actions,
)
},
title = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
} else {
AppBarTitle(title, subtitle)
}
},
actions = {
if (isActionMode) {
actionModeActions()
} else {
actions()
}
},
// Background handled by parent
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
)
if (downloadedOnlyMode) {
DownloadedOnlyModeBanner()
}
if (incognitoMode) {
IncognitoModeBanner()
}
}
}
@Composable

View File

@ -60,9 +60,9 @@ import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
import eu.kanade.presentation.manga.components.MangaActionRow
import eu.kanade.presentation.manga.components.MangaAppBar
import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaSmallAppBar
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrollingUp
import eu.kanade.presentation.util.plus
@ -237,7 +237,7 @@ private fun MangaScreenSmallImpl(
val animatedBgAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
)
MangaSmallAppBar(
MangaAppBar(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
@ -458,7 +458,7 @@ fun MangaScreenLargeImpl(
Scaffold(
modifier = Modifier.padding(insetPadding),
topBar = {
MangaSmallAppBar(
MangaAppBar(
modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
title = state.manga.title,
titleAlphaProvider = { if (chapters.any { it.selected }) 1f else 0f },

View File

@ -38,7 +38,7 @@ import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R
@Composable
fun MangaSmallAppBar(
fun MangaAppBar(
modifier: Modifier = Modifier,
title: String,
titleAlphaProvider: () -> Float,
@ -57,7 +57,7 @@ fun MangaSmallAppBar(
) {
val isActionMode = actionModeCounter > 0
val backgroundAlpha = if (isActionMode) 1f else backgroundAlphaProvider()
val backgroundColor by TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f)
val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(1f)
Column(
modifier = modifier.drawBehind {
drawRect(backgroundColor.copy(alpha = backgroundAlpha))
@ -199,7 +199,7 @@ fun MangaSmallAppBar(
}
},
// Background handled by parent
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),

View File

@ -1,7 +1,6 @@
package eu.kanade.presentation.updates
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
@ -12,35 +11,25 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FlipToBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.DownloadedOnlyModeBanner
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.IncognitoModeBanner
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefreshIndicator
@ -201,72 +190,36 @@ fun UpdatesAppBar(
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
) {
val isActionMode = actionModeCounter > 0
val backgroundColor = if (isActionMode) {
TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f).value
} else {
MaterialTheme.colorScheme.surface
}
Column(
modifier = modifier.drawBehind { drawRect(backgroundColor) },
) {
SmallTopAppBar(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
navigationIcon = {
if (isActionMode) {
IconButton(onClick = { selected.clear() }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(id = R.string.action_cancel),
)
}
}
},
title = {
Text(
text = if (isActionMode) actionModeCounter.toString() else stringResource(R.string.label_recent_updates),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
AppBar(
modifier = modifier,
title = stringResource(R.string.label_recent_updates),
actions = {
IconButton(onClick = onUpdateLibrary) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.action_update_library),
)
},
actions = {
if (isActionMode) {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Default.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse),
)
}
} else {
IconButton(onClick = onUpdateLibrary) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.action_update_library),
)
}
}
},
// Background handled by parent
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
)
if (downloadedOnlyMode) {
DownloadedOnlyModeBanner()
}
if (incognitoMode) {
IncognitoModeBanner()
}
}
}
},
actionModeCounter = actionModeCounter,
onCancelActionMode = { selected.clear() },
actionModeActions = {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Default.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse),
)
}
},
downloadedOnlyMode = downloadedOnlyMode,
incognitoMode = incognitoMode,
)
}
@Composable

View File

@ -1,22 +1,18 @@
package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourcesFilterScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
class SourceFilterController : ComposeController<SourcesFilterPresenter>() {
override fun getTitle() = resources?.getString(R.string.label_sources)
class SourceFilterController : FullComposeController<SourcesFilterPresenter>() {
override fun createPresenter(): SourcesFilterPresenter = SourcesFilterPresenter()
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
override fun ComposeContent() {
SourcesFilterScreen(
nestedScrollInterop = nestedScrollInterop,
navigateUp = router::popCurrentController,
presenter = presenter,
onClickLang = { language ->
presenter.toggleLanguage(language)

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.chapter
import android.content.Context
import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ChapterDownloadIndicator
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.download.model.Download
class ChapterDownloadView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
private var state by mutableStateOf(Download.State.NOT_DOWNLOADED)
private var progress by mutableStateOf(0)
var listener: (ChapterDownloadAction) -> Unit = {}
@Composable
override fun Content() {
TachiyomiTheme {
ChapterDownloadIndicator(
downloadStateProvider = { state },
downloadProgressProvider = { progress },
onClick = listener,
)
}
}
fun setState(state: Download.State, progress: Int = 0) {
this.state = state
this.progress = progress
}
}

View File

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.chapter.base
import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.presentation.components.ChapterDownloadAction
open class BaseChapterHolder(
view: View,
private val adapter: BaseChaptersAdapter<*>,
) : FlexibleViewHolder(view, adapter) {
val downloadActionListener: (ChapterDownloadAction) -> Unit = { action ->
when (action) {
ChapterDownloadAction.START -> adapter.clickListener.downloadChapter(bindingAdapterPosition)
ChapterDownloadAction.START_NOW -> adapter.clickListener.startDownloadNow(bindingAdapterPosition)
ChapterDownloadAction.CANCEL, ChapterDownloadAction.DELETE -> {
adapter.clickListener.deleteChapter(bindingAdapterPosition)
}
}
}
}

View File

@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.chapter.base
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
abstract class BaseChapterItem<T : BaseChapterHolder, H : AbstractHeaderItem<*>>(
val chapter: Chapter,
header: H? = null,
) : AbstractSectionableItem<T, H?>(header) {
private var _status: Download.State = Download.State.NOT_DOWNLOADED
var status: Download.State
get() = download?.status ?: _status
set(value) {
_status = value
}
val progress: Int
get() {
val pages = download?.pages ?: return 0
return pages.map(Page::progress).average().toInt()
}
@Transient
var download: Download? = null
val isDownloaded: Boolean
get() = status == Download.State.DOWNLOADED
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is BaseChapterItem<*, *>) {
return chapter.id == other.chapter.id && chapter.read == other.chapter.read
}
return false
}
override fun hashCode(): Int {
var result = chapter.id.hashCode()
result = 31 * result + chapter.read.hashCode()
return result
}
}

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.chapter.base
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
abstract class BaseChaptersAdapter<T : IFlexible<*>>(
controller: OnChapterClickListener,
items: List<T>? = null,
) : FlexibleAdapter<T>(items, controller, true) {
/**
* Listener for browse item clicks.
*/
val clickListener: OnChapterClickListener = controller
/**
* Listener which should be called when user clicks the download icons.
*/
interface OnChapterClickListener {
fun downloadChapter(position: Int)
fun deleteChapter(position: Int)
fun startDownloadNow(position: Int)
}
}

View File

@ -24,7 +24,6 @@
android:layout_height="95dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:contentDescription="@string/description_cover"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View File

@ -72,6 +72,8 @@
<string name="action_edit_categories">Edit categories</string>
<string name="action_rename_category">Rename category</string>
<string name="action_move_category">Set categories</string>
<string name="delete_category_confirmation">Do you wish to delete the category \"%s\"?</string>
<string name="delete_category">Delete category</string>
<string name="action_edit_cover">Edit cover</string>
<string name="action_view_chapters">View chapters</string>
<string name="action_stop">Stop</string>
@ -135,6 +137,7 @@
<!-- Operations -->
<string name="loading">Loading…</string>
<string name="internal_error">InternalError: Check crash logs for further information</string>
<!-- Shortcuts-->
<string name="app_not_available">App not available</string>
@ -357,6 +360,8 @@
<string name="scale_type_fit_height">Fit height</string>
<string name="scale_type_original_size">Original size</string>
<string name="scale_type_smart_fit">Smart fit</string>
<string name="pref_navigate_pan">Navigate to pan</string>
<string name="pref_landscape_zoom">Zoom landscape image</string>
<string name="pref_zoom_start">Zoom start position</string>
<string name="zoom_start_automatic">Automatic</string>
<string name="zoom_start_left">Left</string>
@ -722,6 +727,7 @@
<!-- Updates fragment -->
<string name="updating_library">Updating library</string>
<string name="cant_open_last_read_chapter">Unable to open last read chapter</string>
<!-- History fragment -->
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
@ -804,9 +810,6 @@
<item quantity="other">%d extension updates available</item>
</plurals>
<!--Content Description-->
<string name="description_cover">Cover of manga</string>
<!-- Information Text -->
<string name="information_no_downloads">No downloads</string>
<string name="information_no_recent">No recent updates</string>
@ -848,12 +851,4 @@
<!-- S Pen actions -->
<string name="spen_previous_page">Previous page</string>
<string name="spen_next_page">Next page</string>
<string name="pref_navigate_pan">Navigate to pan</string>
<string name="pref_landscape_zoom">Zoom landscape image</string>
<string name="cant_open_last_read_chapter">Unable to open last read chapter</string>
<string name="delete_category_confirmation">Do you wish to delete the category %s</string>
<string name="delete_category">Delete category</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="internal_error">InternalError: Check crash logs for further information</string>
</resources>