Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package au.com.shiftyjelly.pocketcasts.player.view.bookmark

import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import au.com.shiftyjelly.pocketcasts.analytics.SourceView
import au.com.shiftyjelly.pocketcasts.compose.extensions.contentWithoutConsumedInsets
import au.com.shiftyjelly.pocketcasts.models.entity.Bookmark
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager
import au.com.shiftyjelly.pocketcasts.utils.extensions.toLocalizedFormatPattern
import au.com.shiftyjelly.pocketcasts.views.fragments.BaseDialogFragment
import com.automattic.eventhorizon.BookmarkPlayTappedEvent
import com.automattic.eventhorizon.EventHorizon
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
import au.com.shiftyjelly.pocketcasts.localization.R as LR

@AndroidEntryPoint
class BookmarkDetailFragment : BaseDialogFragment() {

companion object {
private const val TAG = "bookmark_detail"
private const val ARG_DISPLAY_TITLE = "display_title"
private const val ARG_AI_SUMMARY = "ai_summary"
private const val ARG_EPISODE_TITLE = "episode_title"
private const val ARG_EPISODE_UUID = "episode_uuid"
private const val ARG_PODCAST_UUID = "podcast_uuid"
private const val ARG_PODCAST_TITLE = "podcast_title"
private const val ARG_TIME_SECS = "time_secs"
private const val ARG_CREATED_AT_TEXT = "created_at_text"
private const val ARG_SOURCE_VIEW = "source_view"

fun show(
fragmentManager: FragmentManager,
bookmark: Bookmark,
episodeTitle: String,
podcastUuid: String,
podcastTitle: String,
sourceView: SourceView,
) {
if (!fragmentManager.isStateSaved && fragmentManager.findFragmentByTag(TAG) == null) {
newInstance(bookmark, episodeTitle, podcastUuid, podcastTitle, sourceView)
.show(fragmentManager, TAG)
}
}

private fun newInstance(
bookmark: Bookmark,
episodeTitle: String,
podcastUuid: String,
podcastTitle: String,
sourceView: SourceView,
) = BookmarkDetailFragment().apply {
arguments = Bundle().apply {
putString(ARG_DISPLAY_TITLE, bookmark.displayTitle)
putString(ARG_AI_SUMMARY, bookmark.aiSummary)
putString(ARG_EPISODE_TITLE, episodeTitle)
putString(ARG_EPISODE_UUID, bookmark.episodeUuid)
putString(ARG_PODCAST_UUID, podcastUuid)
putString(ARG_PODCAST_TITLE, podcastTitle)
putInt(ARG_TIME_SECS, bookmark.timeSecs)
putString(
ARG_CREATED_AT_TEXT,
bookmark.createdAt.toLocalizedFormatPattern(bookmark.createdAtDatePattern()),
)
putString(ARG_SOURCE_VIEW, sourceView.key)
}
}
}

@Inject
internal lateinit var playbackManager: PlaybackManager

@Inject
internal lateinit var episodeManager: EpisodeManager

@Inject
internal lateinit var eventHorizon: EventHorizon

private val displayTitle: String get() = requireArguments().getString(ARG_DISPLAY_TITLE, "")
private val aiSummary: String? get() = requireArguments().getString(ARG_AI_SUMMARY)
private val episodeTitle: String get() = requireArguments().getString(ARG_EPISODE_TITLE, "")
private val episodeUuid: String get() = requireArguments().getString(ARG_EPISODE_UUID, "")
private val podcastUuid: String get() = requireArguments().getString(ARG_PODCAST_UUID, "")
private val podcastTitle: String get() = requireArguments().getString(ARG_PODCAST_TITLE, "")
private val timeSecs: Int get() = requireArguments().getInt(ARG_TIME_SECS)
private val createdAtText: String get() = requireArguments().getString(ARG_CREATED_AT_TEXT, "")
private val sourceView: SourceView
get() = SourceView.fromString(requireArguments().getString(ARG_SOURCE_VIEW))

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = contentWithoutConsumedInsets {
DialogBox(fillMaxHeight = false) {
BookmarkDetailPage(
displayTitle = displayTitle,
aiSummary = aiSummary,
episodeTitle = episodeTitle,
podcastUuid = podcastUuid,
podcastTitle = podcastTitle,
timeSecs = timeSecs,
createdAtText = createdAtText,
onPlayClick = ::onPlayClick,
onClose = { dismiss() },
)
}
}

private fun onPlayClick() {
lifecycleScope.launch {
val episode = episodeManager.findEpisodeByUuid(episodeUuid)
if (episode == null) {
Toast.makeText(
requireContext(),
getString(LR.string.episode_not_found),
Toast.LENGTH_SHORT,
).show()
dismiss()
return@launch
}
playbackManager.playNowSuspend(episode, sourceView = sourceView)
playbackManager.seekToTimeMs(positionMs = timeSecs * 1000)
eventHorizon.track(
BookmarkPlayTappedEvent(
source = sourceView.analyticsValue,
episodeUuid = episodeUuid,
podcastUuid = podcastUuid,
),
)
dismiss()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package au.com.shiftyjelly.pocketcasts.player.view.bookmark

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.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.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.compose.bookmark.BookmarkRowColors
import au.com.shiftyjelly.pocketcasts.compose.buttons.RowButton
import au.com.shiftyjelly.pocketcasts.compose.components.PodcastImage
import au.com.shiftyjelly.pocketcasts.compose.components.TextH30
import au.com.shiftyjelly.pocketcasts.compose.components.TextH70
import au.com.shiftyjelly.pocketcasts.compose.components.TextP40
import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider
import au.com.shiftyjelly.pocketcasts.compose.theme
import au.com.shiftyjelly.pocketcasts.localization.helper.TimeHelper
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.images.R as IR
import au.com.shiftyjelly.pocketcasts.localization.R as LR

@Composable
internal fun BookmarkDetailPage(
displayTitle: String,
aiSummary: String?,
episodeTitle: String,
Comment thread
sztomek marked this conversation as resolved.
podcastUuid: String,
podcastTitle: String,
timeSecs: Int,
createdAtText: String,
onPlayClick: () -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
Comment thread
sztomek marked this conversation as resolved.
val theme = MaterialTheme.theme
val playerColors = theme.rememberPlayerColors()
val colors = remember(theme.type, playerColors) {
if (playerColors != null) {
BookmarkRowColors.player(playerColors)
} else {
BookmarkRowColors.default(theme.colors)
}
}
val playButtonBackground = if (playerColors != null) {
playerColors.contrast01
} else {
theme.colors.primaryInteractive01
}
val playButtonText = if (playerColors != null) {
playerColors.background01
} else {
theme.colors.primaryInteractive02
}

Column(
modifier = modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = 24.dp),
) {
DragHandle(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 12.dp),
)

Header(
buttonColor = colors.primaryText,
onClose = onClose,
)

Column(
modifier = Modifier.padding(horizontal = 20.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
PodcastImage(
uuid = podcastUuid,
imageSize = 48.dp,
elevation = null,
)

Spacer(modifier = Modifier.width(12.dp))

Column {
if (podcastTitle.isNotEmpty()) {
TextH70(
text = podcastTitle.uppercase(),
color = colors.secondaryText,
)
Spacer(modifier = Modifier.height(4.dp))
}

if (episodeTitle.isNotEmpty()) {
TextH70(
text = episodeTitle,
color = colors.primaryText,
)
}
}
}

Spacer(modifier = Modifier.height(8.dp))

TextH30(
text = displayTitle,
color = colors.primaryText,
)

Spacer(modifier = Modifier.height(4.dp))

val formattedTime = TimeHelper.formattedSeconds(timeSecs.toDouble())
TextH70(
text = formattedTime,
color = colors.secondaryText,
)

if (!aiSummary.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
TextP40(
text = aiSummary,
color = colors.secondaryText,
)
}

Spacer(modifier = Modifier.height(12.dp))

TextH70(
text = createdAtText,
color = colors.secondaryText,
)

Spacer(modifier = Modifier.height(20.dp))
RowButton(
text = stringResource(LR.string.bookmark_play_from, formattedTime),
onClick = onPlayClick,
includePadding = false,
textIcon = IR.drawable.ic_play,
colors = ButtonDefaults.buttonColors(
backgroundColor = playButtonBackground,
),
textColor = playButtonText,
)
}
}
}

@Composable
private fun DragHandle(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.width(36.dp)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(MaterialTheme.theme.colors.primaryText01.copy(alpha = 0.3f)),
)
}

