Skip to content

Commit

Permalink
Merge pull request #495 from Automattic/task/410-create-dummy-stories
Browse files Browse the repository at this point in the history
End of Year: Generate stories with dummy data + support dynamic lengths
  • Loading branch information
ashiagr authored Oct 27, 2022
2 parents 5cd17ce + 8edd34b commit 95a5324
Show file tree
Hide file tree
Showing 23 changed files with 304 additions and 63 deletions.
2 changes: 2 additions & 0 deletions modules/features/endofyear/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies {
// services
implementation project(':modules:services:compose')
implementation project(':modules:services:localization')
implementation project(':modules:services:model')
implementation project(':modules:services:repositories')
implementation project(':modules:services:ui')
implementation project(':modules:services:utils')
implementation project(':modules:services:views')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.dp
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State.Loaded.SegmentsData

private val StrokeWidth = 2.dp
private val GapWidth = 8.dp
private val SegmentHeight = StrokeWidth
private const val IndicatorBackgroundOpacity = 0.24f
private const val IndicatorBackgroundOpacity = 0.5f

@Composable
fun SegmentedProgressIndicator(
@FloatRange(from = 0.0, to = 1.0) progress: Float,
modifier: Modifier = Modifier,
color: Color = Color.White,
backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity),
numberOfSegments: Int,
segmentsData: SegmentsData,
) {
Canvas(
modifier
Expand All @@ -33,20 +33,20 @@ fun SegmentedProgressIndicator(
.height(SegmentHeight)
.focusable()
) {
drawSegmentsBackground(backgroundColor, numberOfSegments)
drawSegments(progress, color, numberOfSegments)
drawSegmentsBackground(backgroundColor, segmentsData)
drawSegments(progress, color, segmentsData)
}
}

private fun DrawScope.drawSegmentsBackground(
color: Color,
numberOfSegments: Int,
) = drawSegments(1f, color, numberOfSegments)
segmentsData: SegmentsData,
) = drawSegments(1f, color, segmentsData)

