mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-25 23:51:32 +02:00
Compare commits
2 Commits
1024cbdc95
...
3ca646fc13
Author | SHA1 | Date | |
---|---|---|---|
|
3ca646fc13 | ||
|
1b408b18e3 |
@@ -237,6 +237,7 @@ dependencies {
|
||||
implementation(libs.compose.materialmotion)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
|
||||
|
||||
// Logging
|
||||
|
@@ -0,0 +1,23 @@
|
||||
package mihon.core.designsystem.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun isMediumWidthWindow(): Boolean {
|
||||
val configuration = LocalConfiguration.current
|
||||
return configuration.screenWidthDp > MediumWidthWindowSize.value
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun isExpandedWidthWindow(): Boolean {
|
||||
val configuration = LocalConfiguration.current
|
||||
return configuration.screenWidthDp > ExpandedWidthWindowSize.value
|
||||
}
|
||||
|
||||
val MediumWidthWindowSize = 600.dp
|
||||
val ExpandedWidthWindowSize = 840.dp
|
@@ -20,6 +20,7 @@ class UpcomingScreen : Screen() {
|
||||
|
||||
UpcomingScreenContent(
|
||||
state = state,
|
||||
setSelectedYearMonth = screenModel::setSelectedYearMonth,
|
||||
onClickUpcoming = { navigator.push(MangaScreen(it.id)) },
|
||||
)
|
||||
}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -12,18 +9,13 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.components.UpIcon
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -40,90 +32,98 @@ import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
|
||||
@Composable
|
||||
fun UpcomingScreenContent(
|
||||
state: UpcomingScreenModel.State,
|
||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
val onClickDay: (LocalDate, Int) -> Unit = { date, offset ->
|
||||
state.headerIndexes[date]?.let {
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(it + offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
topBar = { UpcomingToolbar() },
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
if (isTabletUi()) {
|
||||
UpcomingScreenLargeImpl(
|
||||
onClickUpcoming = onClickUpcoming,
|
||||
listState = listState,
|
||||
items = state.items,
|
||||
events = state.events,
|
||||
paddingValues = paddingValues,
|
||||
state = state,
|
||||
selectedYearMonth = state.selectedYearMonth,
|
||||
setSelectedYearMonth = setSelectedYearMonth,
|
||||
onClickDay = { onClickDay(it, 0) },
|
||||
onClickUpcoming = onClickUpcoming,
|
||||
)
|
||||
} else {
|
||||
UpcomingScreenSmallImpl(
|
||||
onClickUpcoming = onClickUpcoming,
|
||||
listState = listState,
|
||||
items = state.items,
|
||||
events = state.events,
|
||||
paddingValues = paddingValues,
|
||||
state = state,
|
||||
selectedYearMonth = state.selectedYearMonth,
|
||||
setSelectedYearMonth = setSelectedYearMonth,
|
||||
onClickDay = { onClickDay(it, 1) },
|
||||
onClickUpcoming = onClickUpcoming,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpcomingToolbar(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun UpcomingToolbar() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
Column(modifier = modifier) {
|
||||
TopAppBar(
|
||||
actions = {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
contentDescription = stringResource(MR.strings.upcoming_guide),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
UpIcon()
|
||||
}
|
||||
},
|
||||
title = { AppBarTitle(stringResource(MR.strings.label_upcoming)) },
|
||||
)
|
||||
}
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
AppBar(
|
||||
title = stringResource(MR.strings.label_upcoming),
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
contentDescription = stringResource(MR.strings.upcoming_guide),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpcomingScreenSmallImpl(
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
state: UpcomingScreenModel.State,
|
||||
listState: LazyListState,
|
||||
items: ImmutableList<UpcomingUIModel>,
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
paddingValues: PaddingValues,
|
||||
selectedYearMonth: YearMonth,
|
||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||
onClickDay: (LocalDate) -> Unit,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val configuration = LocalConfiguration.current
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
state = listState,
|
||||
) {
|
||||
item(
|
||||
key = "upcoming-calendar",
|
||||
) {
|
||||
item(key = "upcoming-calendar") {
|
||||
Calendar(
|
||||
events = state.events,
|
||||
screenWidth = configuration.screenWidthDp.dp,
|
||||
onClickDay = { date ->
|
||||
state.headerIndexes[date]?.let<Int, Unit> {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(it + 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedYearMonth = selectedYearMonth,
|
||||
events = events,
|
||||
setSelectedYearMonth = setSelectedYearMonth,
|
||||
onClickDay = onClickDay,
|
||||
)
|
||||
}
|
||||
items(
|
||||
items = state.items,
|
||||
items = items,
|
||||
key = { "upcoming-${it.hashCode()}" },
|
||||
contentType = {
|
||||
when (it) {
|
||||
@@ -139,12 +139,8 @@ private fun UpcomingScreenSmallImpl(
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
ListGroupHeader(text = relativeDateText(item.date))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,104 +149,50 @@ private fun UpcomingScreenSmallImpl(
|
||||
|
||||
@Composable
|
||||
private fun UpcomingScreenLargeImpl(
|
||||
state: UpcomingScreenModel.State,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
listState: LazyListState,
|
||||
items: ImmutableList<UpcomingUIModel>,
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
paddingValues: PaddingValues,
|
||||
selectedYearMonth: YearMonth,
|
||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||
onClickDay: (LocalDate) -> Unit,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
TwoPanelBox(
|
||||
modifier = Modifier.padding(
|
||||
start = paddingValues.calculateStartPadding(layoutDirection),
|
||||
end = paddingValues.calculateEndPadding(layoutDirection),
|
||||
),
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
startContent = {
|
||||
UpcomingLargeCalendar(
|
||||
events = state.events,
|
||||
headerIndexes = state.headerIndexes,
|
||||
listState = listState,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
Calendar(
|
||||
selectedYearMonth = selectedYearMonth,
|
||||
events = events,
|
||||
setSelectedYearMonth = setSelectedYearMonth,
|
||||
onClickDay = onClickDay,
|
||||
)
|
||||
},
|
||||
endContent = {
|
||||
UpcomingLargeContent(
|
||||
upcoming = state.items,
|
||||
listState = listState,
|
||||
contentPadding = paddingValues,
|
||||
onClickUpcoming = onClickUpcoming,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpcomingLargeCalendar(
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
headerIndexes: ImmutableMap<LocalDate, Int>,
|
||||
listState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Calendar(
|
||||
modifier = modifier,
|
||||
events = events,
|
||||
screenWidth = configuration.screenWidthDp.dp,
|
||||
onClickDay = { date ->
|
||||
headerIndexes[date]?.let {
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(it)
|
||||
FastScrollLazyColumn(state = listState) {
|
||||
items(
|
||||
items = items,
|
||||
key = { "upcoming-${it.hashCode()}" },
|
||||
contentType = {
|
||||
when (it) {
|
||||
is UpcomingUIModel.Header -> "header"
|
||||
is UpcomingUIModel.Item -> "item"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
when (item) {
|
||||
is UpcomingUIModel.Item -> {
|
||||
UpcomingItem(
|
||||
upcoming = item.manga,
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(text = relativeDateText(item.date))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpcomingLargeContent(
|
||||
upcoming: ImmutableList<UpcomingUIModel>,
|
||||
listState: LazyListState,
|
||||
contentPadding: PaddingValues,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
state = listState,
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
items = upcoming,
|
||||
key = { "upcoming-${it.hashCode()}" },
|
||||
contentType = {
|
||||
when (it) {
|
||||
is UpcomingUIModel.Header -> "header"
|
||||
is UpcomingUIModel.Item -> "item"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
when (item) {
|
||||
is UpcomingUIModel.Item -> {
|
||||
UpcomingItem(
|
||||
upcoming = item.manga,
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface UpcomingUIModel {
|
||||
data class Header(val date: LocalDate) : UpcomingUIModel
|
||||
data class Item(val manga: Manga) : UpcomingUIModel
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.compose.ui.util.fastMapIndexedNotNull
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
@@ -18,6 +20,7 @@ import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
|
||||
class UpcomingScreenModel(
|
||||
private val getUpcomingManga: GetUpcomingManga = Injekt.get(),
|
||||
@@ -31,7 +34,7 @@ class UpcomingScreenModel(
|
||||
state.copy(
|
||||
items = upcomingItems,
|
||||
events = it.toEvents(),
|
||||
headerIndexes = getHeaderIndexes(upcomingItems),
|
||||
headerIndexes = upcomingItems.getHeaderIndexes(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -39,14 +42,15 @@ class UpcomingScreenModel(
|
||||
}
|
||||
|
||||
private fun List<Manga>.toUpcomingUIModels(): ImmutableList<UpcomingUIModel> {
|
||||
return map { UpcomingUIModel.Item(it) }
|
||||
return fastMap { UpcomingUIModel.Item(it) }
|
||||
.insertSeparators { before, after ->
|
||||
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.
|
||||
else -> null
|
||||
|
||||
if (beforeDate != afterDate && afterDate != null) {
|
||||
UpcomingUIModel.Header(afterDate)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
@@ -58,14 +62,24 @@ 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) }
|
||||
private fun List<UpcomingUIModel>.getHeaderIndexes(): ImmutableMap<LocalDate, Int> {
|
||||
return fastMapIndexedNotNull { index, upcomingUIModel ->
|
||||
if (upcomingUIModel is UpcomingUIModel.Header) {
|
||||
upcomingUIModel.date to index
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
fun setSelectedYearMonth(yearMonth: YearMonth) {
|
||||
mutableState.update { it.copy(selectedYearMonth = yearMonth) }
|
||||
}
|
||||
|
||||
data class State(
|
||||
val selectedYearMonth: YearMonth = YearMonth.now(),
|
||||
val items: ImmutableList<UpcomingUIModel> = persistentListOf(),
|
||||
val events: ImmutableMap<LocalDate, Int> = persistentMapOf(),
|
||||
val headerIndexes: ImmutableMap<LocalDate, Int> = persistentMapOf(),
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import java.time.LocalDate
|
||||
|
||||
sealed interface UpcomingUIModel {
|
||||
data class Header(val date: LocalDate) : UpcomingUIModel
|
||||
data class Item(val manga: Manga) : UpcomingUIModel
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
package mihon.feature.upcoming.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -35,18 +36,14 @@ fun UpcomingItem(
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large),
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
data = upcoming.asMangaCover(),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.large,
|
||||
end = MaterialTheme.padding.small,
|
||||
),
|
||||
modifier = Modifier.weight(1f),
|
||||
text = upcoming.title,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 2,
|
||||
|
@@ -1,31 +1,28 @@
|
||||
package mihon.feature.upcoming.components.calendar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 androidx.compose.ui.util.fastForEach
|
||||
import io.woong.compose.grid.SimpleGridCells
|
||||
import io.woong.compose.grid.VerticalGrid
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import mihon.core.designsystem.utils.isExpandedWidthWindow
|
||||
import mihon.core.designsystem.utils.isMediumWidthWindow
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
@@ -33,74 +30,45 @@ import java.time.YearMonth
|
||||
import java.time.format.TextStyle
|
||||
import java.time.temporal.WeekFields
|
||||
import java.util.Locale
|
||||
import kotlin.math.ceil
|
||||
|
||||
private val FontSize = 16.sp
|
||||
private const val ExtendedScale = 0.31f
|
||||
private const val MediumScale = 0.60f
|
||||
private const val HeightMultiplier = 68
|
||||
private const val DaysOfWeek = 7
|
||||
|
||||
@Composable
|
||||
fun Calendar(
|
||||
selectedYearMonth: YearMonth,
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
screenWidth: Dp,
|
||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||
onClickDay: (day: LocalDate) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
labelFormat: (DayOfWeek) -> String = {
|
||||
it.getDisplayName(
|
||||
TextStyle.NARROW,
|
||||
Locale.getDefault(),
|
||||
)
|
||||
},
|
||||
onClickDay: (day: LocalDate) -> Unit = {},
|
||||
) {
|
||||
var currentYearMonth by remember { mutableStateOf(YearMonth.now()) }
|
||||
val isTabletUi = isTabletUi()
|
||||
|
||||
val widthModifier = when {
|
||||
isTabletUi -> 1.0f
|
||||
screenWidth > 840.dp -> ExtendedScale
|
||||
screenWidth > 600.dp -> MediumScale
|
||||
else -> 1.0f
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(MaterialTheme.padding.small),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
CalenderHeader(
|
||||
yearMonth = currentYearMonth,
|
||||
onPreviousClick = { currentYearMonth = currentYearMonth.minusMonths(1L) },
|
||||
onNextClick = { currentYearMonth = currentYearMonth.plusMonths(1L) },
|
||||
yearMonth = selectedYearMonth,
|
||||
onPreviousClick = { setSelectedYearMonth(selectedYearMonth.minusMonths(1L)) },
|
||||
onNextClick = { setSelectedYearMonth(selectedYearMonth.plusMonths(1L)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.padding.small)
|
||||
.padding(start = MaterialTheme.padding.medium)
|
||||
)
|
||||
CalendarGrid(
|
||||
selectedYearMonth = selectedYearMonth,
|
||||
events = events,
|
||||
onClickDay = onClickDay,
|
||||
)
|
||||
Spacer(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
) {
|
||||
CalendarGrid(
|
||||
labelFormat = labelFormat,
|
||||
currentYearMonth = currentYearMonth,
|
||||
isTabletUi = isTabletUi,
|
||||
events = events,
|
||||
widthModifier = widthModifier,
|
||||
onClickDay = onClickDay,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CalendarGrid(
|
||||
labelFormat: (DayOfWeek) -> String,
|
||||
currentYearMonth: YearMonth,
|
||||
isTabletUi: Boolean,
|
||||
selectedYearMonth: YearMonth,
|
||||
events: ImmutableMap<LocalDate, Int>,
|
||||
onClickDay: (day: LocalDate) -> Unit,
|
||||
widthModifier: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val localeFirstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value
|
||||
val weekDays = remember {
|
||||
@@ -109,33 +77,31 @@ private fun CalendarGrid(
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
val emptyFieldCount = weekDays.indexOf(currentYearMonth.atDay(1).dayOfWeek)
|
||||
val daysInMonth = currentYearMonth.lengthOfMonth()
|
||||
val emptyFieldCount = weekDays.indexOf(selectedYearMonth.atDay(1).dayOfWeek)
|
||||
val daysInMonth = selectedYearMonth.lengthOfMonth()
|
||||
|
||||
val height = (((emptyFieldCount + daysInMonth) / DaysOfWeek + ceil(1.0f - widthModifier)) * HeightMultiplier).dp
|
||||
|
||||
LazyVerticalGrid(
|
||||
modifier = if (isTabletUi) {
|
||||
modifier
|
||||
.fillMaxWidth(widthModifier)
|
||||
VerticalGrid(
|
||||
columns = SimpleGridCells.Fixed(DaysOfWeek),
|
||||
modifier = if (isMediumWidthWindow() && !isExpandedWidthWindow()) {
|
||||
Modifier.widthIn(max = 360.dp)
|
||||
} else {
|
||||
modifier
|
||||
.fillMaxWidth(widthModifier)
|
||||
.height(height)
|
||||
},
|
||||
columns = GridCells.Fixed(DaysOfWeek),
|
||||
Modifier
|
||||
}
|
||||
) {
|
||||
items(weekDays) { item ->
|
||||
weekDays.fastForEach { item ->
|
||||
Text(
|
||||
text = labelFormat(item),
|
||||
text = item.getDisplayName(
|
||||
TextStyle.NARROW,
|
||||
Locale.getDefault(),
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = FontSize,
|
||||
)
|
||||
}
|
||||
items(emptyFieldCount) {}
|
||||
items(daysInMonth) {
|
||||
val localDate = currentYearMonth.atDay(it + 1)
|
||||
repeat(emptyFieldCount) { Box { } }
|
||||
repeat(daysInMonth) { dayIndex ->
|
||||
val localDate = selectedYearMonth.atDay(dayIndex + 1)
|
||||
CalendarDay(
|
||||
date = localDate,
|
||||
onDayClick = { onClickDay(localDate) },
|
||||
|
@@ -1,12 +1,10 @@
|
||||
package mihon.feature.upcoming.components.calendar
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -16,7 +14,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -40,33 +37,33 @@ fun CalendarDay(
|
||||
.then(
|
||||
if (today == date) {
|
||||
Modifier.border(
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onBackground),
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
),
|
||||
shape = CircleShape,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(Color.Transparent)
|
||||
.clip(shape = CircleShape)
|
||||
.clickable(onClick = onDayClick)
|
||||
.circleLayout()
|
||||
.defaultMinSize(56.dp),
|
||||
.circleLayout(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = date.dayOfMonth.toString(),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 16.sp,
|
||||
color = when {
|
||||
date.isBefore(today) -> MaterialTheme.colorScheme.onBackground.copy(alpha = 0.38f)
|
||||
else -> MaterialTheme.colorScheme.onBackground
|
||||
color = if (date.isBefore(today)) {
|
||||
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.38f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.offset(y = 12.dp),
|
||||
) {
|
||||
Row(Modifier.offset(y = 12.dp)) {
|
||||
val size = events.coerceAtMost(MaxEvents)
|
||||
for (index in 0 until size) {
|
||||
CalendarIndicator(
|
||||
@@ -80,17 +77,16 @@ fun CalendarDay(
|
||||
}
|
||||
|
||||
private fun Modifier.circleLayout() = layout { measurable, constraints ->
|
||||
// Measure the composable
|
||||
val placeable = measurable.measure(constraints)
|
||||
|
||||
// get the current max dimension to assign width=height
|
||||
val currentHeight = placeable.height
|
||||
val currentWidth = placeable.width
|
||||
val newDiameter = maxOf(currentHeight, currentWidth)
|
||||
|
||||
// assign the dimension and the center position
|
||||
layout(newDiameter, newDiameter) {
|
||||
// Where the composable gets placed
|
||||
placeable.placeRelative((newDiameter - currentWidth) / 2, (newDiameter - currentHeight) / 2)
|
||||
placeable.placeRelative(
|
||||
x = (newDiameter - currentWidth) / 2,
|
||||
y = (newDiameter - currentHeight) / 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -12,8 +12,6 @@ import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
@@ -27,7 +25,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -41,12 +38,7 @@ fun CalenderHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
@@ -64,6 +64,7 @@ directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0"
|
||||
compose-webview = "io.github.kevinnzou:compose-webview:0.33.4"
|
||||
compose-grid = "io.woong.compose.grid:grid:1.2.2"
|
||||
|
||||
swipe = "me.saket.swipe:swipe:1.3.0"
|
||||
|
||||
|
Reference in New Issue
Block a user