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: Generate stories with dummy data + support dynamic lengths #495

Merged
merged 7 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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