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 = 10713
versionName = "1.7.0-beta.14"
versionCode = 10714
versionName = "1.7.0-beta.15"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resValue("string", "app_name", "TypeType")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package dev.typetype.android.core.ui.share

import android.content.Context
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
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.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import dev.typetype.android.R

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ShareChooserSheet(
serverBaseUrl: String?,
videoUrl: String,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val choices = buildShareChoices(serverBaseUrl, videoUrl)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = MaterialTheme.colorScheme.surface,
) {
Column(
modifier = Modifier.fillMaxWidth().padding(start = 20.dp, end = 20.dp, bottom = 28.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResource(R.string.video_menu_share_sheet_title),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
)
Text(
text = stringResource(R.string.video_menu_share_sheet_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp),
)
choices.forEach { choice ->
ShareTargetRow(
choice = choice,
onClick = {
shareUrl(context, choice.url)
onDismiss()
},
)
}
}
}
}

@Composable
private fun ShareTargetRow(
choice: ShareChoice,
onClick: () -> Unit,
) {
val isTypeType = choice.target == ShareTarget.TypeType
val providerName = choice.providerName.orEmpty()
val iconResource = when {
isTypeType -> R.drawable.ic_typetype_brand
providerName == "YouTube" -> R.drawable.ic_service_youtube
providerName == "NicoNico" -> R.drawable.ic_service_niconico
else -> R.drawable.ic_service_bilibili
}
val iconBackground = when {
isTypeType -> MaterialTheme.colorScheme.surfaceVariant
providerName == "YouTube" -> Color(0xFFCC0000)
providerName == "NicoNico" -> Color(0xFFCC6688)
else -> Color(0xFFEA4C89)
}
val title = if (isTypeType) {
stringResource(R.string.video_menu_share_target_typetype)
} else {
stringResource(R.string.video_menu_share_target_source, providerName)
}
val subtitle = if (isTypeType) {
stringResource(R.string.video_menu_share_target_typetype_description)
} else {
stringResource(R.string.video_menu_share_target_source_description, providerName)
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.42f))
.clickable(onClick = onClick)
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(14.dp))
.background(iconBackground),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(iconResource),
contentDescription = null,
tint = if (isTypeType) Color.Unspecified else Color.White,
modifier = Modifier.size(28.dp),
)
}
Spacer(Modifier.width(14.dp))
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Icon(
imageVector = Icons.Filled.ChevronRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

private fun shareUrl(context: Context, url: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
context.startActivity(
Intent.createChooser(intent, context.getString(R.string.video_menu_share_chooser)),
)
}
111 changes: 28 additions & 83 deletions app/src/main/java/dev/typetype/android/core/ui/share/ShareUrls.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
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
Expand All @@ -36,89 +23,47 @@ fun buildShareUrl(serverBaseUrl: String?, videoUrl: String): String {
fun buildSourceShareUrl(videoUrl: String): String =
resolveIncomingVideoUrl(videoUrl) ?: videoUrl.trim()

fun showShareChooser(
context: Context,
internal enum class ShareTarget {
TypeType,
Source,
}

internal data class ShareChoice(
val target: ShareTarget,
val url: String,
val providerName: String? = null,
)

internal fun buildShareChoices(
serverBaseUrl: String?,
videoUrl: String,
chooserTitle: String,
) {
): List<ShareChoice> {
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))
return buildList {
add(ShareChoice(target = ShareTarget.TypeType, url = typeTypeUrl))
sourceProvider(sourceUrl)?.takeIf { sourceUrl != typeTypeUrl }?.let { provider ->
add(
ShareChoice(
target = ShareTarget.Source,
url = sourceUrl,
providerName = provider,
),
)
}
.show()
}
}

private data class ShareChoice(val label: String, val url: String, val icon: Int)

private fun providerChoice(context: Context, sourceUrl: String): ShareChoice? {
private fun sourceProvider(sourceUrl: String): String? {
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,
)
"youtube.com" in provider || "youtu.be" in provider -> "YouTube"
"nicovideo.jp" in provider || "nico.ms" in provider -> "NicoNico"
"bilibili.com" in provider || "b23.tv" in provider -> "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
Expand Up @@ -13,7 +13,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
Expand All @@ -23,7 +22,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.showShareChooser
import dev.typetype.android.core.ui.share.ShareChooserSheet
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 @@ -67,10 +66,9 @@ fun PlaylistContextTab(
EmptyTab(emptyMessageFor(filter, emptyDefault))
return
}
val context = LocalContext.current
val shareChooserTitle = stringResource(R.string.video_menu_share_chooser)
val serverBaseUrl = LocalServerBaseUrl.current
var pendingMenu by remember { mutableStateOf<PlaylistVideo?>(null) }
var shareVideoUrl by remember { mutableStateOf<String?>(null) }
val urlsMissingInfo = items
.filter { it.channelAvatarUrl.isBlank() || it.channelName.isBlank() }
.map { it.url }
Expand Down Expand Up @@ -102,11 +100,16 @@ fun PlaylistContextTab(
onAddToQueue = { onAddToQueue(video) },
onRemoveFromList = { onRemove(video) },
onToggleWatched = { onToggleWatched(video, video.url in watchedUrls) },
onShare = {
showShareChooser(context, serverBaseUrl, video.url, shareChooserTitle)
},
onShare = { shareVideoUrl = video.url },
onBlockVideo = { onBlockVideo(video) },
onDismiss = { pendingMenu = null },
)
}
shareVideoUrl?.let { videoUrl ->
ShareChooserSheet(
serverBaseUrl = serverBaseUrl,
videoUrl = videoUrl,
onDismiss = { shareVideoUrl = null },
)
}
}
Loading