@Composable
private fun Header(
buttonColor: Color,
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 20.dp, top = 4.dp, bottom = 8.dp),
) {
IconButton(
onClick = onClose,
modifier = Modifier.offset(x = (-4).dp),
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(LR.string.close),
tint = buttonColor,
)
}
}
}

@Preview
@Composable
private fun BookmarkDetailPagePreview(
@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType,
) {
AppThemeWithBackground(themeType) {
BookmarkDetailPage(
displayTitle = "Latency vs throughput tradeoff",
aiSummary = "Why optimizing for low latency often means sacrificing batch throughput.",
episodeTitle = "Can the U.S. Rein in Prediction Markets?",
podcastUuid = "",
podcastTitle = "Hard Fork",
timeSecs = 340,
createdAtText = "May 7, 2024 - 6:40 PM",
onPlayClick = {},
onClose = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ class BookmarksFragment : BaseFragment() {
addFragment(fragment)
}
},
onBookmarkDetailClick = { data ->
BookmarkDetailFragment.show(
fragmentManager = parentFragmentManager,
bookmark = data.bookmark,
episodeTitle = data.episodeTitle,
podcastUuid = data.podcastUuid,
podcastTitle = data.podcastTitle,
sourceView = sourceView,
)
},
onSearchBarClearButtonClick = {
bookmarksViewModel.searchBarClearButtonTapped()
},
Expand Down
Loading