mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-25 15:41:32 +02:00
Compare commits
2 Commits
366a22e0cc
...
1024cbdc95
Author | SHA1 | Date | |
---|---|---|---|
|
1024cbdc95 | ||
|
38770d2563 |
@@ -32,7 +32,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
import mihon.domain.manga.interactor.GetUpcomingManga
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||
|
@@ -28,7 +28,6 @@ import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.feature.upcoming.components.UpcomingItem
|
||||
import mihon.feature.upcoming.components.calendar.Calendar
|
||||
@@ -44,8 +43,8 @@ import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
fun UpcomingScreenContent(
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
state: UpcomingScreenModel.State,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -69,20 +68,12 @@ fun UpcomingScreenContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingToolbar(
|
||||
private fun UpcomingToolbar(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
UpIcon()
|
||||
}
|
||||
},
|
||||
title = { AppBarTitle(stringResource(MR.strings.label_upcoming)) },
|
||||
actions = {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) }) {
|
||||
@@ -92,63 +83,47 @@ internal fun UpcomingToolbar(
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
UpIcon()
|
||||
}
|
||||
},
|
||||
title = { AppBarTitle(stringResource(MR.strings.label_upcoming)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingScreenSmallImpl(
|
||||
private fun UpcomingScreenSmallImpl(
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
state: UpcomingScreenModel.State,
|
||||
paddingValues: PaddingValues,
|
||||
) {
|
||||
UpcomingSmallContent(
|
||||
upcoming = state.items,
|
||||
events = state.events,
|
||||
contentPadding = paddingValues,
|
||||
onClickUpcoming = onClickUpcoming,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingSmallContent(
|
||||
contentPadding: PaddingValues,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
upcoming: ImmutableList<UpcomingUIModel>,
|
||||
modifier: Modifier = Modifier,
|
||||
events: ImmutableMap<LocalDate, Int> = persistentMapOf(),
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val dateToHeaderMap =
|
||||
upcoming.withIndex()
|
||||
.filter { it.value is UpcomingUIModel.Header }
|
||||
.associate { Pair((it.value as UpcomingUIModel.Header).date, it.index + 1) } // Offset 1 for Calendar
|
||||
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
contentPadding = paddingValues,
|
||||
state = listState,
|
||||
modifier = modifier,
|
||||
) {
|
||||
item(
|
||||
key = "upcoming-calendar",
|
||||
) {
|
||||
Calendar(
|
||||
events = events,
|
||||
events = state.events,
|
||||
screenWidth = configuration.screenWidthDp.dp,
|
||||
) { date ->
|
||||
dateToHeaderMap[date]?.let {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(it)
|
||||
onClickDay = { date ->
|
||||
state.headerIndexes[date]?.let<Int, Unit> {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(it + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
items(
|
||||
items = upcoming,
|
||||
items = state.items,
|
||||
key = { "upcoming-${it.hashCode()}" },
|
||||
contentType = {
|
||||
when (it) {
|
||||
@@ -160,10 +135,11 @@ internal fun UpcomingSmallContent(
|
||||
when (item) {
|
||||
is UpcomingUIModel.Item -> {
|
||||
UpcomingItem(
|
||||
upcoming = item.item,
|
||||
onClick = { onClickUpcoming(item.item) },
|
||||
upcoming = item.manga,
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
@@ -176,10 +152,10 @@ internal fun UpcomingSmallContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingScreenLargeImpl(
|
||||
private fun UpcomingScreenLargeImpl(
|
||||
state: UpcomingScreenModel.State,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
paddingValues: PaddingValues,
|
||||
state: UpcomingScreenModel.State,
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val listState = rememberLazyListState()
|
||||
@@ -191,9 +167,9 @@ internal fun UpcomingScreenLargeImpl(
|
||||
),
|
||||
startContent = {
|
||||
UpcomingLargeCalendar(
|
||||
upcoming = state.items,
|
||||
listState = listState,
|
||||
events = state.events,
|
||||
headerIndexes = state.headerIndexes,
|
||||
listState = listState,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
},
|
||||
@@ -209,40 +185,36 @@ internal fun UpcomingScreenLargeImpl(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingLargeCalendar(
|
||||
upcoming: ImmutableList<UpcomingUIModel>,
|
||||
private fun UpcomingLargeCalendar(
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
headerIndexes: ImmutableMap<LocalDate, Int>,
|
||||
listState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
events: ImmutableMap<LocalDate, Int> = persistentMapOf(),
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val dateToHeaderMap =
|
||||
upcoming.withIndex()
|
||||
.filter { it.value is UpcomingUIModel.Header }
|
||||
.associate { Pair((it.value as UpcomingUIModel.Header).date, it.index) }
|
||||
|
||||
Calendar(
|
||||
modifier = modifier,
|
||||
events = events,
|
||||
screenWidth = configuration.screenWidthDp.dp,
|
||||
) { date ->
|
||||
dateToHeaderMap[date]?.let {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(it)
|
||||
onClickDay = { date ->
|
||||
headerIndexes[date]?.let {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingLargeContent(
|
||||
private fun UpcomingLargeContent(
|
||||
upcoming: ImmutableList<UpcomingUIModel>,
|
||||
listState: LazyListState,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -262,10 +234,11 @@ internal fun UpcomingLargeContent(
|
||||
when (item) {
|
||||
is UpcomingUIModel.Item -> {
|
||||
UpcomingItem(
|
||||
upcoming = item.item,
|
||||
onClick = { onClickUpcoming(item.item) },
|
||||
upcoming = item.manga,
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
@@ -279,5 +252,5 @@ internal fun UpcomingLargeContent(
|
||||
|
||||
sealed interface UpcomingUIModel {
|
||||
data class Header(val date: LocalDate) : UpcomingUIModel
|
||||
data class Item(val item: Manga) : UpcomingUIModel
|
||||
data class Item(val manga: Manga) : UpcomingUIModel
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.domain.manga.interactor.GetUpcomingManga
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -27,9 +27,11 @@ class UpcomingScreenModel(
|
||||
screenModelScope.launch {
|
||||
getUpcomingManga.subscribe().collectLatest {
|
||||
mutableState.update { state ->
|
||||
val upcomingItems = it.toUpcomingUIModels()
|
||||
state.copy(
|
||||
items = it.toUpcomingUIModels(),
|
||||
items = upcomingItems,
|
||||
events = it.toEvents(),
|
||||
headerIndexes = getHeaderIndexes(upcomingItems),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -39,8 +41,8 @@ class UpcomingScreenModel(
|
||||
private fun List<Manga>.toUpcomingUIModels(): ImmutableList<UpcomingUIModel> {
|
||||
return map { UpcomingUIModel.Item(it) }
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.expectedNextUpdate?.toLocalDate()
|
||||
val afterDate = after?.item?.expectedNextUpdate?.toLocalDate()
|
||||
val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate()
|
||||
val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate()
|
||||
when {
|
||||
beforeDate != afterDate && afterDate != null -> UpcomingUIModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
@@ -56,9 +58,16 @@ class UpcomingScreenModel(
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
private fun getHeaderIndexes(upcomingItems: List<UpcomingUIModel>): ImmutableMap<LocalDate, Int> {
|
||||
return upcomingItems.withIndex()
|
||||
.filter { it.value is UpcomingUIModel.Header }
|
||||
.associate { Pair((it.value as UpcomingUIModel.Header).date, it.index) }
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
data class State(
|
||||
val items: ImmutableList<UpcomingUIModel> = persistentListOf(),
|
||||
val events: ImmutableMap<LocalDate, Int> = persistentMapOf(),
|
||||
val headerIndexes: Map<LocalDate, Int> = persistentMapOf(),
|
||||
val headerIndexes: ImmutableMap<LocalDate, Int> = persistentMapOf(),
|
||||
)
|
||||
}
|
||||
|
@@ -24,7 +24,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
@@ -58,13 +57,6 @@ fun Calendar(
|
||||
var currentYearMonth by remember { mutableStateOf(YearMonth.now()) }
|
||||
val isTabletUi = isTabletUi()
|
||||
|
||||
val localFirstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value
|
||||
val weekDays = remember {
|
||||
(0 until DaysOfWeek)
|
||||
.map { DayOfWeek.of(((localFirstDayOfWeek - 1 + it) % DaysOfWeek) + 1) }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
val widthModifier = when {
|
||||
isTabletUi -> 1.0f
|
||||
screenWidth > 840.dp -> ExtendedScale
|
||||
@@ -88,7 +80,6 @@ fun Calendar(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
) {
|
||||
CalendarGrid(
|
||||
weekDays = weekDays,
|
||||
labelFormat = labelFormat,
|
||||
currentYearMonth = currentYearMonth,
|
||||
isTabletUi = isTabletUi,
|
||||
@@ -103,34 +94,35 @@ fun Calendar(
|
||||
|
||||
@Composable
|
||||
private fun CalendarGrid(
|
||||
weekDays: ImmutableList<DayOfWeek>,
|
||||
labelFormat: (DayOfWeek) -> String,
|
||||
currentYearMonth: YearMonth,
|
||||
isTabletUi: Boolean,
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
onClickDay: (day: LocalDate) -> Unit,
|
||||
widthModifier: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickDay: (day: LocalDate) -> Unit = {},
|
||||
widthModifier: Float = 1.0F,
|
||||
) {
|
||||
val daysInMonth = currentYearMonth.lengthOfMonth()
|
||||
val startDayOfMonth = currentYearMonth.atDay(1)
|
||||
val firstDayOfMonth = startDayOfMonth.dayOfWeek
|
||||
|
||||
// The lower bound for Calendar Days, between -5 and 1 to provide cell offset
|
||||
val dayEntries = (-weekDays.indexOf(firstDayOfMonth) + 1..daysInMonth).toImmutableList()
|
||||
val height = (((dayEntries.size - 1) / DaysOfWeek + ceil(1.0f - widthModifier)) * HeightMultiplier).dp
|
||||
|
||||
val modeModifier = if (isTabletUi) {
|
||||
modifier
|
||||
.fillMaxWidth(widthModifier)
|
||||
} else {
|
||||
modifier
|
||||
.fillMaxWidth(widthModifier)
|
||||
.height(height)
|
||||
val localeFirstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value
|
||||
val weekDays = remember {
|
||||
(0 until DaysOfWeek)
|
||||
.map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DaysOfWeek + 1) }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
val emptyFieldCount = weekDays.indexOf(currentYearMonth.atDay(1).dayOfWeek)
|
||||
val daysInMonth = currentYearMonth.lengthOfMonth()
|
||||
|
||||
val height = (((emptyFieldCount + daysInMonth) / DaysOfWeek + ceil(1.0f - widthModifier)) * HeightMultiplier).dp
|
||||
|
||||
LazyVerticalGrid(
|
||||
modifier = modeModifier,
|
||||
modifier = if (isTabletUi) {
|
||||
modifier
|
||||
.fillMaxWidth(widthModifier)
|
||||
} else {
|
||||
modifier
|
||||
.fillMaxWidth(widthModifier)
|
||||
.height(height)
|
||||
},
|
||||
columns = GridCells.Fixed(DaysOfWeek),
|
||||
) {
|
||||
items(weekDays) { item ->
|
||||
@@ -141,15 +133,14 @@ private fun CalendarGrid(
|
||||
fontSize = FontSize,
|
||||
)
|
||||
}
|
||||
items(dayEntries) {
|
||||
if (it > 0) {
|
||||
val localDate = currentYearMonth.atDay(it)
|
||||
CalendarDay(
|
||||
date = localDate,
|
||||
onDayClick = { onClickDay(localDate) },
|
||||
events = events[localDate] ?: 0,
|
||||
)
|
||||
}
|
||||
items(emptyFieldCount) {}
|
||||
items(daysInMonth) {
|
||||
val localDate = currentYearMonth.atDay(it + 1)
|
||||
CalendarDay(
|
||||
date = localDate,
|
||||
onDayClick = { onClickDay(localDate) },
|
||||
events = events[localDate] ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -43,7 +43,10 @@ fun CalenderHeader(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -87,12 +90,6 @@ private fun AnimatedContentTransitionScope<YearMonth>.getAnimation(): ContentTra
|
||||
.using(SizeTransform(clip = false))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatted title text for the Calendar header.
|
||||
*
|
||||
* @param monthYear The current month and year pair.
|
||||
* @return The formatted title text.
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getTitleText(monthYear: YearMonth): String {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package mihon.domain.manga.interactor
|
||||
package mihon.domain.upcoming.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.coroutines.flow.Flow
|
Reference in New Issue
Block a user