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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
100 changes: 100 additions & 0 deletions app/src/main/java/dev/typetype/android/core/ui/share/ShareUrls.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<ShareChoice>,
) : 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -188,13 +188,25 @@ 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 }
}
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.then(
if (isFullscreen) {
Modifier
} else {
Modifier.playerTopProgressPadding(
maxTopPx = expandedTopPaddingPx,
progress = hostTransitionProgress,
)
},
),
) {
PlayerContentLayout(
isFullscreen = isFullscreen,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,8 @@
<string name="video_menu_block_video">Block video</string>
<string name="video_menu_block_channel">Block channel</string>
<string name="video_menu_share_chooser">Share via</string>
<string name="video_menu_share_typetype">Share TypeType link</string>
<string name="video_menu_share_source">Share %1$s link</string>
<string name="video_more_actions">Video actions</string>
<string name="video_open_channel_accessibility">Open %1$s channel</string>
<string name="video_open_accessibility">Open %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down