Skip to content

Commit 18a4f04

Browse files
authored
Up Next History - Snapshot and display history entries (#3796)
1 parent fbfe6ac commit 18a4f04

File tree

11 files changed

+554
-0
lines changed

11 files changed

+554
-0
lines changed

modules/features/settings/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,12 @@ dependencies {
8282

8383
debugProdImplementation(libs.compose.ui.tooling)
8484

85+
testImplementation(libs.androidx.arch.core)
8586
testImplementation(libs.coroutines.test)
8687
testImplementation(libs.junit)
8788
testImplementation(libs.mockito.core)
8889
testImplementation(libs.mockito.kotlin)
90+
testImplementation(libs.turbine)
8991

9092
testImplementation(projects.modules.services.sharedtest)
9193
}

modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/SettingsFragmentPage.kt

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import au.com.shiftyjelly.pocketcasts.compose.theme
2626
import au.com.shiftyjelly.pocketcasts.models.to.SignInState
2727
import au.com.shiftyjelly.pocketcasts.settings.about.AboutFragment
2828
import au.com.shiftyjelly.pocketcasts.settings.developer.DeveloperFragment
29+
import au.com.shiftyjelly.pocketcasts.settings.history.HistoryFragment
2930
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingFlow
3031
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingLauncher
3132
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
@@ -117,6 +118,9 @@ fun SettingsFragmentPage(
117118
item {
118119
ImportAndExportOpmlRow(onClick = { openFragment(ExportSettingsFragment()) })
119120
}
121+
item {
122+
RestoreFromLocalHistoryRow(onClick = { openFragment(HistoryFragment()) })
123+
}
120124
item {
121125
AdvancedRow(onClick = { openFragment(AdvancedSettingsFragment()) })
122126
}
@@ -282,6 +286,15 @@ private fun AboutRow(onClick: () -> Unit) {
282286
)
283287
}
284288

289+
@Composable
290+
private fun RestoreFromLocalHistoryRow(onClick: () -> Unit) {
291+
SettingRow(
292+
primaryText = stringResource(LR.string.restore_from_local_history),
293+
icon = painterResource(IR.drawable.ic_history),
294+
modifier = Modifier.rowModifier(onClick),
295+
)
296+
}
297+
285298
@Composable
286299
private fun AdvancedRow(onClick: () -> Unit) {
287300
SettingRow(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package au.com.shiftyjelly.pocketcasts.settings.history
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.ViewGroup
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.platform.LocalContext
9+
import androidx.compose.ui.unit.dp
10+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
11+
import androidx.navigation.NavHostController
12+
import androidx.navigation.compose.NavHost
13+
import androidx.navigation.compose.composable
14+
import androidx.navigation.compose.rememberNavController
15+
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
16+
import au.com.shiftyjelly.pocketcasts.compose.extensions.contentWithoutConsumedInsets
17+
import au.com.shiftyjelly.pocketcasts.preferences.Settings
18+
import au.com.shiftyjelly.pocketcasts.settings.history.upnext.UpNextHistoryPage
19+
import au.com.shiftyjelly.pocketcasts.utils.extensions.pxToDp
20+
import au.com.shiftyjelly.pocketcasts.views.fragments.BaseFragment
21+
import au.com.shiftyjelly.pocketcasts.views.helper.HasBackstack
22+
import dagger.hilt.android.AndroidEntryPoint
23+
import javax.inject.Inject
24+
25+
@AndroidEntryPoint
26+
class HistoryFragment : BaseFragment(), HasBackstack {
27+
28+
@Inject
29+
lateinit var settings: Settings
30+
private lateinit var navController: NavHostController
31+
32+
override fun onCreateView(
33+
inflater: LayoutInflater,
34+
container: ViewGroup?,
35+
savedInstanceState: Bundle?,
36+
) = contentWithoutConsumedInsets {
37+
AppThemeWithBackground(theme.activeTheme) {
38+
val bottomInset = settings.bottomInset.collectAsStateWithLifecycle(0)
39+
val bottomInsetDp = bottomInset.value.pxToDp(LocalContext.current).dp
40+
navController = rememberNavController()
41+
42+
NavHost(
43+
navController = navController,
44+
startDestination = HistoryNavRoutes.History,
45+
modifier = Modifier.fillMaxSize(),
46+
) {
47+
composable(HistoryNavRoutes.History) {
48+
HistoryPage(
49+
onBackClick = {
50+
navController.popBackStack()
51+
onBackClick()
52+
},
53+
onUpNextHistoryClick = {
54+
navController.navigate(HistoryNavRoutes.UpNextHistory)
55+
},
56+
bottomInset = bottomInsetDp,
57+
)
58+
}
59+
composable(HistoryNavRoutes.UpNextHistory) {
60+
UpNextHistoryPage(
61+
onHistoryEntryClick = { date ->
62+
},
63+
onBackClick = navController::popBackStack,
64+
bottomInset = bottomInsetDp,
65+
)
66+
}
67+
}
68+
}
69+
}
70+
71+
@Suppress("DEPRECATION")
72+
private fun onBackClick() {
73+
activity?.onBackPressed()
74+
}
75+
76+
override fun onBackPressed() =
77+
if (navController.currentDestination?.route == HistoryNavRoutes.History) {
78+
super.onBackPressed()
79+
} else {
80+
navController.popBackStack()
81+
}
82+
83+
object HistoryNavRoutes {
84+
const val History = "main"
85+
const val UpNextHistory = "up_next_history"
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package au.com.shiftyjelly.pocketcasts.settings.history
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.lazy.LazyColumn
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.Modifier
7+
import androidx.compose.ui.res.painterResource
8+
import androidx.compose.ui.res.stringResource
9+
import androidx.compose.ui.unit.Dp
10+
import au.com.shiftyjelly.pocketcasts.compose.bars.ThemedTopAppBar
11+
import au.com.shiftyjelly.pocketcasts.compose.components.SettingRow
12+
import au.com.shiftyjelly.pocketcasts.settings.rowModifier
13+
import au.com.shiftyjelly.pocketcasts.images.R as IR
14+
import au.com.shiftyjelly.pocketcasts.localization.R as LR
15+
16+
@Composable
17+
fun HistoryPage(
18+
modifier: Modifier = Modifier,
19+
onBackClick: () -> Unit,
20+
onUpNextHistoryClick: () -> Unit,
21+
bottomInset: Dp,
22+
) {
23+
LazyColumn(
24+
modifier = modifier,
25+
contentPadding = PaddingValues(bottom = bottomInset),
26+
) {
27+
item {
28+
ThemedTopAppBar(
29+
title = stringResource(LR.string.restore_from_local_history),
30+
onNavigationClick = onBackClick,
31+
)
32+
}
33+
item {
34+
UpNextHistoryRow(onClick = onUpNextHistoryClick)
35+
}
36+
}
37+
}
38+
39+
@Composable
40+
private fun UpNextHistoryRow(onClick: () -> Unit) {
41+
SettingRow(
42+
primaryText = stringResource(LR.string.up_next_history),
43+
icon = painterResource(IR.drawable.ic_upnext),
44+
modifier = Modifier.rowModifier(onClick),
45+
)
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package au.com.shiftyjelly.pocketcasts.settings.history.upnext
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.PaddingValues
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.fillMaxHeight
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.lazy.LazyColumn
14+
import androidx.compose.foundation.lazy.items
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.LaunchedEffect
17+
import androidx.compose.runtime.getValue
18+
import androidx.compose.runtime.remember
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.res.pluralStringResource
22+
import androidx.compose.ui.res.stringResource
23+
import androidx.compose.ui.tooling.preview.Preview
24+
import androidx.compose.ui.tooling.preview.PreviewParameter
25+
import androidx.compose.ui.unit.Dp
26+
import androidx.compose.ui.unit.dp
27+
import androidx.hilt.navigation.compose.hiltViewModel
28+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
29+
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
30+
import au.com.shiftyjelly.pocketcasts.compose.Devices
31+
import au.com.shiftyjelly.pocketcasts.compose.bars.ThemedTopAppBar
32+
import au.com.shiftyjelly.pocketcasts.compose.components.SettingsSection
33+
import au.com.shiftyjelly.pocketcasts.compose.components.TextH40
34+
import au.com.shiftyjelly.pocketcasts.compose.components.TextH50
35+
import au.com.shiftyjelly.pocketcasts.compose.components.TextP50
36+
import au.com.shiftyjelly.pocketcasts.compose.loading.LoadingView
37+
import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider
38+
import au.com.shiftyjelly.pocketcasts.models.entity.UpNextHistoryEntry
39+
import au.com.shiftyjelly.pocketcasts.settings.history.upnext.UpNextHistoryViewModel.NavigationState
40+
import au.com.shiftyjelly.pocketcasts.settings.history.upnext.UpNextHistoryViewModel.UiState
41+
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
42+
import java.text.DateFormat
43+
import java.util.Date
44+
import au.com.shiftyjelly.pocketcasts.localization.R as LR
45+
46+
@Composable
47+
fun UpNextHistoryPage(
48+
viewModel: UpNextHistoryViewModel = hiltViewModel(),
49+
onBackClick: () -> Unit,
50+
onHistoryEntryClick: (Date) -> Unit,
51+
bottomInset: Dp,
52+
) {
53+
val state by viewModel.uiState.collectAsStateWithLifecycle()
54+
UpNextHistoryPageView(
55+
state = state,
56+
onBackClick = onBackClick,
57+
onHistoryEntryClick = { viewModel.onHistoryEntryClick(it) },
58+
bottomInset = bottomInset,
59+
)
60+
61+
LaunchedEffect(Unit) {
62+
viewModel.navigationState.collect { navigationState ->
63+
when (navigationState) {
64+
is NavigationState.ShowHistoryDetails -> onHistoryEntryClick(navigationState.date)
65+
}
66+
}
67+
}
68+
}
69+
70+
@Composable
71+
private fun UpNextHistoryPageView(
72+
state: UiState,
73+
onBackClick: () -> Unit,
74+
onHistoryEntryClick: (UpNextHistoryEntry) -> Unit,
75+
bottomInset: Dp,
76+
) {
77+
Column {
78+
ThemedTopAppBar(
79+
title = stringResource(LR.string.up_next_history),
80+
bottomShadow = true,
81+
onNavigationClick = onBackClick,
82+
)
83+
84+
when (state) {
85+
is UiState.Loading -> LoadingView()
86+
is UiState.Loaded -> {
87+
UpNextHistoryEntries(
88+
historyEntries = state.entries,
89+
onHistoryEntryClick = onHistoryEntryClick,
90+
bottomInset = bottomInset,
91+
)
92+
}
93+
94+
is UiState.Error -> UpNextHistoryErrorView()
95+
}
96+
}
97+
}
98+
99+
@Composable
100+
private fun UpNextHistoryEntries(
101+
historyEntries: List<UpNextHistoryEntry>,
102+
onHistoryEntryClick: (UpNextHistoryEntry) -> Unit,
103+
bottomInset: Dp,
104+
modifier: Modifier = Modifier,
105+
) {
106+
LazyColumn(
107+
modifier.fillMaxHeight(),
108+
contentPadding = PaddingValues(bottom = bottomInset),
109+
) {
110+
item {
111+
TextP50(
112+
text = stringResource(LR.string.up_next_history_explanation),
113+
modifier = Modifier.padding(SettingsSection.horizontalPadding),
114+
)
115+
}
116+
items(historyEntries) { entry ->
117+
HistoryEntryRow(
118+
entry = entry,
119+
onClick = { onHistoryEntryClick(entry) },
120+
)
121+
}
122+
}
123+
}
124+
125+
@Composable
126+
private fun HistoryEntryRow(
127+
entry: UpNextHistoryEntry,
128+
onClick: () -> Unit,
129+
modifier: Modifier = Modifier,
130+
) {
131+
val episodesCount = pluralStringResource(
132+
id = LR.plurals.episode_count,
133+
count = entry.episodeCount,
134+
entry.episodeCount,
135+
)
136+
Row(
137+
modifier = modifier
138+
.fillMaxWidth()
139+
.clickable { onClick() }
140+
.padding(horizontal = SettingsSection.horizontalPadding, vertical = SettingsSection.verticalPadding),
141+
horizontalArrangement = Arrangement.SpaceBetween,
142+
verticalAlignment = Alignment.CenterVertically,
143+
) {
144+
TextH40(
145+
text = "${formatDate(entry.date)} $episodesCount",
146+
)
147+
}
148+
}
149+
150+
@Composable
151+
private fun UpNextHistoryErrorView() {
152+
Box(
153+
modifier = Modifier.fillMaxSize(),
154+
contentAlignment = Alignment.Center,
155+
) {
156+
TextH50(
157+
text = stringResource(LR.string.error_generic_message),
158+
modifier = Modifier.padding(horizontal = 16.dp),
159+
)
160+
}
161+
}
162+
163+
@Composable
164+
private fun formatDate(date: Date) = remember(date) {
165+
val dateFormat = DateFormat.getDateTimeInstance(
166+
DateFormat.MEDIUM,
167+
DateFormat.SHORT,
168+
)
169+
dateFormat.format(date)
170+
}
171+
172+
@Preview(device = Devices.PortraitRegular)
173+
@Composable
174+
private fun UpNextHistoryPageViewPreview(
175+
@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
176+
) {
177+
AppThemeWithBackground(themeType) {
178+
UpNextHistoryPageView(
179+
state = UiState.Loaded(
180+
entries = listOf(
181+
UpNextHistoryEntry(
182+
date = Date(),
183+
episodeCount = 5,
184+
),
185+
UpNextHistoryEntry(
186+
date = Date(),
187+
episodeCount = 3,
188+
),
189+
),
190+
),
191+
onBackClick = {},
192+
onHistoryEntryClick = {},
193+
bottomInset = 0.dp,
194+
)
195+
}
196+
}
197+
198+
@Preview(device = Devices.PortraitRegular)
199+
@Composable
200+
private fun UpNextHistoryErrorViewPreview(
201+
@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
202+
) {
203+
AppThemeWithBackground(themeType) {
204+
UpNextHistoryPageView(
205+
state = UiState.Error,
206+
onHistoryEntryClick = {},
207+
onBackClick = {},
208+
bottomInset = 0.dp,
209+
)
210+
}
211+
}

0 commit comments

Comments
 (0)