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'