Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature: Introduce Upcoming page to Mihon #420

Merged
merged 41 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3e762e5
Work in progress upcoming feature
sirlag Feb 16, 2024
980585c
Checkpointing WIP upcoming feature
sirlag Feb 18, 2024
c813861
Functional Upcoming Screen
sirlag Feb 18, 2024
9d26a10
Rename UpdateCalendar to UpdateUpcoming
sirlag Feb 18, 2024
d210b07
Converted Strings to resources
sirlag Feb 18, 2024
0096618
Cleanup
sirlag Feb 18, 2024
e78ab8e
Fixed detekt issues
sirlag Feb 18, 2024
c8c3739
Removed Link icon per @AntsyLich's suggestion.
sirlag Feb 18, 2024
27cad52
Detekt
sirlag Feb 18, 2024
361bc4f
Fixed Calendar display on wide form factor devices
sirlag Feb 18, 2024
30ccce4
Added Key to upcoming lazycolumn
sirlag Feb 18, 2024
3789e13
Updated tablet mode UI to support two column view
sirlag Feb 19, 2024
20a7db6
Updated header creation logic
sirlag Feb 24, 2024
c561f9d
Updated header creation logic... again
sirlag Feb 24, 2024
1de161b
Moved stray string to resources
sirlag Mar 22, 2024
2e800f9
Fixed PR Comments and query refactor
sirlag Mar 22, 2024
e392d10
Merge branch 'main' into update-calendar
sirlag Mar 22, 2024
41c04f9
Tweaks to query, refactored to flow, comments on calendar
sirlag Mar 23, 2024
376dcdc
Merge branch 'update-calendar' of https://github.com/sirlag/mihon int…
sirlag Mar 23, 2024
8b7b53b
Switched to Date Formatter
sirlag Mar 23, 2024
61afdbb
Cleaned up date formatter
sirlag Mar 23, 2024
b8e48f5
More Refactor work
sirlag Mar 23, 2024
ae6d535
Updated Calendar to support localized week formats
sirlag Mar 25, 2024
8a63325
Fixed year format
sirlag Mar 25, 2024
22a0270
Refactored Header animation
sirlag Mar 26, 2024
9e245f1
Moved upcoming FAQ
sirlag Mar 26, 2024
acaa12a
Completed YearMonth Migration
sirlag Mar 26, 2024
272d3af
Replaced currentYearMonth with delegate
sirlag Mar 26, 2024
c47829c
Even more cleanup
sirlag Mar 26, 2024
254d800
cleaned up alignment modifiers
sirlag Mar 26, 2024
7a543e5
Click Handler and other refactors
sirlag Mar 26, 2024
d4a8c23
Removed Wrapped Content Height/Size/extra clips
sirlag Mar 26, 2024
202e156
Huge Refactor for CalendarDay
sirlag Mar 26, 2024
6003b9c
Merge remote-tracking branch 'origin/update-calendar' into update-cal…
sirlag Mar 26, 2024
248eeb8
Another cleanup attempt
sirlag Mar 26, 2024
08f55d2
Migrated to new mihon.feature.* module pattern
sirlag Mar 27, 2024
366a22e
Merge branch 'main' into update-calendar
sirlag Mar 27, 2024
38770d2
changed access modifier
sirlag Mar 27, 2024
1024cbd
A Bunch of changes from the next round of reviews
sirlag Mar 28, 2024
1b408b1
Cleanups
AntsyLich Mar 28, 2024
3ca646f
Cleanup 2
AntsyLich Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/java/eu/kanade/domain/DomainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.GetUpcomingManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
Expand Down Expand Up @@ -111,6 +112,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) }
addFactory { GetUpcomingManga(get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { FetchInterval(get()) }
Expand Down
58 changes: 58 additions & 0 deletions app/src/main/java/eu/kanade/presentation/updates/UpcomingItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package eu.kanade.presentation.updates

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.presentation.core.components.material.padding

private val UpcomingItemHeight = 96.dp

@Composable
fun UpcomingItem(
upcoming: Manga,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.clickable(onClick = onClick)
.height(UpcomingItemHeight)
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Book(
modifier = Modifier.fillMaxHeight(),
data = upcoming.asMangaCover(),
)

sirlag marked this conversation as resolved.
Show resolved Hide resolved
Text(
modifier = Modifier
.weight(1f)
.padding(
start = MaterialTheme.padding.large,
end = MaterialTheme.padding.small,
),
text = upcoming.title,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
package eu.kanade.presentation.updates

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
import androidx.compose.foundation.lazy.rememberLazyListState
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.relativeDateText
import eu.kanade.presentation.updates.components.calendar.Calendar
import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.ui.updates.UpdateUpcomingScreenModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import java.time.LocalDate

@Composable
fun UpdateUpcomingScreen(
onClickUpcoming: (manga: Manga) -> Unit,
state: UpdateUpcomingScreenModel.State,
modifier: Modifier = Modifier,
AntsyLich marked this conversation as resolved.
Show resolved Hide resolved
) {
if (isTabletUi()) {
UpdateUpcomingScreenLargeImpl(
state = state,
modifier = modifier,
onClickUpcoming = onClickUpcoming,
)
} else {
UpdateUpcomingScreenSmallImpl(
state = state,
modifier = modifier,
onClickUpcoming = onClickUpcoming,
)
}
}

@Composable
internal fun UpdateUpcomingScreenSmallImpl(
onClickUpcoming: (manga: Manga) -> Unit,
state: UpdateUpcomingScreenModel.State,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = { UpdateUpcomingToolbar() },
) { paddingValues ->
sirlag marked this conversation as resolved.
Show resolved Hide resolved
UpdateUpcomingSmallContent(
upcoming = state.items,
events = state.events,
contentPadding = paddingValues,
onClickUpcoming = onClickUpcoming,
)
}
}

@Composable
internal fun UpdateUpcomingToolbar(
modifier: Modifier = Modifier,
) {
val navigator = LocalNavigator.currentOrThrow
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) }) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
contentDescription = stringResource(MR.strings.upcoming_guide),
)
}
},
)
}
}

