Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,19 @@ fun <T> TvSortButton(
label: @Composable (T) -> String,
onSelect: (T) -> Unit,
modifier: Modifier = Modifier,
onExpand: () -> Unit = {},
leftFocusRequester: FocusRequester? = null,
) {
var isExpanded by remember { mutableStateOf(false) }

Box(modifier = modifier) {
IconButton(
onClick = { isExpanded = true },
onClick = {
onExpand()
isExpanded = true
},
colors = TvButtonDefaults.iconButtonColors(),
modifier = Modifier.focusProperties { leftFocusRequester?.let { left = it } },
) {
Icon(
painter = painterResource(IR.drawable.ic_sort),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ fun TvPlaylistsScreen(
var isDownloadModalVisible by rememberSaveable { mutableStateOf(false) }
var openedPlaylist by rememberSaveable(stateSaver = OpenedPlaylistSaver) { mutableStateOf<OpenedPlaylist?>(null) }
var restoreFocusTrigger by remember { mutableIntStateOf(0) }
var filterListShownTracked by remember { mutableStateOf(false) }

LaunchedEffect(uiState) {
val state = uiState
if (!filterListShownTracked && state is TvPlaylistsUiState.Loaded) {
viewModel.trackPlaylistsListShown(state.playlists.size)
filterListShownTracked = true
}
}

val playlist = openedPlaylist
Box(modifier = modifier.fillMaxSize()) {
Expand All @@ -85,7 +94,10 @@ fun TvPlaylistsScreen(
refreshArtworkUuids = viewModel::refreshArtworkUuids,
refreshEpisodeCount = viewModel::refreshEpisodeCount,
findPodcastTint = viewModel::findPodcastTint,
onCreatePlaylist = { isDownloadModalVisible = true },
onCreatePlaylist = {
viewModel.trackCreateButtonTapped()
isDownloadModalVisible = true
},
onOpenPlaylist = { preview ->
isDownloadModalVisible = false
openedPlaylist = OpenedPlaylist(preview.uuid, preview.type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistManager
import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistPreview
import au.com.shiftyjelly.pocketcasts.repositories.playlist.SmartPlaylistPreview
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager
import com.automattic.eventhorizon.EventHorizon
import com.automattic.eventhorizon.FilterCreateButtonTappedEvent
import com.automattic.eventhorizon.FilterListShownEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
Expand All @@ -20,6 +23,7 @@ import kotlinx.coroutines.flow.stateIn
class TvPlaylistsViewModel @Inject constructor(
private val playlistManager: PlaylistManager,
private val podcastManager: PodcastManager,
private val eventHorizon: EventHorizon,
) : ViewModel() {

val uiState: StateFlow<TvPlaylistsUiState> = playlistManager.playlistPreviewsFlow()
Expand Down Expand Up @@ -48,6 +52,14 @@ class TvPlaylistsViewModel @Inject constructor(
return podcastManager.findPodcastByUuid(podcastUuid)?.tintColorForLightBg?.takeIf { it != 0 }
}

fun trackPlaylistsListShown(filterCount: Int) {
eventHorizon.track(FilterListShownEvent(filterCount = filterCount.toLong()))
}

fun trackCreateButtonTapped() {
eventHorizon.track(FilterCreateButtonTappedEvent)
}

private fun isDownloadPlaylist(preview: PlaylistPreview): Boolean {
return preview is SmartPlaylistPreview && preview.smartRules.downloadStatus == SmartRules.DownloadStatusRule.Downloaded
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import au.com.shiftyjelly.pocketcasts.component.TvModalButton
import au.com.shiftyjelly.pocketcasts.component.TvModalSurface
import au.com.shiftyjelly.pocketcasts.component.TvSortButton
import au.com.shiftyjelly.pocketcasts.component.rememberTvEpisodeListFocus
import au.com.shiftyjelly.pocketcasts.compose.CallOnce
import au.com.shiftyjelly.pocketcasts.compose.components.PlaylistArtwork
import au.com.shiftyjelly.pocketcasts.compose.components.displayLabel
import au.com.shiftyjelly.pocketcasts.compose.loading.LoadingView
Expand Down Expand Up @@ -92,6 +93,8 @@ fun TvPlaylistDetailsScreen(
val savedToast = stringResource(LR.string.up_next_as_playlist_saved)
val noEpisodesToast = stringResource(LR.string.play_all_no_episodes_message)

CallOnce { viewModel.trackFilterShown() }

LaunchedEffect(uiState, onClose) {
if (uiState is TvPlaylistDetailsUiState.NotFound) {
onClose()
Expand Down Expand Up @@ -121,6 +124,7 @@ fun TvPlaylistDetailsScreen(
TvPlaylistDetailsContent(
uiState = uiState,
onChangeSortType = viewModel::changeSortType,
onSortTap = viewModel::trackSortByTapped,
onToggleArchiveFilter = viewModel::toggleArchiveFilter,
onOpenPodcast = { openedPodcastUuid = it },
onPlayAll = viewModel::playAll,
Expand All @@ -138,7 +142,10 @@ fun TvPlaylistDetailsScreen(
isReplaceUpNextConfirmationVisible = false
viewModel.replaceUpNextAndPlay(saveUpNext = true, upNextName = upNextName)
},
onCancel = { isReplaceUpNextConfirmationVisible = false },
onCancel = {
isReplaceUpNextConfirmationVisible = false
viewModel.trackPlayAllDismissed()
},
)
}
}
Expand All @@ -147,6 +154,7 @@ fun TvPlaylistDetailsScreen(
private fun TvPlaylistDetailsContent(
uiState: TvPlaylistDetailsUiState,
onChangeSortType: (PlaylistEpisodeSortType) -> Unit,
onSortTap: () -> Unit,
onToggleArchiveFilter: () -> Unit,
onOpenPodcast: (String) -> Unit,
onPlayAll: () -> Unit,
Expand Down Expand Up @@ -182,6 +190,7 @@ private fun TvPlaylistDetailsContent(
SortableEpisodeList(
uiState = uiState,
onChangeSortType = onChangeSortType,
onSortTap = onSortTap,
onToggleArchiveFilter = onToggleArchiveFilter,
onOpenPodcast = onOpenPodcast,
playAllFocusRequester = playAllFocusRequester,
Expand All @@ -198,6 +207,7 @@ private fun TvPlaylistDetailsContent(
private fun SortableEpisodeList(
uiState: TvPlaylistDetailsUiState.Loaded,
onChangeSortType: (PlaylistEpisodeSortType) -> Unit,
onSortTap: () -> Unit,
onToggleArchiveFilter: () -> Unit,
onOpenPodcast: (String) -> Unit,
playAllFocusRequester: FocusRequester,
Expand All @@ -212,6 +222,8 @@ private fun SortableEpisodeList(
listState.scrollToItem(0)
}
}
val isManual = uiState.playlist.type == Playlist.Type.Manual
val leftFocusRequester = playAllFocusRequester.takeIf { uiState.episodes.isNotEmpty() }
Column(modifier = modifier) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
Expand All @@ -220,16 +232,20 @@ private fun SortableEpisodeList(
.align(Alignment.End)
.padding(bottom = 12.dp),
) {
TvArchivedFilterButton(
isShowingArchived = uiState.isShowingArchivedOnDevice,
onToggleArchiveFilter = onToggleArchiveFilter,
leftFocusRequester = playAllFocusRequester.takeIf { uiState.episodes.isNotEmpty() },
)
if (isManual) {
TvArchivedFilterButton(
isShowingArchived = uiState.isShowingArchivedOnDevice,
onToggleArchiveFilter = onToggleArchiveFilter,
leftFocusRequester = leftFocusRequester,
)
}
TvSortButton(
selected = sortType,
options = uiState.playlist.availableSortTypes,
label = { it.displayLabel() },
onSelect = onChangeSortType,
onExpand = onSortTap,
leftFocusRequester = if (isManual) null else leftFocusRequester,
)
}
if (uiState.episodes.isNotEmpty()) {
Expand Down Expand Up @@ -487,6 +503,7 @@ private fun TvPlaylistDetailsPreview() {
isShowingArchivedOnDevice = false,
),
onChangeSortType = {},
onSortTap = {},
onToggleArchiveFilter = {},
onOpenPodcast = {},
onPlayAll = {},
Expand Down Expand Up @@ -519,6 +536,7 @@ private fun TvPlaylistDetailsLoadedPreview() {
isShowingArchivedOnDevice = false,
),
onChangeSortType = {},
onSortTap = {},
onToggleArchiveFilter = {},
onOpenPodcast = {},
onPlayAll = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import au.com.shiftyjelly.pocketcasts.repositories.playback.PlayAllHandler
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlayAllResponse
import au.com.shiftyjelly.pocketcasts.repositories.playlist.Playlist
import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistManager
import com.automattic.eventhorizon.EventHorizon
import com.automattic.eventhorizon.FilterHideArchivedTappedEvent
import com.automattic.eventhorizon.FilterPlayAllDismissedEvent
import com.automattic.eventhorizon.FilterPlayAllReplaceAndPlayTappedEvent
import com.automattic.eventhorizon.FilterPlayAllTappedEvent
import com.automattic.eventhorizon.FilterShowArchivedTappedEvent
import com.automattic.eventhorizon.FilterShownEvent
import com.automattic.eventhorizon.FilterSortByChangedEvent
import com.automattic.eventhorizon.FilterSortByTappedEvent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
Expand Down Expand Up @@ -41,11 +50,14 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor(
@Assisted private val playlistType: Playlist.Type,
private val playlistManager: PlaylistManager,
private val preferences: TvPreferences,
private val eventHorizon: EventHorizon,
playAllHandlerFactory: PlayAllHandler.Factory,
) : ViewModel() {

private val playAllHandler = playAllHandlerFactory.create(SourceView.FILTERS)

private val filterType = playlistType.analyticsValue

private val _events = MutableSharedFlow<TvPlaylistDetailsEvent>(extraBufferCapacity = 2)
val events: SharedFlow<TvPlaylistDetailsEvent> = _events.asSharedFlow()

Expand All @@ -59,7 +71,9 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor(
Playlist.Type.Smart -> playlistManager.smartPlaylistFlow(playlistUuid, includeArchived = true)
}

private val isShowingArchivedFlow = MutableStateFlow(preferences.isPlaylistShowingArchived(playlistUuid))
private val isShowingArchivedFlow = MutableStateFlow(
playlistType == Playlist.Type.Manual && preferences.isPlaylistShowingArchived(playlistUuid),
)

val uiState: StateFlow<TvPlaylistDetailsUiState> = combine(
playlistFlow,
Expand All @@ -81,14 +95,29 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor(
TvPlaylistDetailsUiState.Loading,
)

fun trackSortByTapped() {
eventHorizon.track(FilterSortByTappedEvent(filterType = filterType))
}

fun changeSortType(sortType: PlaylistEpisodeSortType) {
eventHorizon.track(FilterSortByChangedEvent(sortOrder = sortType.analyticsValue, filterType = filterType))
viewModelScope.launch {
playlistManager.updateSortType(playlistUuid, sortType)
}
}

fun trackFilterShown() {
eventHorizon.track(FilterShownEvent(filterType = filterType))
}

fun trackPlayAllDismissed() {
eventHorizon.track(FilterPlayAllDismissedEvent(filterType = filterType))
}

fun toggleArchiveFilter() {
if (playlistType != Playlist.Type.Manual) return
val isShowingArchived = !isShowingArchivedFlow.value
eventHorizon.track(if (isShowingArchived) FilterShowArchivedTappedEvent else FilterHideArchivedTappedEvent)
preferences.setPlaylistShowingArchived(playlistUuid, isShowingArchived)
isShowingArchivedFlow.value = isShowingArchived
}
Expand All @@ -97,9 +126,12 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor(
if (isBusy) {
return
}
val episodes = (uiState.value as? TvPlaylistDetailsUiState.Loaded)?.episodes.orEmpty()
if (episodes.isNotEmpty()) {
eventHorizon.track(FilterPlayAllTappedEvent(filterType = filterType))
}
playAllJob = viewModelScope.launch {
try {
val episodes = (uiState.value as? TvPlaylistDetailsUiState.Loaded)?.episodes.orEmpty()
when (playAllHandler.handlePlayAllEpisodes(episodes)) {
PlayAllResponse.DoNothing -> _events.tryEmit(TvPlaylistDetailsEvent.OpenNowPlaying)
PlayAllResponse.ShowWarning -> _events.tryEmit(TvPlaylistDetailsEvent.ShowReplaceUpNextConfirmation)
Expand All @@ -117,6 +149,7 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor(
if (isBusy) {
return
}
eventHorizon.track(FilterPlayAllReplaceAndPlayTappedEvent(filterType = filterType, saveUpNext = saveUpNext))
replaceUpNextJob = viewModelScope.launch {
val played = withContext(NonCancellable) {
if (saveUpNext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistPreview
import au.com.shiftyjelly.pocketcasts.repositories.playlist.SmartPlaylistPreview
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager
import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule
import com.automattic.eventhorizon.EventHorizon
import com.automattic.eventhorizon.FilterCreateButtonTappedEvent
import com.automattic.eventhorizon.FilterListShownEvent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.runTest
Expand All @@ -20,6 +23,7 @@ import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

@OptIn(ExperimentalCoroutinesApi::class)
class TvPlaylistsViewModelTest {
Expand All @@ -32,6 +36,21 @@ class TvPlaylistsViewModelTest {
on { playlistPreviewsFlow() } doReturn playlistPreviews
}
private val podcastManager = mock<PodcastManager>()
private val eventHorizon = mock<EventHorizon>()

@Test
fun `tracks the playlists list shown with the filter count`() = runTest {
createViewModel().trackPlaylistsListShown(3)

verify(eventHorizon).track(FilterListShownEvent(filterCount = 3))
}

@Test
fun `tracks the create button tapped`() = runTest {
createViewModel().trackCreateButtonTapped()

verify(eventHorizon).track(FilterCreateButtonTappedEvent)
}

@Test
fun `state starts as loading`() = runTest {
Expand Down Expand Up @@ -115,6 +134,7 @@ class TvPlaylistsViewModelTest {
private fun createViewModel(podcastManager: PodcastManager = this.podcastManager) = TvPlaylistsViewModel(
playlistManager = playlistManager,
podcastManager = podcastManager,
eventHorizon = eventHorizon,
)

private fun smartPreview(
Expand Down
Loading