diff --git a/app/build.gradle b/app/build.gradle
index e13b1161844..86582258273 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -93,6 +93,7 @@ dependencies {
implementation project(':modules:features:filters')
implementation project(':modules:features:navigation')
implementation project(':modules:features:account')
+ implementation project(':modules:features:endofyear')
}
task appStart(type: Exec, dependsOn: 'installDebug') {
diff --git a/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt b/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
index 1fc7e1a65ff..11dbc7e3b96 100644
--- a/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
+++ b/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
@@ -33,6 +33,7 @@ import au.com.shiftyjelly.pocketcasts.compose.bottomsheet.BottomSheetContentStat
import au.com.shiftyjelly.pocketcasts.compose.bottomsheet.ModalBottomSheet
import au.com.shiftyjelly.pocketcasts.databinding.ActivityMainBinding
import au.com.shiftyjelly.pocketcasts.discover.view.DiscoverFragment
+import au.com.shiftyjelly.pocketcasts.endofyear.StoriesFragment
import au.com.shiftyjelly.pocketcasts.filters.FiltersFragment
import au.com.shiftyjelly.pocketcasts.localization.helper.LocaliseHelper
import au.com.shiftyjelly.pocketcasts.models.entity.Episode
@@ -465,7 +466,10 @@ class MainActivity :
summaryText = stringResource(LR.string.end_of_year_launch_modal_summary),
primaryButton = Button.Primary(
label = stringResource(LR.string.end_of_year_launch_modal_primary_button_title),
- onClick = {}
+ onClick = {
+ StoriesFragment.newInstance()
+ .show(supportFragmentManager, "stories_dialog")
+ }
),
secondaryButton = Button.Secondary(
label = stringResource(LR.string.end_of_year_launch_modal_secondary_button_title),
diff --git a/modules/features/endofyear/build.gradle b/modules/features/endofyear/build.gradle
new file mode 100644
index 00000000000..7ea247923de
--- /dev/null
+++ b/modules/features/endofyear/build.gradle
@@ -0,0 +1,18 @@
+apply from: "../../modules.gradle"
+
+android {
+ namespace 'au.com.shiftyjelly.pocketcasts.endofyear'
+ buildFeatures {
+ viewBinding true
+ dataBinding = true
+ compose true
+ }
+}
+
+dependencies {
+ // services
+ implementation project(':modules:services:compose')
+ implementation project(':modules:services:localization')
+ implementation project(':modules:services:ui')
+ implementation project(':modules:services:views')
+}
diff --git a/modules/features/endofyear/src/main/AndroidManifest.xml b/modules/features/endofyear/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..a5918e68abc
--- /dev/null
+++ b/modules/features/endofyear/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/SegmentedProgressIndicator.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/SegmentedProgressIndicator.kt
new file mode 100644
index 00000000000..503748fc6fb
--- /dev/null
+++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/SegmentedProgressIndicator.kt
@@ -0,0 +1,78 @@
+package au.com.shiftyjelly.pocketcasts.endofyear
+
+import androidx.annotation.FloatRange
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.progressSemantics
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+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
+
+private val StrokeWidth = 2.dp
+private val GapWidth = 8.dp
+private val SegmentHeight = StrokeWidth
+private const val IndicatorBackgroundOpacity = 0.24f
+
+@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,
+) {
+ Canvas(
+ modifier
+ .progressSemantics(progress)
+ .fillMaxWidth()
+ .height(SegmentHeight)
+ .focusable()
+ ) {
+ drawSegmentsBackground(backgroundColor, numberOfSegments)
+ drawSegments(progress, color, numberOfSegments)
+ }
+}
+
+private fun DrawScope.drawSegmentsBackground(
+ color: Color,
+ numberOfSegments: Int,
+) = drawSegments(1f, color, numberOfSegments)
+
+private fun DrawScope.drawSegments(
+ endFraction: Float,
+ color: Color,
+ numberOfSegments: Int,
+) {
+ val width = size.width
+ val height = size.height
+ // Start drawing from the vertical center of the stroke
+ val yOffset = height / 2
+
+ val barEnd = endFraction * width
+
+ val segmentWidth = calculateSegmentWidth(numberOfSegments)
+ val segmentAndGapWidth = segmentWidth + GapWidth.toPx()
+
+ repeat(numberOfSegments) { index ->
+ val xOffsetStart = index * segmentAndGapWidth
+ val shouldDrawLine = xOffsetStart < barEnd
+ if (shouldDrawLine) {
+ val xOffsetEnd = (xOffsetStart + segmentWidth).coerceAtMost(barEnd)
+ // Progress line
+ drawLine(color, Offset(xOffsetStart, yOffset), Offset(xOffsetEnd, yOffset), StrokeWidth.toPx())
+ }
+ }
+}
+
+private fun DrawScope.calculateSegmentWidth(
+ numberOfSegments: Int,
+): Float {
+ val width = size.width
+ val gapsWidth = (numberOfSegments - 1) * GapWidth.toPx()
+ return (width - gapsWidth) / numberOfSegments
+}
diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt
new file mode 100644
index 00000000000..0bfc6445530
--- /dev/null
+++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesFragment.kt
@@ -0,0 +1,44 @@
+package au.com.shiftyjelly.pocketcasts.endofyear
+
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+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() {
+ override val statusBarColor: StatusBarColor
+ get() = StatusBarColor.Custom(Color.BLACK, true)
+
+ override fun onCreate(savedInstance: Bundle?) {
+ super.onCreate(savedInstance)
+ setStyle(STYLE_NORMAL, R.style.BottomSheetDialogThemeBlack)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setContent {
+ AppTheme(theme.activeTheme) {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ StoriesScreen(
+ onCloseClicked = { dismiss() },
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ fun newInstance() = StoriesFragment()
+ }
+}
diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt
new file mode 100644
index 00000000000..c73a284ecb1
--- /dev/null
+++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesScreen.kt
@@ -0,0 +1,141 @@
+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.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+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.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.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+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.preview.ThemePreviewParameterProvider
+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(
+ 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
+ )
+ )
+ Box(modifier = modifier.background(color = Color.Black)) {
+ StoryView(color = Color.Gray)
+ SegmentedProgressIndicator(
+ progress = progress,
+ numberOfSegments = NumberOfSegments,
+ modifier = modifier
+ .padding(8.dp)
+ .fillMaxWidth(),
+ )
+ CloseButtonView(onCloseClicked)
+ }
+
+ LaunchedEffect(Unit) {
+ running = true
+ }
+}
+
+@Composable
+private fun StoryView(
+ color: Color,
+ modifier: Modifier = Modifier,
+) {
+ Column {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .weight(weight = 1f, fill = true)
+ .clip(RoundedCornerShape(StoryViewCornerSize))
+ .background(color = color)
+ ) {}
+ ShareButton()
+ }
+}
+
+@Composable
+private fun ShareButton() {
+ RowOutlinedButton(
+ text = stringResource(id = LR.string.share),
+ border = BorderStroke(ShareButtonStrokeWidth, Color.White),
+ colors = ButtonDefaults
+ .outlinedButtonColors(
+ backgroundColor = Color.Transparent,
+ contentColor = Color.White,
+ ),
+ iconImage = Icons.Default.Share,
+ onClick = {}
+ )
+}
+
+@Composable
+private fun CloseButtonView(
+ onCloseClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ horizontalArrangement = Arrangement.End
+ ) {
+ IconButton(
+ onClick = onCloseClicked
+ ) {
+ Icon(
+ imageVector = NavigationButton.Close.image,
+ contentDescription = stringResource(NavigationButton.Close.contentDescription),
+ tint = Color.White
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun StoriesScreenPreview(
+ @PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
+) {
+ AppTheme(themeType) {
+ StoriesScreen(
+ onCloseClicked = {}
+ )
+ }
+}
diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/bottomsheet/BottomSheetContent.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/bottomsheet/BottomSheetContent.kt
index a8515894f70..afeb6bff74d 100644
--- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/bottomsheet/BottomSheetContent.kt
+++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/bottomsheet/BottomSheetContent.kt
@@ -95,7 +95,12 @@ fun BottomSheetContent(
Spacer(modifier = modifier.height(16.dp))
- Button(onClick = content.primaryButton.onClick) {
+ Button(
+ onClick = {
+ onDismiss.invoke()
+ content.primaryButton.onClick.invoke()
+ }
+ ) {
Text(text = content.primaryButton.label)
}
diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/buttons/RowOutlinedButton.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/buttons/RowOutlinedButton.kt
index a9f9aa7067a..ec1400b4c34 100644
--- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/buttons/RowOutlinedButton.kt
+++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/buttons/RowOutlinedButton.kt
@@ -5,11 +5,17 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ButtonColors
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -25,7 +31,10 @@ fun RowOutlinedButton(
text: String,
modifier: Modifier = Modifier,
includePadding: Boolean = true,
- onClick: () -> Unit
+ border: BorderStroke = outlinedBorder,
+ colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
+ iconImage: ImageVector? = null,
+ onClick: () -> Unit,
) {
Row(
modifier = modifier
@@ -35,9 +44,15 @@ fun RowOutlinedButton(
OutlinedButton(
onClick = { onClick() },
shape = RoundedCornerShape(12.dp),
- border = outlinedBorder,
- modifier = Modifier.fillMaxWidth()
+ border = border,
+ colors = colors,
+ modifier = Modifier
+ .fillMaxWidth()
) {
+ iconImage?.let {
+ Icon(imageVector = it, contentDescription = "")
+ }
+
Text(
text = text,
fontSize = 18.sp,
@@ -52,7 +67,8 @@ fun RowOutlinedButton(
fun RowOutlinedButtonLightPreview() {
AppTheme(Theme.ThemeType.LIGHT) {
RowOutlinedButton(
- text = "Accept",
+ text = "Share",
+ iconImage = Icons.Default.Share,
onClick = {}
)
}
@@ -63,7 +79,8 @@ fun RowOutlinedButtonLightPreview() {
fun RowOutlinedButtonDarkPreview() {
AppTheme(Theme.ThemeType.DARK) {
RowOutlinedButton(
- text = "Accept",
+ text = "Share",
+ iconImage = Icons.Default.Share,
onClick = {}
)
}
diff --git a/modules/services/ui/src/main/res/values/themes.xml b/modules/services/ui/src/main/res/values/themes.xml
index b4add37711b..31486c47463 100644
--- a/modules/services/ui/src/main/res/values/themes.xml
+++ b/modules/services/ui/src/main/res/values/themes.xml
@@ -1657,6 +1657,15 @@
- @drawable/bg_bottom_sheet_dialog_fragment_dark
+
+
+
diff --git a/settings.gradle b/settings.gradle
index 8d69266df5d..17b422c1655 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,23 +1,28 @@
include ':app'
include ':automotive'
-include ':modules:services:localization'
+
+// features
+include ':modules:features:account'
+include ':modules:features:cartheme'
+include ':modules:features:discover'
+include ':modules:features:endofyear'
+include ':modules:features:filters'
+include ':modules:features:navigation'
+include ':modules:features:player'
+include ':modules:features:podcasts'
+include ':modules:features:profile'
+include ':modules:features:search'
+include ':modules:features:settings'
+
+// services
+include ':modules:services:analytics'
include ':modules:services:compose'
-include ':modules:services:utils'
include ':modules:services:images'
+include ':modules:services:localization'
include ':modules:services:model'
+include ':modules:services:preferences'
include ':modules:services:repositories'
include ':modules:services:servers'
-include ':modules:services:preferences'
include ':modules:services:ui'
+include ':modules:services:utils'
include ':modules:services:views'
-include ':modules:features:discover'
-include ':modules:features:profile'
-include ':modules:features:settings'
-include ':modules:features:podcasts'
-include ':modules:features:search'
-include ':modules:features:player'
-include ':modules:features:filters'
-include ':modules:features:navigation'
-include ':modules:features:cartheme'
-include ':modules:features:account'
-include ':modules:services:analytics'