diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/ShareableTextProvider.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/ShareableTextProvider.kt new file mode 100644 index 00000000000..bb126c7a2ea --- /dev/null +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/ShareableTextProvider.kt @@ -0,0 +1,125 @@ +package au.com.shiftyjelly.pocketcasts.endofyear + +import android.content.Context +import au.com.shiftyjelly.pocketcasts.preferences.Settings +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.Story +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryListenedCategories +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryListenedNumbers +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryListeningTime +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryLongestEpisode +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryTopFivePodcasts +import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryTopPodcast +import au.com.shiftyjelly.pocketcasts.servers.list.ListServerManager +import au.com.shiftyjelly.pocketcasts.settings.stats.StatsHelper +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import au.com.shiftyjelly.pocketcasts.localization.R as LR + +class ShareableTextProvider @Inject constructor( + @ApplicationContext private val context: Context, + private val listServerManager: ListServerManager, +) { + private var shortURL: String = Settings.SERVER_SHORT_URL + private val hashtags = listOf("pocketcasts", "endofyear2022").joinToString(" ") { "#$it" } + + suspend fun getShareableDataForStory( + story: Story, + ): ShareTextData { + var showShortURLAtEnd = false + val shareableLink: String = when (story) { + is StoryTopFivePodcasts -> { + try { + listServerManager.createPodcastList( + title = context.resources.getString( + LR.string.end_of_year_story_top_podcasts_share_text, + "" + ), + description = "", + podcasts = story.topPodcasts.map { it.toPodcast() } + ) ?: shortURL + } catch (ex: Exception) { + Timber.e(ex) + shortURL + } + } + is StoryTopPodcast -> { + "$shortURL/podcast/${story.topPodcast.uuid}" + } + is StoryLongestEpisode -> { + "$shortURL/episode/${story.longestEpisode.uuid}" + } + else -> { + showShortURLAtEnd = true + shortURL + } + } + val textWithLink = getTextWithLink(story, shareableLink) + return ShareTextData( + textWithLink = textWithLink, + hashTags = hashtags, + showShortURLAtEnd = showShortURLAtEnd + ) + } + + private fun getTextWithLink( + story: Story, + shareableLink: String + ): String { + val resources = context.resources + return when (story) { + is StoryListeningTime -> { + val timeText = + StatsHelper.secondsToFriendlyString(story.listeningTimeInSecs, resources) + resources.getString( + LR.string.end_of_year_story_listened_to_share_text, + timeText + ) + } + + is StoryListenedCategories -> { + resources.getString( + LR.string.end_of_year_story_listened_to_categories_share_text, + story.listenedCategories.size + ) + } + + is StoryListenedNumbers -> { + resources.getString( + LR.string.end_of_year_story_listened_to_numbers_share_text, + story.listenedNumbers.numberOfPodcasts, + story.listenedNumbers.numberOfEpisodes + ) + } + + is StoryTopPodcast -> { + resources.getString( + LR.string.end_of_year_story_top_podcast_share_text, + shareableLink + ) + } + + is StoryTopFivePodcasts -> { + resources.getString( + LR.string.end_of_year_story_top_podcasts_share_text, + shareableLink + ) + } + + is StoryLongestEpisode -> { + resources.getString( + LR.string.end_of_year_story_longest_episode_share_text, + shareableLink + ) + } + + else -> "" + } + } + + data class ShareTextData( + val textWithLink: String, + val hashTags: String, + val showShortURLAtEnd: Boolean + ) +} diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesPage.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesPage.kt index 76b29b65bc8..d85b836a1df 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesPage.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesPage.kt @@ -56,6 +56,7 @@ import au.com.shiftyjelly.pocketcasts.compose.bars.NavigationButton import au.com.shiftyjelly.pocketcasts.compose.buttons.RowOutlinedButton import au.com.shiftyjelly.pocketcasts.compose.components.TextP50 import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider +import au.com.shiftyjelly.pocketcasts.endofyear.ShareableTextProvider.ShareTextData import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State import au.com.shiftyjelly.pocketcasts.endofyear.views.SegmentedProgressIndicator import au.com.shiftyjelly.pocketcasts.endofyear.views.convertibleToBitmap @@ -69,6 +70,7 @@ import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryTopFivePodcas import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryTopListenedCategoriesView import au.com.shiftyjelly.pocketcasts.endofyear.views.stories.StoryTopPodcastView import au.com.shiftyjelly.pocketcasts.models.db.helper.ListenedNumbers +import au.com.shiftyjelly.pocketcasts.preferences.Settings import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.Story import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryEpilogue import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.StoryIntro @@ -126,21 +128,29 @@ fun StoriesPage( ) { Box(modifier = modifier.size(dialogSize)) { when (state) { - is State.Loaded -> StoriesView( - state = state as State.Loaded, - progress = viewModel.progress, - onSkipPrevious = { viewModel.skipPrevious() }, - onSkipNext = { viewModel.skipNext() }, - onPause = { viewModel.pause() }, - onStart = { viewModel.start() }, - onCloseClicked = onCloseClicked, - onReplayClicked = { viewModel.replay() }, - onShareClicked = { - viewModel.onShareClicked(it, context) { file -> - showShareForFile(context, file, shareLauncher) - } - }, - ) + is State.Loaded -> { + StoriesView( + state = state as State.Loaded, + progress = viewModel.progress, + onSkipPrevious = { viewModel.skipPrevious() }, + onSkipNext = { viewModel.skipNext() }, + onPause = { viewModel.pause() }, + onStart = { viewModel.start() }, + onCloseClicked = onCloseClicked, + onReplayClicked = { viewModel.replay() }, + onShareClicked = { + val currentStory = requireNotNull((state as State.Loaded).currentStory) + viewModel.onShareClicked(it, currentStory, context) { file, shareTextData -> + showShareForFile( + context, + file, + shareLauncher, + shareTextData + ) + } + }, + ) + } State.Loading -> StoriesLoadingView(onCloseClicked) State.Error -> StoriesErrorView(onCloseClicked) } @@ -193,6 +203,9 @@ private fun StoriesView( .padding(8.dp) .fillMaxWidth(), ) + if (state.preparingShareText) { + LoadingOverContentView() + } CloseButtonView(onCloseClicked) } } @@ -204,6 +217,13 @@ private fun StoriesView( } } +@Composable +private fun LoadingOverContentView() { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator(color = Color.White) + } +} + @Composable private fun StorySharableContent( story: Story, @@ -383,13 +403,19 @@ private fun showShareForFile( context: Context, file: File, shareLauncher: ActivityResultLauncher, + shareTextData: ShareTextData, ) { try { val uri = FileUtil.getUriForFile(context, file) + var shareText = "${shareTextData.textWithLink} ${shareTextData.hashTags}" + if (shareTextData.showShortURLAtEnd) { + shareText += " ${Settings.SERVER_SHORT_URL}" + } val chooserIntent = ShareCompat.IntentBuilder(context) .setType("image/png") .addStream(uri) + .setText(shareText) .setChooserTitle(LR.string.end_of_year_share_via) .createChooserIntent() diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt index 80d71401e00..8e506c1e603 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModel.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import androidx.annotation.FloatRange import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import au.com.shiftyjelly.pocketcasts.endofyear.ShareableTextProvider.ShareTextData import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewModel.State.Loaded.SegmentsData import au.com.shiftyjelly.pocketcasts.repositories.endofyear.EndOfYearManager import au.com.shiftyjelly.pocketcasts.repositories.endofyear.stories.Story @@ -25,6 +26,7 @@ import kotlin.math.roundToInt class StoriesViewModel @Inject constructor( private val endOfYearManager: EndOfYearManager, private val fileUtilWrapper: FileUtilWrapper, + private val shareableTextProvider: ShareableTextProvider, ) : ViewModel() { private val mutableState = MutableStateFlow(State.Loading) @@ -128,8 +130,9 @@ class StoriesViewModel @Inject constructor( fun onShareClicked( onCaptureBitmap: () -> Bitmap, + story: Story, context: Context, - showShareForFile: (File) -> Unit, + showShareForFile: (File, ShareTextData) -> Unit, ) { pause() viewModelScope.launch { @@ -139,7 +142,14 @@ class StoriesViewModel @Inject constructor( EOY_STORY_SAVE_FOLDER_NAME, EOY_STORY_SAVE_FILE_NAME ) - savedFile?.let { showShareForFile.invoke(it) } + + val currentState = (state.value as State.Loaded) + mutableState.value = currentState.copy(preparingShareText = true) + + val shareTextData = shareableTextProvider.getShareableDataForStory(story) + mutableState.value = currentState.copy(preparingShareText = false) + + savedFile?.let { showShareForFile.invoke(it, shareTextData) } } } @@ -183,6 +193,7 @@ class StoriesViewModel @Inject constructor( data class Loaded( val currentStory: Story?, val segmentsData: SegmentsData, + val preparingShareText: Boolean = false, ) : State() { data class SegmentsData( val widths: List = emptyList(), diff --git a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt index 46a48c31cfb..7975af26383 100644 --- a/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt +++ b/modules/features/endofyear/src/main/java/au/com/shiftyjelly/pocketcasts/endofyear/views/SnapShot.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.viewinterop.AndroidView import au.com.shiftyjelly.pocketcasts.endofyear.StoriesViewAspectRatioForTablet import au.com.shiftyjelly.pocketcasts.utils.Util import au.com.shiftyjelly.pocketcasts.utils.extensions.deviceAspectRatio +import au.com.shiftyjelly.pocketcasts.utils.extensions.dpToPx /* Returns a callback to get bitmap for the passed composable. The composable is converted to ComposeView and laid out into AndroidView otherwise an illegal state exception is thrown: @@ -37,10 +38,12 @@ fun convertibleToBitmap( ) return { + val height = composeView.width * getAspectRatioForBitmap(context) + val availableHeight = height - 50.dpToPx(context) // Reduce approx share button height createBitmapFromView( view = composeView, width = composeView.width, - height = (composeView.width * getAspectRatioForBitmap(context)).toInt() + height = availableHeight.toInt() ) } } diff --git a/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt b/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt index f675cb6c556..2f6c858c1e5 100644 --- a/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt +++ b/modules/features/endofyear/src/test/java/au/com/shiftyjelly/pocketcasts/endofyear/StoriesViewModelTest.kt @@ -112,7 +112,8 @@ class StoriesViewModelTest { whenever(endOfYearManager.loadStories()).thenReturn(flowOf(mockStories)) return StoriesViewModel( endOfYearManager = endOfYearManager, - fileUtilWrapper = fileUtilWrapper + fileUtilWrapper = fileUtilWrapper, + shareableTextProvider = mock() ) } } diff --git a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/dao/EpisodeDao.kt b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/dao/EpisodeDao.kt index 390de6aa733..e3077288b7d 100644 --- a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/dao/EpisodeDao.kt +++ b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/dao/EpisodeDao.kt @@ -339,7 +339,7 @@ abstract class EpisodeDao { @Query( """ - SELECT episodes.title, episodes.duration, podcasts.uuid as podcastUuid, podcasts.title as podcastTitle, podcasts.primary_color as tintColorForLightBg, podcasts.secondary_color as tintColorForDarkBg + SELECT episodes.uuid, episodes.title, episodes.duration, podcasts.uuid as podcastUuid, podcasts.title as podcastTitle, podcasts.primary_color as tintColorForLightBg, podcasts.secondary_color as tintColorForDarkBg FROM episodes JOIN podcasts ON episodes.podcast_id = podcasts.uuid WHERE episodes.last_playback_interaction_date IS NOT NULL AND episodes.last_playback_interaction_date > :fromEpochMs AND episodes.last_playback_interaction_date < :toEpochMs diff --git a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/helper/LongestEpisode.kt b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/helper/LongestEpisode.kt index bb64aa9e6f0..8ba43df8a25 100644 --- a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/helper/LongestEpisode.kt +++ b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/helper/LongestEpisode.kt @@ -3,6 +3,7 @@ package au.com.shiftyjelly.pocketcasts.models.db.helper import au.com.shiftyjelly.pocketcasts.models.entity.Podcast data class LongestEpisode( + val uuid: String, val title: String, val duration: Double, val podcastUuid: String,