MangaController overhaul (#7244)
This commit is contained in:
parent
cf7ca5bd28
commit
33a778873a
@ -150,13 +150,16 @@ dependencies {
|
||||
implementation(compose.activity)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3.core)
|
||||
implementation(compose.material3.windowsizeclass)
|
||||
implementation(compose.material3.adapter)
|
||||
implementation(compose.material.icons)
|
||||
implementation(compose.animation)
|
||||
implementation(compose.animation.graphics)
|
||||
implementation(compose.ui.tooling)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.webview)
|
||||
implementation(compose.accompanist.swiperefresh)
|
||||
implementation(compose.accompanist.flowlayout)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
@ -299,7 +302,9 @@ tasks {
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,10 @@ class MangaRepositoryImpl(
|
||||
return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
|
||||
return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
|
||||
}
|
||||
|
||||
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
|
||||
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
|
||||
import eu.kanade.domain.manga.interactor.GetMangaById
|
||||
import eu.kanade.domain.manga.interactor.GetMangaWithChapters
|
||||
import eu.kanade.domain.manga.interactor.ResetViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
@ -71,6 +72,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetMangaById(get()) }
|
||||
addFactory { GetNextChapter(get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { UpdateManga(get()) }
|
||||
addFactory { MoveMangaToCategories(get()) }
|
||||
|
||||
|
@ -20,4 +20,8 @@ class GetMangaWithChapters(
|
||||
Pair(manga, chapters)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitManga(id: Long): Manga {
|
||||
return mangaRepository.getMangaById(id)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,95 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetMangaChapterFlags(private val mangaRepository: MangaRepository) {
|
||||
|
||||
suspend fun awaitSetDownloadedFilter(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DOWNLOADED_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetUnreadFilter(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_UNREAD_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetBookmarkFilter(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_BOOKMARKED_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetDisplayMode(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DISPLAY_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetSortingModeOrFlipOrder(manga: Manga, flag: Long): Boolean {
|
||||
val newFlags = manga.chapterFlags.let {
|
||||
if (manga.sorting == flag) {
|
||||
// Just flip the order
|
||||
val orderFlag = if (manga.sortDescending()) {
|
||||
Manga.CHAPTER_SORT_ASC
|
||||
} else {
|
||||
Manga.CHAPTER_SORT_DESC
|
||||
}
|
||||
it.setFlag(orderFlag, Manga.CHAPTER_SORT_DIR_MASK)
|
||||
} else {
|
||||
// Set new flag with ascending order
|
||||
it
|
||||
.setFlag(flag, Manga.CHAPTER_SORTING_MASK)
|
||||
.setFlag(Manga.CHAPTER_SORT_ASC, Manga.CHAPTER_SORT_DIR_MASK)
|
||||
}
|
||||
}
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = newFlags,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetAllFlags(
|
||||
mangaId: Long,
|
||||
unreadFilter: Long,
|
||||
downloadedFilter: Long,
|
||||
bookmarkedFilter: Long,
|
||||
sortingMode: Long,
|
||||
sortingDirection: Long,
|
||||
displayMode: Long,
|
||||
): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = mangaId,
|
||||
chapterFlags = 0L.setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK)
|
||||
.setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK)
|
||||
.setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK)
|
||||
.setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK)
|
||||
.setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK)
|
||||
.setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Long.setFlag(flag: Long, mask: Long): Long {
|
||||
return this and mask.inv() or (flag and mask)
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class UpdateManga(
|
||||
@ -22,7 +24,7 @@ class UpdateManga(
|
||||
localManga: Manga,
|
||||
remoteManga: MangaInfo,
|
||||
manualFetch: Boolean,
|
||||
coverCache: CoverCache,
|
||||
coverCache: CoverCache = Injekt.get(),
|
||||
): Boolean {
|
||||
// if the manga isn't a favorite, set its title from source and update in db
|
||||
val title = if (!localManga.favorite) remoteManga.title else null
|
||||
@ -66,4 +68,14 @@ class UpdateManga(
|
||||
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Date().time))
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
||||
val dateAdded = when (favorite) {
|
||||
true -> Date().time
|
||||
false -> 0
|
||||
}
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ interface MangaRepository {
|
||||
|
||||
suspend fun subscribeMangaById(id: Long): Flow<Manga>
|
||||
|
||||
suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga>
|
||||
|
||||
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
|
||||
|
||||
suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga?
|
||||
|
101
app/src/main/java/eu/kanade/presentation/components/Button.kt
Normal file
101
app/src/main/java/eu/kanade/presentation/components/Button.kt
Normal file
@ -0,0 +1,101 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ButtonElevation
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun TextButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
elevation: ButtonElevation? = null,
|
||||
shape: Shape = Shapes.Full,
|
||||
border: BorderStroke? = null,
|
||||
colors: ButtonColors = ButtonDefaults.textButtonColors(),
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) =
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
elevation = elevation,
|
||||
shape = shape,
|
||||
border = border,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
content = content,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Button(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
||||
shape: Shape = Shapes.Full,
|
||||
border: BorderStroke? = null,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val containerColor = colors.containerColor(enabled).value
|
||||
val contentColor = colors.contentColor(enabled).value
|
||||
val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
|
||||
val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
onLongClick = onLongClick,
|
||||
shape = shape,
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
border = border,
|
||||
interactionSource = interactionSource,
|
||||
enabled = enabled,
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
|
||||
Row(
|
||||
Modifier.defaultMinSize(
|
||||
minWidth = ButtonDefaults.MinWidth,
|
||||
minHeight = ButtonDefaults.MinHeight,
|
||||
)
|
||||
.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.FloatingActionButtonElevation
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ExtendedFloatingActionButton(
|
||||
text: @Composable () -> Unit,
|
||||
icon: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
expanded: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = MaterialTheme.shapes.large,
|
||||
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||
) {
|
||||
val minWidth by animateDpAsState(if (expanded) ExtendedFabMinimumWidth else FabContainerWidth)
|
||||
FloatingActionButton(
|
||||
modifier = modifier.sizeIn(minWidth = minWidth),
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
elevation = elevation,
|
||||
) {
|
||||
val startPadding by animateDpAsState(if (expanded) ExtendedFabIconSize / 2 else 0.dp)
|
||||
val endPadding by animateDpAsState(if (expanded) ExtendedFabTextPadding else 0.dp)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(start = startPadding, end = endPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
icon()
|
||||
AnimatedVisibility(
|
||||
visible = expanded,
|
||||
enter = ExtendedFabExpandAnimation,
|
||||
exit = ExtendedFabCollapseAnimation,
|
||||
) {
|
||||
Row {
|
||||
Spacer(Modifier.width(ExtendedFabIconPadding))
|
||||
text()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val EasingLinearCubicBezier = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f)
|
||||
private val EasingEmphasizedCubicBezier = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f)
|
||||
|
||||
private val ExtendedFabMinimumWidth = 80.dp
|
||||
private val ExtendedFabIconSize = 24.0.dp
|
||||
private val ExtendedFabIconPadding = 12.dp
|
||||
private val ExtendedFabTextPadding = 20.dp
|
||||
|
||||
private val ExtendedFabCollapseAnimation = fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = 100,
|
||||
easing = EasingLinearCubicBezier,
|
||||
),
|
||||
) + shrinkHorizontally(
|
||||
animationSpec = tween(
|
||||
durationMillis = 500,
|
||||
easing = EasingEmphasizedCubicBezier,
|
||||
),
|
||||
shrinkTowards = Alignment.Start,
|
||||
)
|
||||
|
||||
private val ExtendedFabExpandAnimation = fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 100,
|
||||
easing = EasingLinearCubicBezier,
|
||||
),
|
||||
) + expandHorizontally(
|
||||
animationSpec = tween(
|
||||
durationMillis = 500,
|
||||
easing = EasingEmphasizedCubicBezier,
|
||||
),
|
||||
expandFrom = Alignment.Start,
|
||||
)
|
||||
|
||||
private val FabContainerWidth = 56.0.dp
|
108
app/src/main/java/eu/kanade/presentation/components/Surface.kt
Normal file
108
app/src/main/java/eu/kanade/presentation/components/Surface.kt
Normal file
@ -0,0 +1,108 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.minimumTouchTargetSize
|
||||
import kotlin.math.ln
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun Surface(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
shape: Shape = Shapes.None,
|
||||
color: Color = MaterialTheme.colorScheme.surface,
|
||||
contentColor: Color = contentColorFor(color),
|
||||
tonalElevation: Dp = 0.dp,
|
||||
shadowElevation: Dp = 0.dp,
|
||||
border: BorderStroke? = null,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides contentColor,
|
||||
LocalAbsoluteTonalElevation provides absoluteElevation,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.minimumTouchTargetSize()
|
||||
.surface(
|
||||
shape = shape,
|
||||
backgroundColor = surfaceColorAtElevation(
|
||||
color = color,
|
||||
elevation = absoluteElevation,
|
||||
),
|
||||
border = border,
|
||||
shadowElevation = shadowElevation,
|
||||
)
|
||||
.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
),
|
||||
propagateMinConstraints = true,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.surface(
|
||||
shape: Shape,
|
||||
backgroundColor: Color,
|
||||
border: BorderStroke?,
|
||||
shadowElevation: Dp,
|
||||
) = this
|
||||
.shadow(shadowElevation, shape, clip = false)
|
||||
.then(if (border != null) Modifier.border(border, shape) else Modifier)
|
||||
.background(color = backgroundColor, shape = shape)
|
||||
.clip(shape)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
|
||||
return if (color == MaterialTheme.colorScheme.surface) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
} else {
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
private fun ColorScheme.surfaceColorAtElevation(
|
||||
elevation: Dp,
|
||||
): Color {
|
||||
if (elevation == 0.dp) return surface
|
||||
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
|
||||
return surfaceTint.copy(alpha = alpha).compositeOver(surface)
|
||||
}
|
803
app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
Normal file
803
app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
Normal file
@ -0,0 +1,803 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberTopAppBarScrollState
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.manga.model.Manga.Companion.CHAPTER_DISPLAY_NUMBER
|
||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||
import eu.kanade.presentation.components.VerticalFastScroller
|
||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
||||
import eu.kanade.presentation.manga.components.MangaBottomActionMenu
|
||||
import eu.kanade.presentation.manga.components.MangaChapterListItem
|
||||
import eu.kanade.presentation.manga.components.MangaInfoHeader
|
||||
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
||||
import eu.kanade.presentation.manga.components.MangaTopAppBar
|
||||
import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
|
||||
import eu.kanade.presentation.util.isScrolledToEnd
|
||||
import eu.kanade.presentation.util.isScrollingUp
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Date
|
||||
|
||||
private val chapterDecimalFormat = DecimalFormat(
|
||||
"#.###",
|
||||
DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' },
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
state: MangaScreenState.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
onBackClicked: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onContinueReading: () -> Unit,
|
||||
onSearch: (query: String, global: Boolean) -> Unit,
|
||||
|
||||
// For cover dialog
|
||||
onCoverClicked: () -> Unit,
|
||||
|
||||
// For top action menu
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
|
||||
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
|
||||
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
||||
) {
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
MangaScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onRefresh = onRefresh,
|
||||
onContinueReading = onContinueReading,
|
||||
onSearch = onSearch,
|
||||
onCoverClicked = onCoverClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
)
|
||||
} else {
|
||||
MangaScreenLargeImpl(
|
||||
state = state,
|
||||
windowWidthSizeClass = windowWidthSizeClass,
|
||||
snackbarHostState = snackbarHostState,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onRefresh = onRefresh,
|
||||
onContinueReading = onContinueReading,
|
||||
onSearch = onSearch,
|
||||
onCoverClicked = onCoverClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaScreenSmallImpl(
|
||||
state: MangaScreenState.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onBackClicked: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onContinueReading: () -> Unit,
|
||||
onSearch: (query: String, global: Boolean) -> Unit,
|
||||
|
||||
// For cover dialog
|
||||
onCoverClicked: () -> Unit,
|
||||
|
||||
// For top action menu
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
|
||||
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
|
||||
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||
val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
|
||||
val chapterListState = rememberLazyListState()
|
||||
SideEffect {
|
||||
if (chapterListState.firstVisibleItemIndex > 0 || chapterListState.firstVisibleItemScrollOffset > 0) {
|
||||
// Should go here after a configuration change
|
||||
// Safe to say that the app bar is fully scrolled
|
||||
scrollBehavior.state.offset = scrollBehavior.state.offsetLimit
|
||||
}
|
||||
}
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) }
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
|
||||
onRefresh = onRefresh,
|
||||
indicatorPadding = PaddingValues(
|
||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||
top = with(LocalDensity.current) { topBarHeight.toDp() },
|
||||
end = insetPadding.calculateEndPadding(layoutDirection),
|
||||
),
|
||||
indicator = { s, trigger ->
|
||||
SwipeRefreshIndicator(
|
||||
state = s,
|
||||
refreshTriggerDistance = trigger,
|
||||
)
|
||||
},
|
||||
) {
|
||||
val chapters = remember(state) { state.processedChapters.toList() }
|
||||
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
|
||||
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.clear()
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(insetPadding),
|
||||
topBar = {
|
||||
MangaTopAppBar(
|
||||
modifier = Modifier
|
||||
.scrollable(
|
||||
state = rememberScrollableState {
|
||||
var consumed = runBlocking { chapterListState.scrollBy(-it) } * -1
|
||||
if (consumed == 0f) {
|
||||
// Pass scroll to app bar if we're on the top of the list
|
||||
val newOffset =
|
||||
(scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f)
|
||||
consumed = newOffset - scrollBehavior.state.offset
|
||||
scrollBehavior.state.offset = newOffset
|
||||
}
|
||||
consumed
|
||||
},
|
||||
orientation = Orientation.Vertical,
|
||||
interactionSource = chapterListState.interactionSource as MutableInteractionSource,
|
||||
),
|
||||
title = state.manga.title,
|
||||
author = state.manga.author,
|
||||
artist = state.manga.artist,
|
||||
description = state.manga.description,
|
||||
tagsProvider = { state.manga.genre },
|
||||
coverDataProvider = { state.manga },
|
||||
sourceName = remember { state.source.getNameForMangaInfo() },
|
||||
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||
favorite = state.manga.favorite,
|
||||
status = state.manga.status,
|
||||
trackingCount = state.trackingCount,
|
||||
chapterCount = chapters.size,
|
||||
chapterFiltered = state.manga.chaptersFiltered(),
|
||||
incognitoMode = state.isIncognitoMode,
|
||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||
fromSource = state.isFromSource,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
onCoverClick = onCoverClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
doGlobalSearch = onSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
actionModeCounter = selected.size,
|
||||
onSelectAll = {
|
||||
selected.clear()
|
||||
selected.addAll(chapters)
|
||||
},
|
||||
onInvertSelection = {
|
||||
val toSelect = chapters - selected
|
||||
selected.clear()
|
||||
selected.addAll(toSelect)
|
||||
},
|
||||
onSmallAppBarHeightChanged = onTopBarHeightChanged,
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
MangaBottomActionMenu(
|
||||
visible = selected.isNotEmpty(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.chapter.bookmark } },
|
||||
onRemoveBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.all { it.chapter.bookmark } },
|
||||
onMarkAsReadClicked = {
|
||||
onMultiMarkAsReadClicked(selected.map { it.chapter }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.chapter.read } },
|
||||
onMarkAsUnreadClicked = {
|
||||
onMultiMarkAsReadClicked(selected.map { it.chapter }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { it.chapter.read } },
|
||||
onMarkPreviousAsReadClicked = {
|
||||
onMarkPreviousAsReadClicked(selected[0].chapter)
|
||||
selected.clear()
|
||||
}.takeIf { selected.size == 1 },
|
||||
onDownloadClicked = {
|
||||
onDownloadChapter!!(selected, ChapterDownloadAction.START)
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
onDownloadChapter != null && selected.any { it.downloadState != Download.State.DOWNLOADED }
|
||||
},
|
||||
onDeleteClicked = {
|
||||
onMultiDeleteClicked(selected.map { it.chapter })
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
onDownloadChapter != null && selected.any { it.downloadState == Download.State.DOWNLOADED }
|
||||
},
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (chapters.any { it.chapter.read }) {
|
||||
R.string.action_resume
|
||||
} else {
|
||||
R.string.action_start
|
||||
}
|
||||
Text(text = stringResource(id = id))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
||||
modifier = Modifier
|
||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
val withNavBarContentPadding = contentPadding +
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||
VerticalFastScroller(
|
||||
listState = chapterListState,
|
||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
state = chapterListState,
|
||||
contentPadding = withNavBarContentPadding,
|
||||
) {
|
||||
items(items = chapters) { chapterItem ->
|
||||
val (chapter, downloadState, downloadProgress) = chapterItem
|
||||
val chapterTitle = remember(state.manga.displayMode, chapter.chapterNumber, chapter.name) {
|
||||
if (state.manga.displayMode == CHAPTER_DISPLAY_NUMBER) {
|
||||
chapterDecimalFormat.format(chapter.chapterNumber.toDouble())
|
||||
} else {
|
||||
chapter.name
|
||||
}
|
||||
}
|
||||
val date = remember(chapter.dateUpload) {
|
||||
chapter.dateUpload
|
||||
.takeIf { it > 0 }
|
||||
?.let { Date(it).toRelativeString(context, state.dateRelativeTime, state.dateFormat) }
|
||||
}
|
||||
val lastPageRead = remember(chapter.lastPageRead) {
|
||||
chapter.lastPageRead.takeIf { !chapter.read && it > 0 }
|
||||
}
|
||||
val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
|
||||
|
||||
MangaChapterListItem(
|
||||
title = chapterTitle,
|
||||
date = date,
|
||||
readProgress = lastPageRead?.let { stringResource(id = R.string.chapter_progress, it + 1) },
|
||||
scanlator = scanlator,
|
||||
read = chapter.read,
|
||||
bookmark = chapter.bookmark,
|
||||
selected = selected.contains(chapterItem),
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
onLongClick = {
|
||||
val dispatched = onChapterItemLongClick(
|
||||
chapterItem = chapterItem,
|
||||
selected = selected,
|
||||
chapters = chapters,
|
||||
selectedPositions = selectedPositions,
|
||||
)
|
||||
if (dispatched) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = chapterItem,
|
||||
selected = selected,
|
||||
chapters = chapters,
|
||||
selectedPositions = selectedPositions,
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
onDownloadClick = if (onDownloadChapter != null) {
|
||||
{ onDownloadChapter(listOf(chapterItem), it) }
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaScreenLargeImpl(
|
||||
state: MangaScreenState.Success,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onBackClicked: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onContinueReading: () -> Unit,
|
||||
onSearch: (query: String, global: Boolean) -> Unit,
|
||||
|
||||
// For cover dialog
|
||||
onCoverClicked: () -> Unit,
|
||||
|
||||
// For top action menu
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
|
||||
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
|
||||
onMultiDeleteClicked: (List<Chapter>) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val density = LocalDensity.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
|
||||
onRefresh = onRefresh,
|
||||
indicatorPadding = PaddingValues(
|
||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||
top = with(density) { topBarHeight.toDp() },
|
||||
end = insetPadding.calculateEndPadding(layoutDirection),
|
||||
),
|
||||
clipIndicatorToPadding = true,
|
||||
indicator = { s, trigger ->
|
||||
SwipeRefreshIndicator(
|
||||
state = s,
|
||||
refreshTriggerDistance = trigger,
|
||||
)
|
||||
},
|
||||
) {
|
||||
val chapterListState = rememberLazyListState()
|
||||
val chapters = remember(state) { state.processedChapters.toList() }
|
||||
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
|
||||
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.clear()
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.padding(insetPadding),
|
||||
topBar = {
|
||||
MangaSmallAppBar(
|
||||
modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
|
||||
title = state.manga.title,
|
||||
titleAlphaProvider = { if (selected.isEmpty()) 0f else 1f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
incognitoMode = state.isIncognitoMode,
|
||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
actionModeCounter = selected.size,
|
||||
onSelectAll = {
|
||||
selected.clear()
|
||||
selected.addAll(chapters)
|
||||
},
|
||||
onInvertSelection = {
|
||||
val toSelect = chapters - selected
|
||||
selected.clear()
|
||||
selected.addAll(toSelect)
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
MangaBottomActionMenu(
|
||||
visible = selected.isNotEmpty(),
|
||||
modifier = Modifier.fillMaxWidth(0.5f),
|
||||
onBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.chapter.bookmark } },
|
||||
onRemoveBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.all { it.chapter.bookmark } },
|
||||
onMarkAsReadClicked = {
|
||||
onMultiMarkAsReadClicked(selected.map { it.chapter }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.chapter.read } },
|
||||
onMarkAsUnreadClicked = {
|
||||
onMultiMarkAsReadClicked(selected.map { it.chapter }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { it.chapter.read } },
|
||||
onMarkPreviousAsReadClicked = {
|
||||
onMarkPreviousAsReadClicked(selected[0].chapter)
|
||||
selected.clear()
|
||||
}.takeIf { selected.size == 1 },
|
||||
onDownloadClicked = {
|
||||
onDownloadChapter!!(selected, ChapterDownloadAction.START)
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
onDownloadChapter != null && selected.any { it.downloadState != Download.State.DOWNLOADED }
|
||||
},
|
||||
onDeleteClicked = {
|
||||
onMultiDeleteClicked(selected.map { it.chapter })
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
onDownloadChapter != null && selected.any { it.downloadState == Download.State.DOWNLOADED }
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (chapters.any { it.chapter.read }) {
|
||||
R.string.action_resume
|
||||
} else {
|
||||
R.string.action_start
|
||||
}
|
||||
Text(text = stringResource(id = id))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
|
||||
modifier = Modifier
|
||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
Row {
|
||||
val withNavBarContentPadding = contentPadding +
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||
MangaInfoHeader(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
|
||||
windowWidthSizeClass = WindowWidthSizeClass.Expanded,
|
||||
appBarPadding = contentPadding.calculateTopPadding(),
|
||||
title = state.manga.title,
|
||||
author = state.manga.author,
|
||||
artist = state.manga.artist,
|
||||
description = state.manga.description,
|
||||
tagsProvider = { state.manga.genre },
|
||||
sourceName = remember { state.source.getNameForMangaInfo() },
|
||||
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||
coverDataProvider = { state.manga },
|
||||
favorite = state.manga.favorite,
|
||||
status = state.manga.status,
|
||||
trackingCount = state.trackingCount,
|
||||
fromSource = state.isFromSource,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClicked,
|
||||
doSearch = onSearch,
|
||||
)
|
||||
|
||||
val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
|
||||
VerticalFastScroller(
|
||||
listState = chapterListState,
|
||||
modifier = Modifier.weight(chaptersWeight),
|
||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(layoutDirection),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
state = chapterListState,
|
||||
contentPadding = withNavBarContentPadding,
|
||||
) {
|
||||
item(contentType = "header") {
|
||||
ChapterHeader(
|
||||
chapterCount = chapters.size,
|
||||
isChapterFiltered = state.manga.chaptersFiltered(),
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
)
|
||||
}
|
||||
|
||||
items(items = chapters) { chapterItem ->
|
||||
val (chapter, downloadState, downloadProgress) = chapterItem
|
||||
val chapterTitle = remember(state.manga.displayMode, chapter.chapterNumber, chapter.name) {
|
||||
if (state.manga.displayMode == CHAPTER_DISPLAY_NUMBER) {
|
||||
chapterDecimalFormat.format(chapter.chapterNumber.toDouble())
|
||||
} else {
|
||||
chapter.name
|
||||
}
|
||||
}
|
||||
val date = remember(chapter.dateUpload) {
|
||||
chapter.dateUpload
|
||||
.takeIf { it > 0 }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
state.dateRelativeTime,
|
||||
state.dateFormat,
|
||||
)
|
||||
}
|
||||
}
|
||||
val lastPageRead = remember(chapter.lastPageRead) {
|
||||
chapter.lastPageRead.takeIf { !chapter.read && it > 0 }
|
||||
}
|
||||
val scanlator =
|
||||
remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
|
||||
|
||||
MangaChapterListItem(
|
||||
title = chapterTitle,
|
||||
date = date,
|
||||
readProgress = lastPageRead?.let {
|
||||
stringResource(
|
||||
id = R.string.chapter_progress,
|
||||
it + 1,
|
||||
)
|
||||
},
|
||||
scanlator = scanlator,
|
||||
read = chapter.read,
|
||||
bookmark = chapter.bookmark,
|
||||
selected = selected.contains(chapterItem),
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
onLongClick = {
|
||||
val dispatched = onChapterItemLongClick(
|
||||
chapterItem = chapterItem,
|
||||
selected = selected,
|
||||
chapters = chapters,
|
||||
selectedPositions = selectedPositions,
|
||||
)
|
||||
if (dispatched) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = chapterItem,
|
||||
selected = selected,
|
||||
chapters = chapters,
|
||||
selectedPositions = selectedPositions,
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
onDownloadClick = if (onDownloadChapter != null) {
|
||||
{ onDownloadChapter(listOf(chapterItem), it) }
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChapterItemLongClick(
|
||||
chapterItem: ChapterItem,
|
||||
selected: MutableList<ChapterItem>,
|
||||
chapters: List<ChapterItem>,
|
||||
selectedPositions: Array<Int>,
|
||||
): Boolean {
|
||||
if (!selected.contains(chapterItem)) {
|
||||
val selectedIndex = chapters.indexOf(chapterItem)
|
||||
if (selected.isEmpty()) {
|
||||
selected.add(chapterItem)
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
return true
|
||||
}
|
||||
|
||||
// Try to select the items in-between when possible
|
||||
val range: IntRange
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
range = selectedIndex until selectedPositions[0]
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
range = (selectedPositions[1] + 1)..selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
} else {
|
||||
// Just select itself
|
||||
range = selectedIndex..selectedIndex
|
||||
}
|
||||
|
||||
range.forEach {
|
||||
val toAdd = chapters[it]
|
||||
if (!selected.contains(toAdd)) {
|
||||
selected.add(toAdd)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onChapterItemClick(
|
||||
chapterItem: ChapterItem,
|
||||
selected: MutableList<ChapterItem>,
|
||||
chapters: List<ChapterItem>,
|
||||
selectedPositions: Array<Int>,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
) {
|
||||
val selectedIndex = chapters.indexOf(chapterItem)
|
||||
when {
|
||||
selected.contains(chapterItem) -> {
|
||||
val removedIndex = chapters.indexOf(chapterItem)
|
||||
selected.remove(chapterItem)
|
||||
|
||||
if (removedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = chapters.indexOfFirst { selected.contains(it) }
|
||||
} else if (removedIndex == selectedPositions[1]) {
|
||||
selectedPositions[1] = chapters.indexOfLast { selected.contains(it) }
|
||||
}
|
||||
}
|
||||
selected.isNotEmpty() -> {
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
selectedPositions[1] = selectedIndex
|
||||
}
|
||||
selected.add(chapterItem)
|
||||
}
|
||||
else -> onChapterClicked(chapterItem.chapter)
|
||||
}
|
||||
}
|
@ -1,8 +1,12 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
enum class EditCoverAction {
|
||||
EDIT,
|
||||
DELETE,
|
||||
enum class DownloadAction {
|
||||
NEXT_1_CHAPTER,
|
||||
NEXT_5_CHAPTERS,
|
||||
NEXT_10_CHAPTERS,
|
||||
CUSTOM,
|
||||
UNREAD_CHAPTERS,
|
||||
ALL_CHAPTERS
|
||||
}
|
||||
|
||||
enum class ChapterDownloadAction {
|
||||
@ -11,3 +15,8 @@ enum class ChapterDownloadAction {
|
||||
CANCEL,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
EDIT,
|
||||
DELETE,
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.quantityStringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
@Composable
|
||||
fun ChapterHeader(
|
||||
chapterCount: Int?,
|
||||
isChapterFiltered: Boolean,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = if (chapterCount == null) {
|
||||
stringResource(id = R.string.chapters)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.manga_num_chapters, quantity = chapterCount)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
IconButton(onClick = onFilterButtonClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterList,
|
||||
contentDescription = stringResource(id = R.string.action_filter),
|
||||
tint = if (isChapterFiltered) {
|
||||
Color(LocalContext.current.getResourceColor(R.attr.colorFilterActive))
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun DotSeparatorText() {
|
||||
Text(text = " • ")
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BookmarkAdd
|
||||
import androidx.compose.material.icons.filled.BookmarkRemove
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.RemoveDone
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MangaBottomActionMenu(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBookmarkClicked: (() -> Unit)?,
|
||||
onRemoveBookmarkClicked: (() -> Unit)?,
|
||||
onMarkAsReadClicked: (() -> Unit)?,
|
||||
onMarkAsUnreadClicked: (() -> Unit)?,
|
||||
onMarkPreviousAsReadClicked: (() -> Unit)?,
|
||||
onDownloadClicked: (() -> Unit)?,
|
||||
onDeleteClicked: (() -> Unit)?,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
tonalElevation = 3.dp,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0 until 7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1000)
|
||||
if (isActive) confirm[toConfirmIndex] = false
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||
) {
|
||||
if (onBookmarkClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_bookmark),
|
||||
icon = Icons.Default.BookmarkAdd,
|
||||
toConfirm = confirm[0],
|
||||
onLongClick = { onLongClickItem(0) },
|
||||
onClick = onBookmarkClicked,
|
||||
)
|
||||
}
|
||||
if (onRemoveBookmarkClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_remove_bookmark),
|
||||
icon = Icons.Default.BookmarkRemove,
|
||||
toConfirm = confirm[1],
|
||||
onLongClick = { onLongClickItem(1) },
|
||||
onClick = onRemoveBookmarkClicked,
|
||||
)
|
||||
}
|
||||
if (onMarkAsReadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_mark_as_read),
|
||||
icon = Icons.Default.DoneAll,
|
||||
toConfirm = confirm[2],
|
||||
onLongClick = { onLongClickItem(2) },
|
||||
onClick = onMarkAsReadClicked,
|
||||
)
|
||||
}
|
||||
if (onMarkAsUnreadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_mark_as_unread),
|
||||
icon = Icons.Default.RemoveDone,
|
||||
toConfirm = confirm[3],
|
||||
onLongClick = { onLongClickItem(3) },
|
||||
onClick = onMarkAsUnreadClicked,
|
||||
)
|
||||
}
|
||||
if (onMarkPreviousAsReadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_mark_previous_as_read),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp),
|
||||
toConfirm = confirm[4],
|
||||
onLongClick = { onLongClickItem(4) },
|
||||
onClick = onMarkPreviousAsReadClicked,
|
||||
)
|
||||
}
|
||||
if (onDownloadClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_download),
|
||||
icon = Icons.Default.Download,
|
||||
toConfirm = confirm[5],
|
||||
onLongClick = { onLongClickItem(5) },
|
||||
onClick = onDownloadClicked,
|
||||
)
|
||||
}
|
||||
if (onDeleteClicked != null) {
|
||||
Button(
|
||||
title = stringResource(id = R.string.action_delete),
|
||||
icon = Icons.Default.Delete,
|
||||
toConfirm = confirm[6],
|
||||
onLongClick = { onLongClickItem(6) },
|
||||
onClick = onDeleteClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Button(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
toConfirm: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.weight(animatedWeight)
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = toConfirm,
|
||||
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
@Composable
|
||||
fun MangaChapterListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
date: String?,
|
||||
readProgress: String?,
|
||||
scanlator: String?,
|
||||
read: Boolean,
|
||||
bookmark: Boolean,
|
||||
selected: Boolean,
|
||||
downloadState: Download.State,
|
||||
downloadProgress: Int,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (read) ReadItemAlpha else 1f),
|
||||
) {
|
||||
val textColor = if (bookmark) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bookmark,
|
||||
contentDescription = stringResource(id = R.string.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = textColor,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
.copy(color = textColor),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row {
|
||||
ProvideTextStyle(
|
||||
value = MaterialTheme.typography.bodyMedium
|
||||
.copy(color = textColor, fontSize = 12.sp),
|
||||
) {
|
||||
if (date != null) {
|
||||
Text(
|
||||
text = date,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
if (scanlator != null) {
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download view
|
||||
if (onDownloadClick != null) {
|
||||
ChapterDownloadIndicator(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ReadItemAlpha = .38f
|
@ -0,0 +1,616 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachMoney
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.SuggestionChipDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.components.TextButton
|
||||
import eu.kanade.presentation.util.clickableNoIndication
|
||||
import eu.kanade.presentation.util.quantityStringResource
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun MangaInfoHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
appBarPadding: Dp,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
coverDataProvider: () -> Manga,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
fromSource: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
Box {
|
||||
// Backdrop
|
||||
val backdropGradientColors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background,
|
||||
)
|
||||
AsyncImage(
|
||||
model = coverDataProvider(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(colors = backdropGradientColors),
|
||||
)
|
||||
}
|
||||
.alpha(.2f),
|
||||
)
|
||||
|
||||
// Manga & source info
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
MangaAndSourceTitlesSmall(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
} else {
|
||||
MangaAndSourceTitlesLarge(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(id = R.string.in_library)
|
||||
} else {
|
||||
stringResource(id = R.string.add_to_library)
|
||||
},
|
||||
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onTrackingClicked != null) {
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(id = R.string.manga_tracking_tab)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
|
||||
},
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
}
|
||||
if (onWebViewClicked != null) {
|
||||
MangaActionButton(
|
||||
title = stringResource(id = R.string.action_web_view),
|
||||
icon = Icons.Default.Public,
|
||||
color = defaultActionButtonColor,
|
||||
onClick = onWebViewClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Expandable description-tags
|
||||
Column {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
|
||||
}
|
||||
if (!description.isNullOrBlank()) {
|
||||
val trimmedDescription = remember(description) {
|
||||
description
|
||||
.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
|
||||
.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
}
|
||||
MangaSummary(
|
||||
expandedDescription = description,
|
||||
shrunkDescription = trimmedDescription,
|
||||
expanded = expanded,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(description, description) },
|
||||
onClick = { onExpanded(!expanded) },
|
||||
),
|
||||
)
|
||||
}
|
||||
val tags = tagsProvider()
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(vertical = 12.dp)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
mainAxisSpacing = 4.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaAndSourceTitlesLarge(
|
||||
appBarPadding: Dp,
|
||||
coverDataProvider: () -> Manga,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.fillMaxWidth(0.4f),
|
||||
data = coverDataProvider(),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title.takeIf { it.isNotBlank() } ?: stringResource(id = R.string.unknown),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() } ?: stringResource(id = R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (!artist.isNullOrBlank()) {
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(id = R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(id = R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(id = R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(id = R.string.publishing_finished)
|
||||
SManga.CANCELLED.toLong() -> stringResource(id = R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(id = R.string.on_hiatus)
|
||||
else -> stringResource(id = R.string.unknown)
|
||||
},
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaAndSourceTitlesSmall(
|
||||
appBarPadding: Dp,
|
||||
coverDataProvider: () -> Manga,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.sizeIn(maxWidth = 100.dp),
|
||||
data = coverDataProvider(),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Column(modifier = Modifier.padding(start = 16.dp)) {
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(id = R.string.unknown) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() } ?: stringResource(id = R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
)
|
||||
if (!artist.isNullOrBlank()) {
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(id = R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(id = R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(id = R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(id = R.string.publishing_finished)
|
||||
SManga.CANCELLED.toLong() -> stringResource(id = R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(id = R.string.on_hiatus)
|
||||
else -> stringResource(id = R.string.unknown)
|
||||
},
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
expandedDescription: String,
|
||||
shrunkDescription: String,
|
||||
expanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expandedHeight by remember { mutableStateOf(0) }
|
||||
var shrunkHeight by remember { mutableStateOf(0) }
|
||||
val heightDelta = remember(expandedHeight, shrunkHeight) { expandedHeight - shrunkHeight }
|
||||
val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
|
||||
val scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } }
|
||||
|
||||
SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
|
||||
val shrunkPlaceable = subcompose("description-s") {
|
||||
Text(
|
||||
text = "\n\n", // Shows at least 3 lines
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}.map { it.measure(constraints) }
|
||||
shrunkHeight = shrunkPlaceable.maxByOrNull { it.height }?.height ?: 0
|
||||
|
||||
val expandedPlaceable = subcompose("description-l") {
|
||||
Text(
|
||||
text = expandedDescription,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}.map { it.measure(constraints) }
|
||||
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
|
||||
|
||||
val actualPlaceable = subcompose("description") {
|
||||
Text(
|
||||
text = if (expanded) expandedDescription else shrunkDescription,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
)
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val scrimPlaceable = subcompose("scrim") {
|
||||
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
|
||||
Box(
|
||||
modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(image, !expanded),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
|
||||
)
|
||||
}
|
||||
}.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
|
||||
|
||||
val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
|
||||
layout(constraints.maxWidth, currentHeight) {
|
||||
actualPlaceable.forEach {
|
||||
it.place(0, 0)
|
||||
}
|
||||
|
||||
val scrimY = currentHeight - scrimHeight
|
||||
scrimPlaceable.forEach {
|
||||
it.place(0, scrimY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TagsChip(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||
SuggestionChip(
|
||||
onClick = onClick,
|
||||
label = { Text(text = text, style = MaterialTheme.typography.bodySmall) },
|
||||
border = null,
|
||||
colors = SuggestionChipDefaults.suggestionChipColors(
|
||||
containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
labelColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.MangaActionButton(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
color: Color,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = title,
|
||||
color = color,
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.FlipToBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun MangaSmallAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
val isActionMode = actionModeCounter > 0
|
||||
val backgroundAlpha = if (isActionMode) 1f else backgroundAlphaProvider()
|
||||
val backgroundColor by TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f)
|
||||
Column(
|
||||
modifier = modifier.drawBehind {
|
||||
drawRect(backgroundColor.copy(alpha = backgroundAlpha))
|
||||
},
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
|
||||
title = {
|
||||
Text(
|
||||
text = if (isActionMode) actionModeCounter.toString() else title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(titleAlphaProvider()),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
Icon(
|
||||
imageVector = if (isActionMode) Icons.Default.Close else Icons.Default.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.abc_action_bar_up_description),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(id = R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipToBack,
|
||||
contentDescription = stringResource(id = R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (onShareClicked != null) {
|
||||
IconButton(onClick = onShareClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = stringResource(id = R.string.action_share),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onDownloadClicked != null) {
|
||||
val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { onDownloadExpanded(!downloadExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = stringResource(id = R.string.manga_download),
|
||||
)
|
||||
}
|
||||
val onDismissRequest = { onDownloadExpanded(false) }
|
||||
DropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_1)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_1_CHAPTER)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_5)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_5_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_10)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_10_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_custom)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.CUSTOM)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_unread)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.UNREAD_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.download_all)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.ALL_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onEditCategoryClicked != null && onMigrateClicked != null) {
|
||||
val (moreExpanded, onMoreExpanded) = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.abc_action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
val onDismissRequest = { onMoreExpanded(false) }
|
||||
DropdownMenu(
|
||||
expanded = moreExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.action_edit_categories)) },
|
||||
onClick = {
|
||||
onEditCategoryClicked()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.action_migrate)) },
|
||||
onClick = {
|
||||
onMigrateClicked()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Background handled by parent
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.label_downloaded_only),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.tertiary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
if (incognitoMode) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.pref_incognito_mode),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun MangaTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
coverDataProvider: () -> Manga,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
chapterCount: Int?,
|
||||
chapterFiltered: Boolean,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
fromSource: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onCoverClick: () -> Unit,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
onSmallAppBarHeightChanged: (Int) -> Unit,
|
||||
) {
|
||||
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
|
||||
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
val (smallHeightPx, onSmallHeightPxChanged) = remember { mutableStateOf(0) }
|
||||
Column(modifier = Modifier.layoutId("mangaInfo")) {
|
||||
MangaInfoHeader(
|
||||
windowWidthSizeClass = WindowWidthSizeClass.Compact,
|
||||
appBarPadding = with(LocalDensity.current) { smallHeightPx.toDp() },
|
||||
title = title,
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
tagsProvider = tagsProvider,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
coverDataProvider = coverDataProvider,
|
||||
favorite = favorite,
|
||||
status = status,
|
||||
trackingCount = trackingCount,
|
||||
fromSource = fromSource,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClick,
|
||||
doSearch = doGlobalSearch,
|
||||
)
|
||||
ChapterHeader(
|
||||
chapterCount = chapterCount,
|
||||
isChapterFiltered = chapterFiltered,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
)
|
||||
}
|
||||
|
||||
MangaSmallAppBar(
|
||||
modifier = Modifier
|
||||
.layoutId("topBar")
|
||||
.onSizeChanged {
|
||||
onSmallHeightPxChanged(it.height)
|
||||
onSmallAppBarHeightChanged(it.height)
|
||||
},
|
||||
title = title,
|
||||
titleAlphaProvider = { if (actionModeCounter == 0) scrollPercentageProvider() else 1f },
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
onBackClicked = onBackClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
actionModeCounter = actionModeCounter,
|
||||
onSelectAll = onSelectAll,
|
||||
onInvertSelection = onInvertSelection,
|
||||
)
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val mangaInfoPlaceable = measurables
|
||||
.first { it.layoutId == "mangaInfo" }
|
||||
.measure(constraints.copy(maxHeight = Constraints.Infinity))
|
||||
val topBarPlaceable = measurables
|
||||
.first { it.layoutId == "topBar" }
|
||||
.measure(constraints)
|
||||
val mangaInfoHeight = mangaInfoPlaceable.height
|
||||
val topBarHeight = topBarPlaceable.height
|
||||
val mangaInfoSansTopBarHeightPx = mangaInfoHeight - topBarHeight
|
||||
val layoutHeight = topBarHeight +
|
||||
(mangaInfoSansTopBarHeightPx * inverseScrollPercentageProvider()).roundToInt()
|
||||
|
||||
layout(constraints.maxWidth, layoutHeight) {
|
||||
val mangaInfoY = (-mangaInfoSansTopBarHeightPx * scrollPercentageProvider()).roundToInt()
|
||||
mangaInfoPlaceable.place(0, mangaInfoY)
|
||||
topBarPlaceable.place(0, 0)
|
||||
|
||||
// Update offset limit
|
||||
val offsetLimit = -mangaInfoSansTopBarHeightPx.toFloat()
|
||||
if (scrollBehavior?.state?.offsetLimit != offsetLimit) {
|
||||
scrollBehavior?.state?.offsetLimit = offsetLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,29 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
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
|
||||
|
||||
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrollingUp(): Boolean {
|
||||
var previousIndex by remember { mutableStateOf(firstVisibleItemIndex) }
|
||||
var previousScrollOffset by remember { mutableStateOf(firstVisibleItemScrollOffset) }
|
||||
return remember {
|
||||
derivedStateOf {
|
||||
if (previousIndex != firstVisibleItemIndex) {
|
||||
previousIndex > firstVisibleItemIndex
|
||||
} else {
|
||||
previousScrollOffset >= firstVisibleItemScrollOffset
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
}
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.animation.core.AnimationState
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.animateDecay
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.TopAppBarScrollState
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* A [TopAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a top
|
||||
* app bar.
|
||||
*
|
||||
* A top app bar that is set up with this [TopAppBarScrollBehavior] will immediately collapse when
|
||||
* the nested content is pulled up, and will expand back the collapsed area when the content is
|
||||
* pulled all the way down.
|
||||
*
|
||||
* @param decayAnimationSpec a [DecayAnimationSpec] that will be used by the top app bar motion
|
||||
* when the user flings the content. Preferably, this should match the animation spec used by the
|
||||
* scrollable content. See also [androidx.compose.animation.rememberSplineBasedDecay] for a
|
||||
* default [DecayAnimationSpec] that can be used with this behavior.
|
||||
* @param canScroll a callback used to determine whether scroll events are to be
|
||||
* handled by this [ExitUntilCollapsedScrollBehavior]
|
||||
*/
|
||||
class ExitUntilCollapsedScrollBehavior(
|
||||
override val state: TopAppBarScrollState,
|
||||
val decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
val canScroll: () -> Boolean = { true },
|
||||
) : TopAppBarScrollBehavior {
|
||||
override val scrollFraction: Float
|
||||
get() = if (state.offsetLimit != 0f) state.offset / state.offsetLimit else 0f
|
||||
override var nestedScrollConnection =
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
// Don't intercept if scrolling down.
|
||||
if (!canScroll() || available.y > 0f) return Offset.Zero
|
||||
|
||||
val newOffset = (state.offset + available.y)
|
||||
val coerced =
|
||||
newOffset.coerceIn(minimumValue = state.offsetLimit, maximumValue = 0f)
|
||||
return if (newOffset == coerced) {
|
||||
// Nothing coerced, meaning we're in the middle of top app bar collapse or
|
||||
// expand.
|
||||
state.offset = coerced
|
||||
// Consume only the scroll on the Y axis.
|
||||
available.copy(x = 0f)
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
if (!canScroll()) return Offset.Zero
|
||||
state.contentOffset += consumed.y
|
||||
|
||||
if (available.y < 0f || consumed.y < 0f) {
|
||||
// When scrolling up, just update the state's offset.
|
||||
val oldOffset = state.offset
|
||||
state.offset = (state.offset + consumed.y).coerceIn(
|
||||
minimumValue = state.offsetLimit,
|
||||
maximumValue = 0f,
|
||||
)
|
||||
return Offset(0f, state.offset - oldOffset)
|
||||
}
|
||||
|
||||
if (consumed.y == 0f && available.y > 0) {
|
||||
// Reset the total offset to zero when scrolling all the way down. This will
|
||||
// eliminate some float precision inaccuracies.
|
||||
state.contentOffset = 0f
|
||||
}
|
||||
|
||||
if (available.y > 0f) {
|
||||
// Adjust the offset in case the consumed delta Y is less than what was recorded
|
||||
// as available delta Y in the pre-scroll.
|
||||
val oldOffset = state.offset
|
||||
state.offset = (state.offset + available.y).coerceIn(
|
||||
minimumValue = state.offsetLimit,
|
||||
maximumValue = 0f,
|
||||
)
|
||||
return Offset(0f, state.offset - oldOffset)
|
||||
}
|
||||
return Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
val result = super.onPostFling(consumed, available)
|
||||
if ((available.y < 0f && state.contentOffset == 0f) ||
|
||||
(available.y > 0f && state.offset < 0f)
|
||||
) {
|
||||
return result +
|
||||
onTopBarFling(
|
||||
scrollBehavior = this@ExitUntilCollapsedScrollBehavior,
|
||||
initialVelocity = available.y,
|
||||
decayAnimationSpec = decayAnimationSpec,
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tachiyomi: Remove snap behavior
|
||||
*/
|
||||
private suspend fun onTopBarFling(
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
initialVelocity: Float,
|
||||
decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
): Velocity {
|
||||
if (abs(initialVelocity) > 1f) {
|
||||
var remainingVelocity = initialVelocity
|
||||
var lastValue = 0f
|
||||
AnimationState(
|
||||
initialValue = 0f,
|
||||
initialVelocity = initialVelocity,
|
||||
)
|
||||
.animateDecay(decayAnimationSpec) {
|
||||
val delta = value - lastValue
|
||||
val initialOffset = scrollBehavior.state.offset
|
||||
scrollBehavior.state.offset =
|
||||
(initialOffset + delta).coerceIn(
|
||||
minimumValue = scrollBehavior.state.offsetLimit,
|
||||
maximumValue = 0f,
|
||||
)
|
||||
val consumed = abs(initialOffset - scrollBehavior.state.offset)
|
||||
lastValue = value
|
||||
remainingVelocity = this.velocity
|
||||
// avoid rounding errors and stop if anything is unconsumed
|
||||
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
|
||||
}
|
||||
return Velocity(0f, remainingVelocity)
|
||||
}
|
||||
return Velocity.Zero
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun calculateWindowWidthSizeClass(): WindowWidthSizeClass {
|
||||
val configuration = LocalConfiguration.current
|
||||
return fromWidth(configuration.smallestScreenWidthDp.dp)
|
||||
}
|
||||
|
||||
private fun fromWidth(width: Dp): WindowWidthSizeClass {
|
||||
require(width >= 0.dp) { "Width must not be negative" }
|
||||
return when {
|
||||
width < 720.dp -> WindowWidthSizeClass.Compact // Was 600
|
||||
width < 840.dp -> WindowWidthSizeClass.Medium
|
||||
else -> WindowWidthSizeClass.Expanded
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import java.io.Serializable
|
||||
import eu.kanade.domain.chapter.model.Chapter as DomainChapter
|
||||
|
||||
interface Chapter : SChapter, Serializable {
|
||||
|
||||
@ -29,3 +30,21 @@ interface Chapter : SChapter, Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
if (id == null || manga_id == null) return null
|
||||
return DomainChapter(
|
||||
id = id!!,
|
||||
mangaId = manga_id!!,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
lastPageRead = last_page_read.toLong(),
|
||||
dateFetch = date_fetch,
|
||||
sourceOrder = source_order.toLong(),
|
||||
url = url,
|
||||
name = name,
|
||||
dateUpload = date_upload,
|
||||
chapterNumber = chapter_number,
|
||||
scanlator = scanlator,
|
||||
)
|
||||
}
|
||||
|
@ -12,4 +12,24 @@ class LibraryManga : MangaImpl() {
|
||||
get() = readCount > 0
|
||||
|
||||
var category: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is LibraryManga) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (unreadCount != other.unreadCount) return false
|
||||
if (readCount != other.readCount) return false
|
||||
if (category != other.category) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + unreadCount
|
||||
result = 31 * result + readCount
|
||||
result = 31 * result + category
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -121,3 +121,5 @@ fun Source.getNameForMangaInfo(): String {
|
||||
else -> toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun Source.isLocalOrStub(): Boolean = id == LocalSource.ID || this is SourceManager.StubSource
|
||||
|
@ -57,6 +57,33 @@ open class Page(
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Page) return false
|
||||
|
||||
if (index != other.index) return false
|
||||
if (url != other.url) return false
|
||||
if (imageUrl != other.imageUrl) return false
|
||||
if (number != other.number) return false
|
||||
if (status != other.status) return false
|
||||
if (progress != other.progress) return false
|
||||
if (statusSubject != other.statusSubject) return false
|
||||
if (statusCallback != other.statusCallback) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = index
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + (imageUrl?.hashCode() ?: 0)
|
||||
result = 31 * result + status
|
||||
result = 31 * result + progress
|
||||
result = 31 * result + (statusSubject?.hashCode() ?: 0)
|
||||
result = 31 * result + (statusCallback?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
|
@ -83,7 +83,7 @@ class SearchController(
|
||||
binding.progress.isVisible = isReplacingManga
|
||||
if (!isReplacingManga) {
|
||||
router.popController(this)
|
||||
if (newManga != null) {
|
||||
if (newManga?.id != null) {
|
||||
val newMangaController = RouterTransaction.with(MangaController(newManga.id!!))
|
||||
if (router.backstack.lastOrNull()?.controller is MangaController) {
|
||||
// Replace old MangaController
|
||||
|
@ -1,40 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ChangeMangaCoverDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T : ChangeMangaCoverDialog.Listener {
|
||||
|
||||
private lateinit var manga: Manga
|
||||
|
||||
constructor(target: T, manga: Manga) : this() {
|
||||
targetController = target
|
||||
this.manga = manga
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.action_edit_cover)
|
||||
.setPositiveButton(R.string.action_edit) { _, _ ->
|
||||
(targetController as? Listener)?.openMangaCoverPicker(manga)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(R.string.action_delete) { _, _ ->
|
||||
(targetController as? Listener)?.deleteMangaCover(manga)
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteMangaCover(manga: Manga)
|
||||
|
||||
fun openMangaCoverPicker(manga: Manga)
|
||||
}
|
||||
}
|
@ -420,7 +420,7 @@ class MainActivity : BaseActivity() {
|
||||
SHORTCUT_MANGA -> {
|
||||
val extras = intent.extras ?: return false
|
||||
val fgController = router.backstack.lastOrNull()?.controller as? MangaController
|
||||
if (fgController?.manga?.id != extras.getLong(MangaController.MANGA_EXTRA)) {
|
||||
if (fgController?.mangaId != extras.getLong(MangaController.MANGA_EXTRA)) {
|
||||
router.popToRoot()
|
||||
setSelectedNavItem(R.id.nav_library)
|
||||
router.pushController(RouterTransaction.with(MangaController(extras)))
|
||||
@ -601,6 +601,9 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
val isFullComposeController = internalTo is FullComposeController<*>
|
||||
binding.appbar.isVisible = !isFullComposeController
|
||||
binding.controllerContainer.enableScrollingBehavior(!isFullComposeController)
|
||||
|
||||
if (!isTablet()) {
|
||||
// Save lift state
|
||||
if (isPush) {
|
||||
@ -623,17 +626,6 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.root.isLiftAppBarOnScroll = internalTo !is NoAppBarElevationController
|
||||
|
||||
binding.appbar.isVisible = !isFullComposeController
|
||||
binding.controllerContainer.enableScrollingBehavior(!isFullComposeController)
|
||||
|
||||
// TODO: Remove when MangaController is full compose
|
||||
if (!isFullComposeController) {
|
||||
binding.appbar.isTransparentWhenNotLifted = internalTo is MangaController
|
||||
binding.controllerContainer.overlapHeader = internalTo is MangaController
|
||||
}
|
||||
} else {
|
||||
binding.appbar.isVisible = !isFullComposeController
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,127 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.color
|
||||
import androidx.core.view.isVisible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import java.util.Date
|
||||
|
||||
class ChapterHolder(
|
||||
view: View,
|
||||
private val adapter: ChaptersAdapter,
|
||||
) : BaseChapterHolder(view, adapter) {
|
||||
|
||||
private val binding = ChaptersItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.download.listener = downloadActionListener
|
||||
}
|
||||
|
||||
fun bind(item: ChapterItem, manga: Manga) {
|
||||
val chapter = item.chapter
|
||||
|
||||
binding.chapterTitle.text = when (manga.displayMode) {
|
||||
Manga.CHAPTER_DISPLAY_NUMBER -> {
|
||||
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||
}
|
||||
else -> chapter.name
|
||||
// TODO: show cleaned name consistently around the app
|
||||
// else -> cleanChapterName(chapter, manga)
|
||||
}
|
||||
|
||||
// Set correct text color
|
||||
val chapterTitleColor = when {
|
||||
chapter.read -> adapter.readColor
|
||||
chapter.bookmark -> adapter.bookmarkedColor
|
||||
else -> adapter.unreadColor
|
||||
}
|
||||
binding.chapterTitle.setTextColor(chapterTitleColor)
|
||||
|
||||
val chapterDescriptionColor = when {
|
||||
chapter.read -> adapter.readColor
|
||||
chapter.bookmark -> adapter.bookmarkedColor
|
||||
else -> adapter.unreadColorSecondary
|
||||
}
|
||||
binding.chapterDescription.setTextColor(chapterDescriptionColor)
|
||||
|
||||
binding.bookmarkIcon.isVisible = chapter.bookmark
|
||||
|
||||
val descriptions = mutableListOf<CharSequence>()
|
||||
|
||||
if (chapter.date_upload > 0) {
|
||||
descriptions.add(Date(chapter.date_upload).toRelativeString(itemView.context, adapter.relativeTime, adapter.dateFormat))
|
||||
}
|
||||
if (!chapter.read && chapter.last_page_read > 0) {
|
||||
val lastPageRead = buildSpannedString {
|
||||
color(adapter.readColor) {
|
||||
append(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1))
|
||||
}
|
||||
}
|
||||
descriptions.add(lastPageRead)
|
||||
}
|
||||
if (!chapter.scanlator.isNullOrBlank()) {
|
||||
descriptions.add(chapter.scanlator!!)
|
||||
}
|
||||
|
||||
if (descriptions.isNotEmpty()) {
|
||||
binding.chapterDescription.text = descriptions.joinTo(SpannableStringBuilder(), " • ")
|
||||
} else {
|
||||
binding.chapterDescription.text = ""
|
||||
}
|
||||
|
||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||
binding.download.setState(item.status, item.progress)
|
||||
}
|
||||
|
||||
private fun cleanChapterName(chapter: Chapter, manga: Manga): String {
|
||||
return chapter.name
|
||||
.trim()
|
||||
.removePrefix(manga.title)
|
||||
.trim(*CHAPTER_TRIM_CHARS)
|
||||
}
|
||||
}
|
||||
|
||||
private val CHAPTER_TRIM_CHARS = arrayOf(
|
||||
// Whitespace
|
||||
' ',
|
||||
'\u0009',
|
||||
'\u000A',
|
||||
'\u000B',
|
||||
'\u000C',
|
||||
'\u000D',
|
||||
'\u0020',
|
||||
'\u0085',
|
||||
'\u00A0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u2001',
|
||||
'\u2002',
|
||||
'\u2003',
|
||||
'\u2004',
|
||||
'\u2005',
|
||||
'\u2006',
|
||||
'\u2007',
|
||||
'\u2008',
|
||||
'\u2009',
|
||||
'\u200A',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202F',
|
||||
'\u205F',
|
||||
'\u3000',
|
||||
|
||||
// Separators
|
||||
'-',
|
||||
'_',
|
||||
',',
|
||||
':',
|
||||
).toCharArray()
|
@ -1,33 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterItem
|
||||
|
||||
class ChapterItem(chapter: Chapter, val manga: Manga) :
|
||||
BaseChapterItem<ChapterHolder, AbstractHeaderItem<FlexibleViewHolder>>(chapter) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.chapters_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ChapterHolder {
|
||||
return ChapterHolder(view, adapter as ChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
holder.bind(this, manga)
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
class ChaptersAdapter(
|
||||
controller: MangaController,
|
||||
context: Context,
|
||||
) : BaseChaptersAdapter<ChapterItem>(controller) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
var items: List<ChapterItem> = emptyList()
|
||||
|
||||
val readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
|
||||
val unreadColor = context.getResourceColor(R.attr.colorOnSurface)
|
||||
val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
|
||||
|
||||
val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val decimalFormat = DecimalFormat(
|
||||
"#.###",
|
||||
DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' },
|
||||
)
|
||||
|
||||
val relativeTime: Int = preferences.relativeTime().get()
|
||||
val dateFormat: DateFormat = preferences.dateFormat()
|
||||
|
||||
override fun updateDataSet(items: List<ChapterItem>?) {
|
||||
this.items = items ?: emptyList()
|
||||
super.updateDataSet(items)
|
||||
}
|
||||
|
||||
fun indexOf(item: ChapterItem): Int {
|
||||
return items.indexOf(item)
|
||||
}
|
||||
}
|
@ -7,10 +7,11 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.domain.manga.model.toTriStateGroupState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||
@ -18,6 +19,9 @@ import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChaptersSettingsSheet(
|
||||
private val router: Router,
|
||||
@ -28,7 +32,7 @@ class ChaptersSettingsSheet(
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
val filters = Filter(context)
|
||||
private val filters = Filter(context)
|
||||
private val sort = Sort(context)
|
||||
private val display = Display(context)
|
||||
|
||||
@ -42,8 +46,14 @@ class ChaptersSettingsSheet(
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
scope = MainScope()
|
||||
// TODO: Listen to changes
|
||||
updateManga()
|
||||
scope.launch {
|
||||
presenter.state
|
||||
.filterIsInstance<MangaScreenState.Success>()
|
||||
.collectLatest {
|
||||
manga = it.manga
|
||||
getTabViews().forEach { settings -> (settings as Settings).updateView() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
@ -63,17 +73,13 @@ class ChaptersSettingsSheet(
|
||||
R.string.action_display,
|
||||
)
|
||||
|
||||
private fun updateManga() {
|
||||
manga = presenter.manga.toDomainManga()
|
||||
}
|
||||
|
||||
private fun showPopupMenu(view: View) {
|
||||
view.popupMenu(
|
||||
menuRes = R.menu.default_chapter_filter,
|
||||
onMenuItemClick = {
|
||||
when (itemId) {
|
||||
R.id.set_as_default -> {
|
||||
SetChapterSettingsDialog(presenter.manga).showDialog(router)
|
||||
SetChapterSettingsDialog(presenter.manga!!.toDbManga()).showDialog(router)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -144,10 +150,6 @@ class ChaptersSettingsSheet(
|
||||
bookmarked -> presenter.setBookmarkedFilter(newState)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
updateManga()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,16 +204,11 @@ class ChaptersSettingsSheet(
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
when (item) {
|
||||
source -> presenter.setSorting(Manga.CHAPTER_SORTING_SOURCE.toInt())
|
||||
chapterNum -> presenter.setSorting(Manga.CHAPTER_SORTING_NUMBER.toInt())
|
||||
uploadDate -> presenter.setSorting(Manga.CHAPTER_SORTING_UPLOAD_DATE.toInt())
|
||||
source -> presenter.setSorting(Manga.CHAPTER_SORTING_SOURCE)
|
||||
chapterNum -> presenter.setSorting(Manga.CHAPTER_SORTING_NUMBER)
|
||||
uploadDate -> presenter.setSorting(Manga.CHAPTER_SORTING_UPLOAD_DATE)
|
||||
else -> throw Exception("Unknown sorting")
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
presenter.reverseSortOrder()
|
||||
updateManga()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -257,14 +254,10 @@ class ChaptersSettingsSheet(
|
||||
if (item.checked) return
|
||||
|
||||
when (item) {
|
||||
displayTitle -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NAME.toInt())
|
||||
displayChapterNum -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NUMBER.toInt())
|
||||
displayTitle -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NAME)
|
||||
displayChapterNum -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NUMBER)
|
||||
else -> throw NotImplementedError("Unknown display mode")
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
updateManga()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : DeleteChaptersDialog.Listener {
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.confirm_delete_chapters)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters()
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.MangaChaptersHeaderBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
||||
class MangaChaptersHeaderAdapter(
|
||||
private val controller: MangaController,
|
||||
) :
|
||||
RecyclerView.Adapter<MangaChaptersHeaderAdapter.HeaderViewHolder>() {
|
||||
|
||||
private var numChapters: Int? = null
|
||||
private var hasActiveFilters: Boolean = false
|
||||
|
||||
private lateinit var binding: MangaChaptersHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||
binding = MangaChaptersHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return HeaderViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun getItemId(position: Int): Long = hashCode().toLong()
|
||||
|
||||
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
|
||||
holder.bind()
|
||||
}
|
||||
|
||||
fun setNumChapters(numChapters: Int) {
|
||||
this.numChapters = numChapters
|
||||
notifyItemChanged(0, this)
|
||||
}
|
||||
|
||||
fun setHasActiveFilters(hasActiveFilters: Boolean) {
|
||||
this.hasActiveFilters = hasActiveFilters
|
||||
notifyItemChanged(0, this)
|
||||
}
|
||||
|
||||
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun bind() {
|
||||
binding.chaptersLabel.text = if (numChapters == null) {
|
||||
view.context.getString(R.string.chapters)
|
||||
} else {
|
||||
view.context.resources.getQuantityString(R.plurals.manga_num_chapters, numChapters!!, numChapters)
|
||||
}
|
||||
|
||||
val filterColor = if (hasActiveFilters) {
|
||||
view.context.getResourceColor(R.attr.colorFilterActive)
|
||||
} else {
|
||||
view.context.getResourceColor(R.attr.colorOnBackground)
|
||||
}
|
||||
binding.btnChaptersFilter.drawable.setTint(filterColor)
|
||||
|
||||
merge(view.clicks(), binding.btnChaptersFilter.clicks())
|
||||
.onEach { controller.showSettingsSheet() }
|
||||
.launchIn(controller.viewScope)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,276 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
import reactivecircus.flowbinding.android.view.longClicks
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaInfoHeaderAdapter(
|
||||
private val controller: MangaController,
|
||||
private val fromSource: Boolean,
|
||||
private val isTablet: Boolean,
|
||||
) :
|
||||
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
private var manga: Manga = controller.presenter.manga
|
||||
private var source: Source = controller.presenter.source
|
||||
private var trackCount: Int = 0
|
||||
|
||||
private lateinit var binding: MangaInfoHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||
binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
updateCoverPosition()
|
||||
|
||||
// Expand manga info if navigated from source listing or explicitly set to
|
||||
// (e.g. on tablets)
|
||||
binding.mangaSummarySection.expanded = fromSource || isTablet
|
||||
|
||||
return HeaderViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun getItemId(position: Int): Long = hashCode().toLong()
|
||||
|
||||
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
|
||||
holder.bind()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the view with manga information.
|
||||
*
|
||||
* @param manga manga object containing information about manga.
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
fun update(manga: Manga, source: Source) {
|
||||
this.manga = manga
|
||||
this.source = source
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
notifyItemChanged(0, this)
|
||||
}
|
||||
|
||||
fun setTrackingCount(trackCount: Int) {
|
||||
this.trackCount = trackCount
|
||||
update()
|
||||
}
|
||||
|
||||
private fun updateCoverPosition() {
|
||||
if (isTablet) return
|
||||
val appBarHeight = controller.getMainAppBarHeight()
|
||||
binding.mangaCover.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin += appBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun bind() {
|
||||
// For rounded corners
|
||||
binding.mangaCover.clipToOutline = true
|
||||
|
||||
binding.btnFavorite.clicks()
|
||||
.onEach { controller.onFavoriteClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
if (controller.presenter.manga.favorite) {
|
||||
binding.btnFavorite.longClicks()
|
||||
.onEach { controller.onCategoriesClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
}
|
||||
|
||||
with(binding.btnTracking) {
|
||||
if (trackManager.hasLoggedServices()) {
|
||||
isVisible = true
|
||||
|
||||
if (trackCount > 0) {
|
||||
setIconResource(R.drawable.ic_done_24dp)
|
||||
text = view.context.resources.getQuantityString(
|
||||
R.plurals.num_trackers,
|
||||
trackCount,
|
||||
trackCount,
|
||||
)
|
||||
isActivated = true
|
||||
} else {
|
||||
setIconResource(R.drawable.ic_sync_24dp)
|
||||
text = view.context.getString(R.string.manga_tracking_tab)
|
||||
isActivated = false
|
||||
}
|
||||
|
||||
clicks()
|
||||
.onEach { controller.onTrackingClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
if (controller.presenter.source is HttpSource) {
|
||||
binding.btnWebview.isVisible = true
|
||||
binding.btnWebview.clicks()
|
||||
.onEach { controller.openMangaInWebView() }
|
||||
.launchIn(controller.viewScope)
|
||||
}
|
||||
|
||||
binding.mangaFullTitle.longClicks()
|
||||
.onEach {
|
||||
controller.activity?.copyToClipboard(
|
||||
view.context.getString(R.string.title),
|
||||
binding.mangaFullTitle.text.toString(),
|
||||
)
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaFullTitle.clicks()
|
||||
.onEach {
|
||||
controller.performGlobalSearch(binding.mangaFullTitle.text.toString())
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaAuthor.longClicks()
|
||||
.onEach {
|
||||
controller.activity?.copyToClipboard(
|
||||
binding.mangaAuthor.text.toString(),
|
||||
binding.mangaAuthor.text.toString(),
|
||||
)
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaAuthor.clicks()
|
||||
.onEach {
|
||||
controller.performGlobalSearch(binding.mangaAuthor.text.toString())
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaArtist.longClicks()
|
||||
.onEach {
|
||||
controller.activity?.copyToClipboard(
|
||||
binding.mangaArtist.text.toString(),
|
||||
binding.mangaArtist.text.toString(),
|
||||
)
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaArtist.clicks()
|
||||
.onEach {
|
||||
controller.performGlobalSearch(binding.mangaArtist.text.toString())
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaCover.clicks()
|
||||
.onEach {
|
||||
controller.showFullCoverDialog()
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
setMangaInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the view with manga information.
|
||||
*
|
||||
* @param manga manga object containing information about manga.
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
private fun setMangaInfo() {
|
||||
// Update full title TextView.
|
||||
binding.mangaFullTitle.text = manga.title.ifBlank { view.context.getString(R.string.unknown) }
|
||||
|
||||
// Update author TextView.
|
||||
binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) {
|
||||
view.context.getString(R.string.unknown_author)
|
||||
} else {
|
||||
manga.author
|
||||
}
|
||||
|
||||
// Update artist TextView.
|
||||
val hasArtist = !manga.artist.isNullOrBlank() && manga.artist != manga.author
|
||||
binding.mangaArtist.isVisible = hasArtist
|
||||
if (hasArtist) {
|
||||
binding.mangaArtist.text = manga.artist
|
||||
}
|
||||
|
||||
// If manga source is known update source TextView.
|
||||
binding.mangaMissingSourceIcon.isVisible = source is SourceManager.StubSource
|
||||
|
||||
with(binding.mangaSource) {
|
||||
text = source.getNameForMangaInfo()
|
||||
|
||||
setOnClickListener {
|
||||
controller.performSearch(sourceManager.getOrStub(source.id).name)
|
||||
}
|
||||
}
|
||||
|
||||
// Update manga status.
|
||||
val (statusDrawable, statusString) = when (manga.status) {
|
||||
SManga.ONGOING -> R.drawable.ic_status_ongoing_24dp to R.string.ongoing
|
||||
SManga.COMPLETED -> R.drawable.ic_status_completed_24dp to R.string.completed
|
||||
SManga.LICENSED -> R.drawable.ic_status_licensed_24dp to R.string.licensed
|
||||
SManga.PUBLISHING_FINISHED -> R.drawable.ic_done_24dp to R.string.publishing_finished
|
||||
SManga.CANCELLED -> R.drawable.ic_close_24dp to R.string.cancelled
|
||||
SManga.ON_HIATUS -> R.drawable.ic_pause_24dp to R.string.on_hiatus
|
||||
else -> R.drawable.ic_status_unknown_24dp to R.string.unknown
|
||||
}
|
||||
binding.mangaStatusIcon.setImageResource(statusDrawable)
|
||||
binding.mangaStatus.setText(statusString)
|
||||
|
||||
// Set the favorite drawable to the correct one.
|
||||
setFavoriteButtonState(manga.favorite)
|
||||
|
||||
// Set cover if changed.
|
||||
binding.backdrop.loadAutoPause(manga)
|
||||
binding.mangaCover.loadAutoPause(manga)
|
||||
|
||||
// Manga info section
|
||||
binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch)
|
||||
binding.mangaSummarySection.description = manga.description
|
||||
binding.mangaSummarySection.isVisible = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite button with correct drawable and text.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
private fun setFavoriteButtonState(isFavorite: Boolean) {
|
||||
// Set the Favorite drawable to the correct one.
|
||||
// Border drawable if false, filled drawable if true.
|
||||
val (iconResource, stringResource) = when (isFavorite) {
|
||||
true -> R.drawable.ic_favorite_24dp to R.string.in_library
|
||||
false -> R.drawable.ic_favorite_border_24dp to R.string.add_to_library
|
||||
}
|
||||
binding.btnFavorite.apply {
|
||||
setIconResource(iconResource)
|
||||
text = context.getString(stringResource)
|
||||
isActivated = isFavorite
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -80,7 +80,7 @@ class TrackSearchDialog : DialogController {
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedViewState == null) {
|
||||
currentlySearched = trackController.presenter.manga.title
|
||||
currentlySearched = trackController.presenter.manga!!.title
|
||||
binding!!.titleInput.editText?.append(currentlySearched)
|
||||
}
|
||||
search(currentlySearched)
|
||||
|
@ -10,6 +10,7 @@ import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.DateValidatorPointBackward
|
||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
|
||||
@ -25,7 +26,7 @@ import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||
|
||||
class TrackSheet(
|
||||
val controller: MangaController,
|
||||
val fragmentManager: FragmentManager,
|
||||
private val fragmentManager: FragmentManager,
|
||||
) : BaseBottomSheetDialog(controller.activity!!),
|
||||
TrackAdapter.OnClickListener,
|
||||
SetTrackStatusDialog.Listener,
|
||||
@ -74,8 +75,8 @@ class TrackSheet(
|
||||
|
||||
override fun onSetClick(position: Int) {
|
||||
val item = adapter.getItem(position) ?: return
|
||||
val manga = controller.presenter.manga
|
||||
val source = controller.presenter.source
|
||||
val manga = controller.presenter.manga?.toDbManga() ?: return
|
||||
val source = controller.presenter.source ?: return
|
||||
|
||||
if (item.service is EnhancedTrackService) {
|
||||
if (item.track != null) {
|
||||
|
@ -34,7 +34,7 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
|
||||
nestedScrollInterop = nestedScrollInterop,
|
||||
presenter = presenter,
|
||||
onClickCover = { history ->
|
||||
router.pushController(MangaController(history))
|
||||
router.pushController(MangaController(history.id))
|
||||
},
|
||||
onClickResume = { history ->
|
||||
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
|
||||
|
@ -7,6 +7,7 @@ import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@ -48,19 +49,18 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
}
|
||||
|
||||
fun Manga.removeCovers(coverCache: CoverCache) {
|
||||
if (isLocal()) return
|
||||
fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int {
|
||||
if (isLocal()) return 0
|
||||
|
||||
cover_last_modified = Date().time
|
||||
coverCache.deleteFromCache(this, true)
|
||||
}
|
||||
|
||||
fun Manga.updateCoverLastModified(db: DatabaseHelper) {
|
||||
cover_last_modified = Date().time
|
||||
db.updateMangaCoverLastModified(this).executeAsBlocking()
|
||||
return coverCache.deleteFromCache(this, true)
|
||||
}
|
||||
|
||||
fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper): Boolean {
|
||||
return toDomainManga()?.shouldDownloadNewChapters(db, prefs) ?: false
|
||||
}
|
||||
|
||||
fun DomainManga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper): Boolean {
|
||||
if (!favorite) return false
|
||||
|
||||
// Boolean to determine if user wants to automatically download new chapters.
|
||||
@ -75,7 +75,7 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
||||
|
||||
// Get all categories, else default category (0)
|
||||
val categoriesForManga =
|
||||
db.getCategoriesForManga(this).executeAsBlocking()
|
||||
db.getCategoriesForManga(toDbManga()).executeAsBlocking()
|
||||
.mapNotNull { it.id }
|
||||
.takeUnless { it.isEmpty() } ?: listOf(0)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -34,6 +35,18 @@ object ChapterSettingsHelper {
|
||||
db.updateChapterFlags(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
suspend fun applySettingDefaults(mangaId: Long, setMangaChapterFlags: SetMangaChapterFlags) {
|
||||
setMangaChapterFlags.awaitSetAllFlags(
|
||||
mangaId = mangaId,
|
||||
unreadFilter = prefs.filterChapterByRead().toLong(),
|
||||
downloadedFilter = prefs.filterChapterByDownloaded().toLong(),
|
||||
bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(),
|
||||
sortingMode = prefs.sortChapterBySourceOrNumber().toLong(),
|
||||
sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(),
|
||||
displayMode = prefs.displayChapterByNameOrNumber().toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all mangas in library with global Chapter Settings.
|
||||
*/
|
||||
|
@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.domain.chapter.model.Chapter as DomainChapter
|
||||
import eu.kanade.domain.manga.model.Manga as DomainManga
|
||||
|
||||
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
|
||||
return when (manga.sorting) {
|
||||
@ -20,3 +23,28 @@ fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending(
|
||||
else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapterSort(
|
||||
manga: DomainManga,
|
||||
sortDescending: Boolean = manga.sortDescending(),
|
||||
): (DomainChapter, DomainChapter) -> Int {
|
||||
return when (manga.sorting) {
|
||||
DomainManga.CHAPTER_SORTING_SOURCE -> when (sortDescending) {
|
||||
true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) }
|
||||
false -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) }
|
||||
}
|
||||
DomainManga.CHAPTER_SORTING_NUMBER -> when (sortDescending) {
|
||||
true -> { c1, c2 ->
|
||||
c2.chapterNumber.toString().compareToCaseInsensitiveNaturalOrder(c1.chapterNumber.toString())
|
||||
}
|
||||
false -> { c1, c2 ->
|
||||
c1.chapterNumber.toString().compareToCaseInsensitiveNaturalOrder(c2.chapterNumber.toString())
|
||||
}
|
||||
}
|
||||
DomainManga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) {
|
||||
true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) }
|
||||
false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) }
|
||||
}
|
||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
||||
}
|
||||
}
|
||||
|
@ -1,196 +0,0 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.MangaSummaryBinding
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.view.setChips
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
class MangaSummaryView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
@StyleRes defStyleRes: Int = 0,
|
||||
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
private val binding = MangaSummaryBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
private var animatorSet: AnimatorSet? = null
|
||||
|
||||
private var recalculateHeights = false
|
||||
private var descExpandedHeight = -1
|
||||
private var descShrunkHeight = -1
|
||||
|
||||
var expanded = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
updateExpandState()
|
||||
}
|
||||
}
|
||||
|
||||
var description: CharSequence? = null
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = if (value.isNullOrBlank()) {
|
||||
context.getString(R.string.unknown)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
binding.descriptionText.text = field
|
||||
recalculateHeights = true
|
||||
doOnNextLayout {
|
||||
updateExpandState()
|
||||
}
|
||||
if (!isInLayout) {
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setTags(items: List<String>?, onClick: (item: String) -> Unit) {
|
||||
listOfNotNull(binding.tagChipsShrunk, binding.tagChipsExpanded).forEach { chips ->
|
||||
chips.setChips(items, onClick) { tag -> context.copyToClipboard(tag, tag) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExpandState() = binding.apply {
|
||||
val initialSetup = descriptionText.maxHeight < 0
|
||||
|
||||
val maxHeightTarget = if (expanded) descExpandedHeight else descShrunkHeight
|
||||
val maxHeightStart = if (initialSetup) maxHeightTarget else descriptionText.maxHeight
|
||||
val descMaxHeightAnimator = ValueAnimator().apply {
|
||||
setIntValues(maxHeightStart, maxHeightTarget)
|
||||
addUpdateListener {
|
||||
descriptionText.maxHeight = it.animatedValue as Int
|
||||
}
|
||||
}
|
||||
|
||||
val toggleDrawable = ContextCompat.getDrawable(
|
||||
context,
|
||||
if (expanded) R.drawable.anim_caret_up else R.drawable.anim_caret_down,
|
||||
)
|
||||
toggleMore.setImageDrawable(toggleDrawable)
|
||||
|
||||
var pastHalf = false
|
||||
val toggleTarget = if (expanded) 1F else 0F
|
||||
val toggleStart = if (initialSetup) {
|
||||
toggleTarget
|
||||
} else {
|
||||
toggleMore.translationY / toggleMore.height
|
||||
}
|
||||
val toggleAnimator = ValueAnimator().apply {
|
||||
setFloatValues(toggleStart, toggleTarget)
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
|
||||
toggleMore.translationY = toggleMore.height * value
|
||||
descriptionScrim.translationY = toggleMore.translationY
|
||||
toggleMoreScrim.translationY = toggleMore.translationY
|
||||
tagChipsShrunkContainer.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
topMargin = toggleMore.translationY.roundToInt()
|
||||
}
|
||||
tagChipsExpanded.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
topMargin = toggleMore.translationY.roundToInt()
|
||||
}
|
||||
|
||||
// Update non-animatable objects mid-animation makes it feel less abrupt
|
||||
if (it.animatedFraction >= 0.5F && !pastHalf) {
|
||||
pastHalf = true
|
||||
descriptionText.text = trimWhenNeeded(description)
|
||||
tagChipsShrunkContainer.scrollX = 0
|
||||
tagChipsShrunkContainer.isVisible = !expanded
|
||||
tagChipsExpanded.isVisible = expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animatorSet?.cancel()
|
||||
animatorSet = AnimatorSet().apply {
|
||||
interpolator = FastOutSlowInInterpolator()
|
||||
duration = (TOGGLE_ANIM_DURATION * context.animatorDurationScale).roundToLong()
|
||||
playTogether(toggleAnimator, descMaxHeightAnimator)
|
||||
start()
|
||||
}
|
||||
(toggleDrawable as? Animatable)?.start()
|
||||
}
|
||||
|
||||
private fun trimWhenNeeded(text: CharSequence?): CharSequence? {
|
||||
return if (!expanded) {
|
||||
text
|
||||
?.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
|
||||
?.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
// Wait until parent view has determined the exact width
|
||||
// because this affect the description line count
|
||||
val measureWidthFreely = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
|
||||
if (!recalculateHeights || measureWidthFreely) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
recalculateHeights = false
|
||||
|
||||
// Measure with expanded lines
|
||||
binding.descriptionText.maxLines = Int.MAX_VALUE
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
descExpandedHeight = binding.descriptionText.measuredHeight
|
||||
|
||||
// Measure with shrunk lines
|
||||
binding.descriptionText.maxLines = SHRUNK_DESC_MAX_LINES
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
descShrunkHeight = binding.descriptionText.measuredHeight
|
||||
}
|
||||
|
||||
init {
|
||||
binding.descriptionText.apply {
|
||||
// So that 1 line of text won't be hidden by scrim
|
||||
minLines = DESC_MIN_LINES
|
||||
|
||||
setOnLongClickListener {
|
||||
description?.let {
|
||||
context.copyToClipboard(
|
||||
context.getString(R.string.description),
|
||||
it.toString(),
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
arrayOf(
|
||||
binding.descriptionText,
|
||||
binding.descriptionScrim,
|
||||
binding.toggleMoreScrim,
|
||||
binding.toggleMore,
|
||||
).forEach {
|
||||
it.setOnClickListener { expanded = !expanded }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TOGGLE_ANIM_DURATION = 300L
|
||||
|
||||
private const val DESC_MIN_LINES = 2
|
||||
private const val SHRUNK_DESC_MAX_LINES = 3
|
@ -4,6 +4,7 @@ import android.view.LayoutInflater
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
@ -11,6 +12,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.databinding.DialogStubQuadstatemultichoiceBinding
|
||||
import eu.kanade.tachiyomi.databinding.DialogStubTextinputBinding
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
fun MaterialAlertDialogBuilder.setTextInput(
|
||||
hint: String? = null,
|
||||
@ -71,3 +74,19 @@ fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems(
|
||||
}
|
||||
return setView(binding.root)
|
||||
}
|
||||
|
||||
suspend fun MaterialAlertDialogBuilder.await(
|
||||
@StringRes positiveLabelId: Int,
|
||||
@StringRes negativeLabelId: Int,
|
||||
@StringRes neutralLabelId: Int? = null,
|
||||
) = suspendCancellableCoroutine<Int> { cont ->
|
||||
setPositiveButton(positiveLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_POSITIVE) }
|
||||
setNegativeButton(negativeLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_NEGATIVE) }
|
||||
if (neutralLabelId != null) {
|
||||
setNeutralButton(neutralLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_NEUTRAL) }
|
||||
}
|
||||
setOnDismissListener { cont.cancel() }
|
||||
|
||||
val dialog = show()
|
||||
cont.invokeOnCancellation { dialog.dismiss() }
|
||||
}
|
||||
|
@ -1,84 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="caret_up"
|
||||
android:height="24.0dip"
|
||||
android:width="24.0dip"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<group
|
||||
android:name="caret02"
|
||||
android:rotation="90.0"
|
||||
android:translateX="12.0"
|
||||
android:translateY="9.0">
|
||||
<group
|
||||
android:name="caret02_l"
|
||||
android:rotation="-45.0">
|
||||
<group
|
||||
android:name="caret02_l_pivot"
|
||||
android:translateY="4.0">
|
||||
<group
|
||||
android:name="caret02_l_rect_position"
|
||||
android:translateY="-1.0">
|
||||
<path
|
||||
android:name="caret02_l_rect"
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M -1.0,-4.0 l 2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l -2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,-8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="caret02_r"
|
||||
android:rotation="45.0">
|
||||
<group
|
||||
android:name="caret02_r_pivot"
|
||||
android:translateY="-4.0">
|
||||
<group
|
||||
android:name="caret02_r_rect_position"
|
||||
android:translateY="1.0">
|
||||
<path
|
||||
android:name="caret02_r_rect"
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M -1.0,-4.0 l 2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l -2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,-8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
|
||||
<target android:name="caret02">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:duration="300"
|
||||
android:pathData="M 12.0,15.0 c 0.0,-1.0 0.0,-5.33333 0.0,-6.0"
|
||||
android:propertyXName="translateX"
|
||||
android:propertyYName="translateY" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="caret02_l">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:duration="300"
|
||||
android:valueFrom="45.0"
|
||||
android:valueTo="-45.0"
|
||||
android:valueType="floatType"
|
||||
android:propertyName="rotation" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="caret02_r">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:duration="300"
|
||||
android:valueFrom="-45.0"
|
||||
android:valueTo="45.0"
|
||||
android:valueType="floatType"
|
||||
android:propertyName="rotation" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linear_recycler_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/info_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/chapters_recycler"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_max="@dimen/tablet_sidebar_max_width"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/manga_info_header" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/chapters_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/fab_list_padding"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/info_recycler"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:listitem="@layout/chapters_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_gravity="end"
|
||||
app:fastScrollerBubbleEnabled="false"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,208 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backdrop"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="-32dp"
|
||||
android:alpha="0.2"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/manga_cover"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:background="@mipmap/ic_launcher"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<View
|
||||
android:id="@+id/backdrop_overlay"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="1"
|
||||
android:background="@drawable/manga_backdrop_gradient"
|
||||
android:backgroundTint="?android:attr/colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/backdrop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manga_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/tablet_horizontal_cover_margin"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="@dimen/tablet_horizontal_cover_margin"
|
||||
android:background="@drawable/rounded_rectangle"
|
||||
android:contentDescription="@string/description_cover"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="w,3:2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manga_detail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="-8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/backdrop">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_full_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/manga_info_full_title_label"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge"
|
||||
android:textIsSelectable="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textAlignment="center"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Author" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_artist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Artist" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manga_status_row"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manga_status_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_status_unknown_24dp"
|
||||
app:tint="?android:attr/textColorSecondary"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Status" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manga_missing_source_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_warning_white_24dp"
|
||||
app:tint="@color/error"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="•"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_source"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Source" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manga_actions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/manga_detail">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_favorite"
|
||||
style="@style/Widget.Tachiyomi.Button.ActionButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/add_to_library"
|
||||
app:icon="@drawable/ic_favorite_border_24dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_tracking"
|
||||
style="@style/Widget.Tachiyomi.Button.ActionButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/manga_tracking_tab"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_sync_24dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_webview"
|
||||
style="@style/Widget.Tachiyomi.Button.ActionButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_web_view"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_public_24dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MangaSummaryView
|
||||
android:id="@+id/manga_summary_section"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/manga_actions" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,38 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="4dp"
|
||||
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chapters_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/chapters"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textIsSelectable="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_chapters_filter"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_chapters_filter"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_filter"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_filter_list_24dp"
|
||||
app:tint="?attr/colorOnBackground" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/full_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/fab_list_padding"
|
||||
tools:listitem="@layout/chapters_item" />
|
||||
|
||||
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_gravity="end"
|
||||
app:fastScrollerBubbleEnabled="false"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="?attr/actionBarTheme"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:menu="@menu/full_cover"
|
||||
app:navigationIcon="@drawable/ic_close_24dp" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
android:id="@+id/container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,220 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.browse.source.browse.BrowseSourceController">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backdrop"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="-32dp"
|
||||
android:alpha="0.2"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/manga_cover"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:background="@mipmap/ic_launcher"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<View
|
||||
android:id="@+id/backdrop_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/manga_backdrop_gradient"
|
||||
android:backgroundTint="?android:attr/colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/backdrop"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/manga_cover_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:barrierMargin="16dp"
|
||||
app:constraint_referenced_ids="manga_cover"
|
||||
app:layout_constraintTop_toTopOf="@id/manga_cover" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manga_cover"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/rounded_rectangle"
|
||||
android:contentDescription="@string/description_cover"
|
||||
android:maxWidth="100dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="w,3:2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:layout_height="133dp"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manga_detail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="@id/manga_info_barrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/manga_cover"
|
||||
app:layout_constraintTop_toTopOf="@id/manga_cover_barrier">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_full_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/manga_info_full_title_label"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge"
|
||||
android:textIsSelectable="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Author" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_artist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Artist" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manga_status_row"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manga_status_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_status_unknown_24dp"
|
||||
app:tint="?android:attr/textColorSecondary"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Status" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="•"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manga_missing_source_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_warning_white_24dp"
|
||||
app:tint="@color/error"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_source"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Source" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/manga_info_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="manga_cover,manga_detail" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manga_actions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/manga_info_barrier">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_favorite"
|
||||
style="@style/Widget.Tachiyomi.Button.ActionButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/add_to_library"
|
||||
app:icon="@drawable/ic_favorite_border_24dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_tracking"
|
||||
style="@style/Widget.Tachiyomi.Button.ActionButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/manga_tracking_tab"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_sync_24dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_webview"
|
||||
style="@style/Widget.Tachiyomi.Button.ActionButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_web_view"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_public_24dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MangaSummaryView
|
||||
android:id="@+id/manga_summary_section"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/manga_actions" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:ellipsize="end"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
app:firstBaselineToTopHeight="0dp"
|
||||
app:lastBaselineToBottomHeight="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Collapsed summary content Collapsed summary content Collapsed summary content Collapsed summary content Collapsed summary content Collapsed summary content" />
|
||||
|
||||
<View
|
||||
android:id="@+id/description_scrim"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="24sp"
|
||||
android:background="@drawable/manga_info_gradient"
|
||||
android:backgroundTint="?android:attr/colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/description_text"
|
||||
app:layout_constraintEnd_toEndOf="@+id/description_text"
|
||||
app:layout_constraintStart_toStartOf="@+id/description_text" />
|
||||
|
||||
<View
|
||||
android:id="@+id/toggle_more_scrim"
|
||||
android:layout_width="36sp"
|
||||
android:layout_height="18sp"
|
||||
android:background="@drawable/manga_info_more_gradient"
|
||||
android:backgroundTint="?android:attr/colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/toggle_more"
|
||||
app:layout_constraintEnd_toEndOf="@+id/toggle_more"
|
||||
app:layout_constraintStart_toStartOf="@+id/toggle_more"
|
||||
app:layout_constraintTop_toTopOf="@+id/toggle_more" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/toggle_more"
|
||||
style="@style/Widget.Tachiyomi.Button.InlineButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="-6dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/manga_info_expand"
|
||||
android:padding="0dp"
|
||||
android:src="@drawable/anim_caret_down"
|
||||
app:layout_constraintBottom_toBottomOf="@id/description_text"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/tag_chips_shrunk_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toggle_more">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/tag_chips_shrunk"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp"
|
||||
app:chipSpacingHorizontal="4dp"
|
||||
app:singleLine="true" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/tag_chips_expanded"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:visibility="gone"
|
||||
app:chipSpacingHorizontal="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toggle_more"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share_cover"
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/action_share"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/action_save_cover"
|
||||
android:icon="@drawable/ic_save_24dp"
|
||||
android:title="@string/action_save"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/action_edit_cover"
|
||||
android:icon="@drawable/ic_edit_24dp"
|
||||
android:title="@string/action_edit"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/action_share"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/download_group"
|
||||
android:icon="@drawable/ic_get_app_24dp"
|
||||
android:title="@string/manga_download"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/download_next"
|
||||
android:title="@string/download_1" />
|
||||
<item
|
||||
android:id="@+id/download_next_5"
|
||||
android:title="@string/download_5" />
|
||||
<item
|
||||
android:id="@+id/download_next_10"
|
||||
android:title="@string/download_10" />
|
||||
<item
|
||||
android:id="@+id/download_custom"
|
||||
android:title="@string/download_custom" />
|
||||
<item
|
||||
android:id="@+id/download_unread"
|
||||
android:title="@string/download_unread" />
|
||||
<item
|
||||
android:id="@+id/download_all"
|
||||
android:title="@string/download_all" />
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_categories"
|
||||
android:title="@string/action_edit_categories"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_migrate"
|
||||
android:title="@string/action_migrate"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
@ -1,17 +1,21 @@
|
||||
[versions]
|
||||
compose = "1.2.0-rc02"
|
||||
accompanist = "0.24.12-rc"
|
||||
material3 = "1.0.0-alpha13"
|
||||
|
||||
[libraries]
|
||||
activity = "androidx.activity:activity-compose:1.6.0-alpha05"
|
||||
foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
|
||||
animation = { module = "androidx.compose.animation:animation", version.ref = "compose" }
|
||||
animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref="compose" }
|
||||
ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||
ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "compose" }
|
||||
|
||||
material3-core = "androidx.compose.material3:material3:1.0.0-alpha13"
|
||||
material3-core = { module = "androidx.compose.material3:material3", version.ref = "material3" }
|
||||
material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3" }
|
||||
material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.11"
|
||||
material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
|
||||
|
||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
|
||||
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" }
|
||||
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" }
|
||||
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref="accompanist" }
|
Loading…
Reference in New Issue
Block a user