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

End of Year: Add stories datasource + gestures #486

Merged
merged 10 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
1 change: 1 addition & 0 deletions modules/features/endofyear/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ dependencies {
implementation project(':modules:services:compose')
implementation project(':modules:services:localization')
implementation project(':modules:services:ui')
implementation project(':modules:services:utils')
implementation project(':modules:services:views')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package au.com.shiftyjelly.pocketcasts.endofyear

import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story
import kotlinx.coroutines.flow.Flow

interface StoriesDataSource {
var stories: List<Story>
val totalLengthInMs
get() = stories.sumOf { it.storyLength }

suspend fun loadStories(): Flow<List<Story>>
fun storyAt(index: Int): Story?
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.ui.R
import au.com.shiftyjelly.pocketcasts.ui.helper.StatusBarColor
import au.com.shiftyjelly.pocketcasts.views.fragments.BaseDialogFragment

class StoriesFragment : BaseDialogFragment() {
private val viewModel: StoriesViewModel by viewModels()
override val statusBarColor: StatusBarColor
get() = StatusBarColor.Custom(Color.BLACK, true)

Expand All @@ -31,6 +33,7 @@ class StoriesFragment : BaseDialogFragment() {
AppTheme(theme.activeTheme) {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
StoriesScreen(
viewModel = viewModel,
onCloseClicked = { dismiss() },
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package au.com.shiftyjelly.pocketcasts.endofyear

import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.annotation.FloatRange
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -14,68 +13,125 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.compose.bars.NavigationButton
import au.com.shiftyjelly.pocketcasts.compose.buttons.RowOutlinedButton
import au.com.shiftyjelly.pocketcasts.compose.components.TextP50
import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State
import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story
import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake1
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.localization.R as LR

private const val ProgressDurationMs = 5_000
private val ShareButtonStrokeWidth = 2.dp
private val StoryViewCornerSize = 10.dp
private const val NumberOfSegments = 2

@Composable
fun StoriesScreen(
viewModel: StoriesViewModel,
onCloseClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
var running by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = ProgressDurationMs,
easing = LinearEasing
val state: State by viewModel.state.collectAsState()
val progress: Float by viewModel.progress.collectAsState()
when (state) {
is State.Loaded -> StoriesView(
state = state as State.Loaded,
progress = progress,
onSkipPrevious = { viewModel.skipPrevious() },
onSkipNext = { viewModel.skipNext() },
onPause = { viewModel.pause() },
onStart = { viewModel.start() },
onCloseClicked = onCloseClicked
)
)
Box(modifier = modifier.background(color = Color.Black)) {
StoryView(color = Color.Gray)
State.Loading -> StoriesLoadingView(onCloseClicked)
State.Error -> StoriesErrorView(onCloseClicked)
}
}

@Composable
private fun StoriesView(
state: State.Loaded,
@FloatRange(from = 0.0, to = 1.0) progress: Float,
onSkipPrevious: () -> Unit,
onSkipNext: () -> Unit,
onPause: () -> Unit,
onStart: () -> Unit,
onCloseClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
var screenWidth by remember { mutableStateOf(1) }
var isPaused by remember { mutableStateOf(false) }
Box(
modifier = modifier
.fillMaxSize()
.background(color = Color.Black)
.onGloballyPositioned {
screenWidth = it.size.width
}
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (!isPaused) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to find a way to trigger onTap while onPause was true. This obviously isn't doing any harm, but maybe with the press and long press handling we don't need to track the onPause state manually anymore? Or maybe I just didn't find the right way to trigger it. πŸ™‚

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onPress gets triggered for both onTap and onLongPress and I want to re-start the timer only when onLongPress is triggered which is why I'm manually tracking onPause state. I'll see if I can improve this behavior as I noted here.

Thank you for finding all these discussion points!

if (it.x > screenWidth / 2) {
onSkipNext()
} else {
onSkipPrevious()
}
}
},
onLongPress = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used onLongPress tap gesture to pause the story. It takes sometime for the story to pause while the tap qualifies as a long press tap. I can improve this behavior later.

isPaused = true
onPause()
},
onPress = {
awaitRelease()
if (isPaused) {
onStart()
isPaused = false
}
}
)
}
) {
state.currentStory?.let {
StoryView(it)
}
SegmentedProgressIndicator(
progress = progress,
numberOfSegments = NumberOfSegments,
numberOfSegments = state.numberOfStories,
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
)
CloseButtonView(onCloseClicked)
}

LaunchedEffect(Unit) {
running = true
}
}

@Composable
private fun StoryView(
color: Color,
story: Story,
modifier: Modifier = Modifier,
) {
Column {
Expand All @@ -84,7 +140,7 @@ private fun StoryView(
.fillMaxSize()
.weight(weight = 1f, fill = true)
.clip(RoundedCornerShape(StoryViewCornerSize))
.background(color = color)
.background(color = story.backgroundColor)
) {}
ShareButton()
}
Expand Down Expand Up @@ -128,13 +184,93 @@ private fun CloseButtonView(
}
}

@Composable
private fun StoriesLoadingView(
onCloseClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
StoriesEmptyView(
content = { CircularProgressIndicator(color = Color.White) },
onCloseClicked = onCloseClicked,
modifier = modifier
)
}

@Composable
private fun StoriesErrorView(
onCloseClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
StoriesEmptyView(
content = {
TextP50(
text = "Failed to load stories.", // TODO: replace hardcoded text
ashiagr marked this conversation as resolved.
Show resolved Hide resolved
color = Color.White,
)
},
onCloseClicked = onCloseClicked,
modifier = modifier
)
}

@Composable
private fun StoriesEmptyView(
onCloseClicked: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxSize()
.background(color = Color.Black)
) {
CloseButtonView(onCloseClicked)
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize()
) {
content()
}
}
}

@Preview(showBackground = true)
@Composable
private fun StoriesScreenPreview(
@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
) {
AppTheme(themeType) {
StoriesScreen(
StoriesView(
state = State.Loaded(currentStory = StoryFake1(), numberOfStories = 2),
progress = 0.5f,
onSkipPrevious = {},
onSkipNext = {},
onPause = {},
onStart = {},
onCloseClicked = {}
)
}
}

@Preview(showBackground = true)
@Composable
private fun StoriesLoadingViewPreview(
@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
) {
AppTheme(themeType) {
StoriesLoadingView(
onCloseClicked = {}
)
}
}

@Preview(showBackground = true)
@Composable
private fun StoriesErrorViewPreview(
@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
) {
AppTheme(themeType) {
StoriesErrorView(
onCloseClicked = {}
)
}
Expand Down
Loading