diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 010c0901..f3452e12 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "dev.typetype.android" minSdk = 23 targetSdk = 37 - versionCode = 10710 - versionName = "1.7.0-beta.11" + versionCode = 10711 + versionName = "1.7.0-beta.12" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resValue("string", "app_name", "TypeType") } diff --git a/app/src/main/java/dev/typetype/android/core/ui/share/ShareUrls.kt b/app/src/main/java/dev/typetype/android/core/ui/share/ShareUrls.kt index 5aef0f6c..25d473b5 100644 --- a/app/src/main/java/dev/typetype/android/core/ui/share/ShareUrls.kt +++ b/app/src/main/java/dev/typetype/android/core/ui/share/ShareUrls.kt @@ -1,6 +1,20 @@ package dev.typetype.android.core.ui.share +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView import androidx.compose.runtime.compositionLocalOf +import dev.typetype.android.R +import dev.typetype.android.domain.navigation.resolveIncomingVideoUrl import dev.typetype.android.domain.navigation.toPublicWatchParameter import java.net.URI import java.net.URLEncoder @@ -19,6 +33,92 @@ fun buildShareUrl(serverBaseUrl: String?, videoUrl: String): String { return "$origin/watch?v=$encoded" } +fun buildSourceShareUrl(videoUrl: String): String = + resolveIncomingVideoUrl(videoUrl) ?: videoUrl.trim() + +fun showShareChooser( + context: Context, + serverBaseUrl: String?, + videoUrl: String, + chooserTitle: String, +) { + val sourceUrl = buildSourceShareUrl(videoUrl) + val typeTypeUrl = buildShareUrl(serverBaseUrl, videoUrl) + val choices = buildList { + add( + ShareChoice( + context.getString(R.string.video_menu_share_typetype), + typeTypeUrl, + android.R.drawable.ic_menu_share, + ), + ) + providerChoice(context, sourceUrl)?.takeIf { it.url != typeTypeUrl }?.let(::add) + } + AlertDialog.Builder(context) + .setTitle(R.string.video_menu_share) + .setAdapter(ShareChoiceAdapter(context, choices)) { _, which -> + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, choices[which].url) + } + context.startActivity(Intent.createChooser(intent, chooserTitle)) + } + .show() +} + +private data class ShareChoice(val label: String, val url: String, val icon: Int) + +private fun providerChoice(context: Context, sourceUrl: String): ShareChoice? { + val provider = sourceUrl.lowercase() + return when { + "youtube.com" in provider || "youtu.be" in provider -> ShareChoice( + context.getString(R.string.video_menu_share_source, "YouTube"), + sourceUrl, + R.drawable.ic_service_youtube, + ) + "nicovideo.jp" in provider || "nico.ms" in provider -> ShareChoice( + context.getString(R.string.video_menu_share_source, "NicoNico"), + sourceUrl, + R.drawable.ic_service_niconico, + ) + "bilibili.com" in provider || "b23.tv" in provider -> ShareChoice( + context.getString(R.string.video_menu_share_source, "BiliBili"), + sourceUrl, + R.drawable.ic_service_bilibili, + ) + else -> null + } +} + +private class ShareChoiceAdapter( + private val context: Context, + private val choices: List, +) : BaseAdapter() { + override fun getCount(): Int = choices.size + override fun getItem(position: Int): ShareChoice = choices[position] + override fun getItemId(position: Int): Long = position.toLong() + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val choice = getItem(position) + val density = context.resources.displayMetrics.density + val row = (convertView as? LinearLayout) ?: LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + background = ColorDrawable(Color.TRANSPARENT) + val icon = ImageView(context) + addView(icon, LinearLayout.LayoutParams((28 * density).toInt(), (28 * density).toInt())) + val label = TextView(context).apply { + setTextAppearance(android.R.style.TextAppearance_Material_Body1) + } + addView(label, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)) + } + row.setPadding((20 * density).toInt(), (12 * density).toInt(), (20 * density).toInt(), (12 * density).toInt()) + (row.getChildAt(0) as ImageView).setImageResource(choice.icon) + (row.getChildAt(1) as TextView).text = choice.label + return row + } +} + fun buildImageUrl(serverBaseUrl: String?, imageUrl: String): String { val source = imageUrl.trim().let { value -> if (value.startsWith("httpss://")) "https://${value.removePrefix("httpss://")}" else value diff --git a/app/src/main/java/dev/typetype/android/feature/library/LibraryPlaylistContextTab.kt b/app/src/main/java/dev/typetype/android/feature/library/LibraryPlaylistContextTab.kt index cb7f09be..4ecfd202 100644 --- a/app/src/main/java/dev/typetype/android/feature/library/LibraryPlaylistContextTab.kt +++ b/app/src/main/java/dev/typetype/android/feature/library/LibraryPlaylistContextTab.kt @@ -1,6 +1,5 @@ package dev.typetype.android.feature.library -import android.content.Intent import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.grid.GridCells @@ -24,7 +23,7 @@ import dev.typetype.android.core.ui.components.LocalAppSnackbarHost import dev.typetype.android.core.ui.components.PlaylistVideoActionsSheet import dev.typetype.android.core.ui.components.PlaylistVideoCard import dev.typetype.android.core.ui.share.LocalServerBaseUrl -import dev.typetype.android.core.ui.share.buildShareUrl +import dev.typetype.android.core.ui.share.showShareChooser import dev.typetype.android.domain.library.PlaylistVideo import dev.typetype.android.feature.library.components.rememberVideoMetas import dev.typetype.android.feature.menu.VideoMenuEvent @@ -104,11 +103,7 @@ fun PlaylistContextTab( onRemoveFromList = { onRemove(video) }, onToggleWatched = { onToggleWatched(video, video.url in watchedUrls) }, onShare = { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, buildShareUrl(serverBaseUrl, video.url)) - } - context.startActivity(Intent.createChooser(intent, shareChooserTitle)) + showShareChooser(context, serverBaseUrl, video.url, shareChooserTitle) }, onBlockVideo = { onBlockVideo(video) }, onDismiss = { pendingMenu = null }, diff --git a/app/src/main/java/dev/typetype/android/feature/library/playlist/PlaylistScreen.kt b/app/src/main/java/dev/typetype/android/feature/library/playlist/PlaylistScreen.kt index 0f5f4610..84db207a 100644 --- a/app/src/main/java/dev/typetype/android/feature/library/playlist/PlaylistScreen.kt +++ b/app/src/main/java/dev/typetype/android/feature/library/playlist/PlaylistScreen.kt @@ -1,6 +1,5 @@ package dev.typetype.android.feature.library.playlist -import android.content.Intent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -41,6 +40,7 @@ import dev.typetype.android.feature.menu.VideoMenuHandlerViewModel import dev.typetype.android.feature.menu.blockVideoUrl import dev.typetype.android.feature.menu.removeFromPlaylist import dev.typetype.android.feature.menu.toggleWatchedUrl +import dev.typetype.android.core.ui.share.showShareChooser @Composable fun PlaylistRoute( @@ -253,17 +253,7 @@ internal fun PlaylistScreen( ) }, onShare = { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - dev.typetype.android.core.ui.share.buildShareUrl( - serverBaseUrl, - video.url, - ), - ) - } - context.startActivity(Intent.createChooser(intent, shareChooserTitle)) + showShareChooser(context, serverBaseUrl, video.url, shareChooserTitle) }, onBlockVideo = { menuVm.blockVideoUrl(video.url) }, onDismiss = { pendingMenu = null }, diff --git a/app/src/main/java/dev/typetype/android/feature/menu/VideoMenuHost.kt b/app/src/main/java/dev/typetype/android/feature/menu/VideoMenuHost.kt index 9c1d5b0b..ede80d50 100644 --- a/app/src/main/java/dev/typetype/android/feature/menu/VideoMenuHost.kt +++ b/app/src/main/java/dev/typetype/android/feature/menu/VideoMenuHost.kt @@ -1,6 +1,5 @@ package dev.typetype.android.feature.menu -import android.content.Intent import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -19,7 +18,7 @@ import dev.typetype.android.core.ui.components.LocalAppSnackbarHost import dev.typetype.android.core.ui.components.VideoMenuAction import dev.typetype.android.core.ui.components.VideoMenuItemState import dev.typetype.android.core.ui.share.LocalServerBaseUrl -import dev.typetype.android.core.ui.share.buildShareUrl +import dev.typetype.android.core.ui.share.showShareChooser import dev.typetype.android.domain.feed.Video import dev.typetype.android.domain.navigation.canonicalVideoIdentity import dev.typetype.android.domain.actions.titleMatchesBlockedKeyword @@ -126,11 +125,7 @@ fun rememberVideoMenuScope( ) VideoMenuAction.Download -> downloadVideo = video VideoMenuAction.Share -> { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, buildShareUrl(serverBaseUrl, video.url)) - } - context.startActivity(Intent.createChooser(intent, shareChooserTitle)) + showShareChooser(context, serverBaseUrl, video.url, shareChooserTitle) } VideoMenuAction.OpenChannel -> onOpenChannel(video.uploaderUrl) VideoMenuAction.BlockVideo -> viewModel.blockVideo(video) diff --git a/app/src/main/java/dev/typetype/android/feature/player/LoadedPlayer.kt b/app/src/main/java/dev/typetype/android/feature/player/LoadedPlayer.kt index 9106a6fc..79c48faa 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/LoadedPlayer.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/LoadedPlayer.kt @@ -5,6 +5,7 @@ import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.padding @@ -25,7 +26,6 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.unit.Constraints import androidx.compose.ui.platform.LocalContext import androidx.paging.PagingData import dev.typetype.android.domain.comments.Comment @@ -188,6 +188,8 @@ fun LoadedPlayer( onSaveProgress = { onAction(PlayerAction.OnSaveProgress(it)) }, ) + val expandedTopPadding = WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() + val expandedTopPaddingPx = with(LocalDensity.current) { expandedTopPadding.toPx() } val autoplayVisible by remember(hostTransitionProgress) { derivedStateOf { hostTransitionProgress() < 0.01f } } @@ -195,6 +197,16 @@ fun LoadedPlayer( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) + .then( + if (isFullscreen) { + Modifier + } else { + Modifier.playerTopProgressPadding( + maxTopPx = expandedTopPaddingPx, + progress = hostTransitionProgress, + ) + }, + ), ) { PlayerContentLayout( isFullscreen = isFullscreen, diff --git a/app/src/main/java/dev/typetype/android/feature/player/PlayerInteractionRow.kt b/app/src/main/java/dev/typetype/android/feature/player/PlayerInteractionRow.kt index 062573e6..cbcc5bac 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/PlayerInteractionRow.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/PlayerInteractionRow.kt @@ -1,6 +1,5 @@ package dev.typetype.android.feature.player -import android.content.Intent import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -30,7 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.typetype.android.R import dev.typetype.android.core.ui.share.LocalServerBaseUrl -import dev.typetype.android.core.ui.share.buildShareUrl +import dev.typetype.android.core.ui.share.showShareChooser @Composable fun PlayerInteractionRow( @@ -104,11 +103,7 @@ fun PlayerInteractionRow( icon = Icons.Filled.Share, contentDescription = stringResource(R.string.video_menu_share), onClick = { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, buildShareUrl(serverBaseUrl, shareUrl)) - } - context.startActivity(Intent.createChooser(intent, shareChooserTitle)) + showShareChooser(context, serverBaseUrl, shareUrl, shareChooserTitle) }, ) } diff --git a/app/src/main/java/dev/typetype/android/feature/player/PlayerTopProgressPadding.kt b/app/src/main/java/dev/typetype/android/feature/player/PlayerTopProgressPadding.kt new file mode 100644 index 00000000..03b78187 --- /dev/null +++ b/app/src/main/java/dev/typetype/android/feature/player/PlayerTopProgressPadding.kt @@ -0,0 +1,25 @@ +package dev.typetype.android.feature.player + +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import kotlin.math.roundToInt + +internal fun Modifier.playerTopProgressPadding( + maxTopPx: Float, + progress: () -> Float, +): Modifier = layout { measurable, constraints -> + val topPx = (maxTopPx * (1f - progress().coerceIn(0f, 1f))).roundToInt() + val maxHeight = if (constraints.hasBoundedHeight) { + (constraints.maxHeight - topPx).coerceAtLeast(0) + } else { + constraints.maxHeight + } + val childConstraints = constraints.copy( + minHeight = constraints.minHeight.coerceAtMost(maxHeight), + maxHeight = maxHeight, + ) + val placeable = measurable.measure(childConstraints) + layout(placeable.width, placeable.height + topPx) { + placeable.placeRelative(0, topPx) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c05d5139..301ad0a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -921,6 +921,8 @@ Block video Block channel Share via + Share TypeType link + Share %1$s link Video actions Open %1$s channel Open %1$s diff --git a/app/src/test/java/dev/typetype/android/core/ui/share/ShareUrlsTest.kt b/app/src/test/java/dev/typetype/android/core/ui/share/ShareUrlsTest.kt index 773bfa67..04885088 100644 --- a/app/src/test/java/dev/typetype/android/core/ui/share/ShareUrlsTest.kt +++ b/app/src/test/java/dev/typetype/android/core/ui/share/ShareUrlsTest.kt @@ -5,6 +5,22 @@ import org.junit.Test class ShareUrlsTest { + @Test + fun sharesCanonicalProviderSourceLinks() { + assertEquals( + "https://www.youtube.com/watch?v=AbCdEfGhI_1", + buildSourceShareUrl("AbCdEfGhI_1"), + ) + assertEquals( + "https://www.bilibili.com/video/BV1UbX3B2EZQ?p=3", + buildSourceShareUrl("BV1UbX3B2EZQ?p=3"), + ) + assertEquals( + "https://www.nicovideo.jp/watch/sm46525483", + buildSourceShareUrl("sm46525483"), + ) + } + @Test fun sharesCompactPublicWatchRoutes() { assertEquals(