private fun DrawScope.drawSegments(
endFraction: Float,
color: Color,
numberOfSegments: Int,
segmentsData: SegmentsData,
) {
val width = size.width
val height = size.height
Expand All @@ -55,11 +55,10 @@ private fun DrawScope.drawSegments(

val barEnd = endFraction * width

val segmentWidth = calculateSegmentWidth(numberOfSegments)
val segmentAndGapWidth = segmentWidth + GapWidth.toPx()
repeat(segmentsData.widths.size) { index ->
val segmentWidth = segmentsData.widths[index] * width
val xOffsetStart = segmentsData.xStartOffsets[index] * width

repeat(numberOfSegments) { index ->
val xOffsetStart = index * segmentAndGapWidth
val shouldDrawLine = xOffsetStart < barEnd
if (shouldDrawLine) {
val xOffsetEnd = (xOffsetStart + segmentWidth).coerceAtMost(barEnd)
Expand All @@ -68,11 +67,3 @@ private fun DrawScope.drawSegments(
}
}
}

private fun DrawScope.calculateSegmentWidth(
numberOfSegments: Int,
): Float {
val width = size.width
val gapsWidth = (numberOfSegments - 1) * GapWidth.toPx()
return (width - gapsWidth) / numberOfSegments
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ abstract class StoriesDataSource {
val numOfStories: Int
get() = stories.size
val totalLengthInMs
get() = stories.sumOf { it.storyLength }
get() = storyLengthsInMs.sum() + gapLengthsInMs
val storyLengthsInMs: List<Long>
get() = stories.map { it.storyLength }
private val gapLengthsInMs: Long
get() = STORY_GAP_LENGTH_MS * numOfStories.minus(1).coerceAtLeast(0)

abstract suspend fun loadStories(): Flow<List<Story>>
abstract fun storyAt(index: Int): Story?

companion object {
const val STORY_GAP_LENGTH_MS = 100L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvi
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.endofyear.stories.StoryFake2
import au.com.shiftyjelly.pocketcasts.endofyear.storyviews.StoryFake1View
import au.com.shiftyjelly.pocketcasts.endofyear.storyviews.StoryFake2View
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.localization.R as LR

Expand Down Expand Up @@ -120,7 +124,7 @@ private fun StoriesView(
}
SegmentedProgressIndicator(
progress = progress,
numberOfSegments = state.numberOfStories,
segmentsData = state.segmentsData,
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
Expand All @@ -140,8 +144,14 @@ private fun StoryView(
.fillMaxSize()
.weight(weight = 1f, fill = true)
.clip(RoundedCornerShape(StoryViewCornerSize))
.background(color = story.backgroundColor)
) {}
.background(color = story.backgroundColor),
contentAlignment = Alignment.Center
) {
when (story) {
is StoryFake1 -> StoryFake1View(story)
is StoryFake2 -> StoryFake2View(story)
}
}
ShareButton()
}
}
Expand Down Expand Up @@ -241,8 +251,14 @@ private fun StoriesScreenPreview(
) {
AppTheme(themeType) {
StoriesView(
state = State.Loaded(currentStory = StoryFake1(), numberOfStories = 2),
progress = 0.5f,
state = State.Loaded(
currentStory = StoryFake1(listOf(Podcast())),
segmentsData = State.Loaded.SegmentsData(
xStartOffsets = listOf(0.0f, 0.28f),
widths = listOf(0.25f, 0.75f)
)
),
progress = 0.75f,
onSkipPrevious = {},
onSkipNext = {},
onPause = {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package au.com.shiftyjelly.pocketcasts.endofyear

import androidx.annotation.FloatRange
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State.Loaded.SegmentsData
import au.com.shiftyjelly.pocketcasts.endofyear.stories.Story
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Timer
import javax.inject.Inject
import kotlin.concurrent.fixedRateTimer
Expand Down Expand Up @@ -37,10 +40,15 @@ class StoriesViewModel @Inject constructor(
val state = if (stories.isEmpty()) {
State.Error
} else {
State.Loaded(
currentStory = storiesDataSource.storyAt(currentIndex),
numberOfStories = numOfStories
)
with(storiesDataSource) {
State.Loaded(
currentStory = storyAt(currentIndex),
segmentsData = SegmentsData(
xStartOffsets = List(numOfStories) { getXStartOffsetAtIndex(it) },
widths = storyLengthsInMs.map { it / totalLengthInMs.toFloat() },
)
)
}
}
mutableState.value = state
if (state is State.Loaded) start()
Expand Down Expand Up @@ -85,6 +93,7 @@ class StoriesViewModel @Inject constructor(
private fun skipToStoryAtIndex(index: Int) {
if (timer == null) start()
mutableProgress.value = getXStartOffsetAtIndex(index)
currentIndex = index
mutableState.value =
(state.value as State.Loaded).copy(currentStory = storiesDataSource.storyAt(index))
}
Expand All @@ -101,16 +110,28 @@ class StoriesViewModel @Inject constructor(

private fun Float.roundOff() = (this * 100.0).roundToInt()

private fun getXStartOffsetAtIndex(index: Int) =
(PROGRESS_END_VALUE / numOfStories.toFloat()).coerceAtMost(PROGRESS_END_VALUE) * index
@FloatRange(from = 0.0, to = 1.0)
fun getXStartOffsetAtIndex(index: Int): Float {
val sumOfStoryLengthsTillIndex = try {
storiesDataSource.storyLengthsInMs.subList(0, index).sum()
} catch (e: IndexOutOfBoundsException) {
Timber.e("Story offset checked at invalid index")
0L
}
return (sumOfStoryLengthsTillIndex + StoriesDataSource.STORY_GAP_LENGTH_MS * index) / storiesDataSource.totalLengthInMs.toFloat()
}

sealed class State {
object Loading : State()
data class Loaded(
val currentStory: Story?,
val numberOfStories: Int,
) : State()

val segmentsData: SegmentsData,
) : State() {
data class SegmentsData(
val widths: List<Float> = emptyList(),
val xStartOffsets: List<Float> = emptyList(),
)
}
object Error : State()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package au.com.shiftyjelly.pocketcasts.endofyear.stories

import au.com.shiftyjelly.pocketcasts.endofyear.StoriesDataSource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import au.com.shiftyjelly.pocketcasts.repositories.podcast.EndOfYearManager
import kotlinx.coroutines.flow.combine
import timber.log.Timber
import javax.inject.Inject

class EndOfYearStoriesDataSource @Inject constructor() : StoriesDataSource() {
class EndOfYearStoriesDataSource @Inject constructor(
private val endOfYearManager: EndOfYearManager,
) : StoriesDataSource() {
override val stories = mutableListOf<Story>()

override suspend fun loadStories(): Flow<List<Story>> {
delay(1000L) // TODO: Remove hardcoded delay added for testing
stories.addAll(listOf(StoryFake1(), StoryFake2()))
return flowOf(stories)
}
override suspend fun loadStories() =
combine(
endOfYearManager.findRandomPodcasts(),
endOfYearManager.findRandomEpisode()
) { podcasts, episode ->
if (podcasts.isNotEmpty()) stories.add(StoryFake1(podcasts))
episode?.let { stories.add(StoryFake2(it)) }

stories
}

override fun storyAt(index: Int) = try {
stories[index]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.ui.graphics.Color
import au.com.shiftyjelly.pocketcasts.utils.seconds

abstract class Story {
val storyLength: Long = 2.seconds()
open val storyLength: Long = 2.seconds()
abstract val backgroundColor: Color
val tintColor: Color = Color.White
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package au.com.shiftyjelly.pocketcasts.endofyear.stories

import androidx.compose.ui.graphics.Color
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast

class StoryFake1 : Story() {
class StoryFake1(
val podcasts: List<Podcast>,
) : Story() {
override val backgroundColor: Color = Color.Magenta
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package au.com.shiftyjelly.pocketcasts.endofyear.stories

import androidx.compose.ui.graphics.Color
import au.com.shiftyjelly.pocketcasts.models.entity.Episode
import au.com.shiftyjelly.pocketcasts.utils.seconds

class StoryFake2 : Story() {
class StoryFake2(
val episode: Episode,
) : Story() {
override val storyLength: Long = 3.seconds()
override val backgroundColor: Color = Color.Green
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package au.com.shiftyjelly.pocketcasts.endofyear.storyviews

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import au.com.shiftyjelly.pocketcasts.compose.components.PodcastItem
import au.com.shiftyjelly.pocketcasts.compose.components.TextH30
import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake1

@Composable
fun StoryFake1View(
story: StoryFake1,
modifier: Modifier = Modifier,
) {
Column {
TextH30(
text = "Your Top Podcasts",
textAlign = TextAlign.Center,
color = story.tintColor,
modifier = modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
)
LazyColumn(modifier = modifier.fillMaxWidth()) {
items(story.podcasts.size) { index ->
PodcastItem(
podcast = story.podcasts[index],
onClick = {},
tintColor = story.tintColor,
showDivider = false
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package au.com.shiftyjelly.pocketcasts.endofyear.storyviews

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import au.com.shiftyjelly.pocketcasts.compose.components.TextH30
import au.com.shiftyjelly.pocketcasts.endofyear.stories.StoryFake2

@Composable
fun StoryFake2View(
story: StoryFake2,
modifier: Modifier = Modifier,
) {
Column(modifier.padding(16.dp)) {
TextH30(
text = "The longest episode you listened to was ${story.episode.title}",
textAlign = TextAlign.Center,
color = story.tintColor,
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
Loading

0 comments on commit 95a5324

Please sign in to comment.