@Composable
internal fun UpdateUpcomingSmallContent(
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,
state = listState,
modifier = modifier,
) {
item(
key = "upcoming-calendar",
) {
Calendar(
events = events,
screenWidth = configuration.screenWidthDp.dp,
) { date ->
dateToHeaderMap[date]?.let {
coroutineScope.launch {
listState.animateScrollToItem(it)
}
}
}
}
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.item,
onClick = { onClickUpcoming(item.item) },
)
}
is UpcomingUIModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
text = relativeDateText(item.date),
)
}
}
}
}
}

@Composable
internal fun UpdateUpcomingScreenLargeImpl(
state: UpdateUpcomingScreenModel.State,
modifier: Modifier = Modifier,
onClickUpcoming: (manga: Manga) -> Unit = {},
) {
val layoutDirection = LocalLayoutDirection.current
val listState = rememberLazyListState()

Scaffold(
modifier = modifier,
topBar = { UpdateUpcomingToolbar() },
) { contentPadding ->
TwoPanelBox(
modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
),
startContent = {
UpdateUpcomingLargeCalendar(
upcoming = state.items,
listState = listState,
events = state.events,
modifier = Modifier.padding(contentPadding),
)
},
endContent = {
UpdateUpcomingLargeContent(
upcoming = state.items,
listState = listState,
contentPadding = contentPadding,
onClickUpcoming = onClickUpcoming,
)
},
)
}
}

@Composable
internal fun UpdateUpcomingLargeCalendar(
upcoming: ImmutableList<UpcomingUIModel>,
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)
}
}
}
}

@Composable
internal fun UpdateUpcomingLargeContent(
upcoming: ImmutableList<UpcomingUIModel>,
listState: LazyListState,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
onClickUpcoming: (manga: Manga) -> Unit,
) {
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.item,
onClick = { onClickUpcoming(item.item) },
)
}
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 item: Manga) : UpcomingUIModel
}
Loading