mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-04 00:47:00 +01:00
added icons instead of text
This commit is contained in:
1 parent
f4961fd6a8
commit
b399f84f54
7 files changed
+201
-33
No files matched your search
@@ -10,6 +10,7 @@
|
||||
|
||||
# Android and Gradle generated state.
|
||||
/android/.gradle/
|
||||
/android/.kotlin/
|
||||
/android/build/
|
||||
/android/*/build/
|
||||
/android/local.properties
|
||||
|
||||
Generated
+7
-7
@@ -4262,7 +4262,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-android"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"jni 0.21.1",
|
||||
@@ -4281,7 +4281,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-app"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"eframe",
|
||||
@@ -4303,7 +4303,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-cli"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -4321,7 +4321,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-core"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"futures",
|
||||
@@ -4339,7 +4339,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-meta"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"pstr-core",
|
||||
"reqwest",
|
||||
@@ -4352,7 +4352,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-player"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"libloading",
|
||||
@@ -4366,7 +4366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pstr-stream"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/narrrl/proton-stream"
|
||||
|
||||
@@ -215,6 +215,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.androidx.compose.material3.navigation.suite)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
@@ -14,12 +14,27 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Fullscreen
|
||||
import androidx.compose.material.icons.filled.FullscreenExit
|
||||
import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.VolumeOff
|
||||
import androidx.compose.material.icons.filled.Subtitles
|
||||
import androidx.compose.material.icons.filled.Audiotrack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -36,6 +51,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import io.narl.protonstream.native.NativeRuntime
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uniffi.pstr_android.AndroidEngine
|
||||
@@ -113,7 +129,13 @@ private data class PlaybackStartup(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LibmpvPlayerSurface(titleKey: String, episode: EpisodeRecord, host: LibmpvHost? = null) {
|
||||
fun LibmpvPlayerSurface(
|
||||
titleKey: String,
|
||||
episode: EpisodeRecord,
|
||||
host: LibmpvHost? = null,
|
||||
isFullscreen: Boolean = false,
|
||||
onToggleFullscreen: () -> Unit = {}
|
||||
) {
|
||||
if (host == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Playback is unavailable: this build does not include libmpv.", color = Color.White)
|
||||
@@ -156,7 +178,20 @@ fun LibmpvPlayerSurface(titleKey: String, episode: EpisodeRecord, host: LibmpvHo
|
||||
scope.launch { session.closeEngine() }
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
|
||||
var showControls by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(showControls) {
|
||||
if (showControls) {
|
||||
delay(4000)
|
||||
showControls = false
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize().clickable(
|
||||
interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { showControls = !showControls }
|
||||
)) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
@@ -169,7 +204,20 @@ fun LibmpvPlayerSurface(titleKey: String, episode: EpisodeRecord, host: LibmpvHo
|
||||
}
|
||||
},
|
||||
)
|
||||
if (host is NativeMpvHost) PlayerControls(titleKey, episode, host, Modifier.align(Alignment.BottomCenter))
|
||||
AnimatedVisibility(
|
||||
visible = showControls,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
if (host is NativeMpvHost) PlayerControls(
|
||||
titleKey,
|
||||
episode,
|
||||
host,
|
||||
isFullscreen,
|
||||
onToggleFullscreen
|
||||
)
|
||||
}
|
||||
playbackError?.let { message ->
|
||||
Text(message, color = Color.White, modifier = Modifier.align(Alignment.Center).background(Color.Black.copy(alpha = 0.8f)).padding(16.dp))
|
||||
}
|
||||
@@ -179,7 +227,14 @@ fun LibmpvPlayerSurface(titleKey: String, episode: EpisodeRecord, host: LibmpvHo
|
||||
private fun sessionKey(episode: EpisodeRecord) = "${episode.shareId}:${episode.linkId}"
|
||||
|
||||
@Composable
|
||||
private fun PlayerControls(titleKey: String, episode: EpisodeRecord, host: NativeMpvHost, modifier: Modifier = Modifier) {
|
||||
private fun PlayerControls(
|
||||
titleKey: String,
|
||||
episode: EpisodeRecord,
|
||||
host: NativeMpvHost,
|
||||
isFullscreen: Boolean,
|
||||
onToggleFullscreen: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val state by host.state.collectAsState()
|
||||
val tracks by host.tracks.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -199,17 +254,32 @@ private fun PlayerControls(titleKey: String, episode: EpisodeRecord, host: Nativ
|
||||
valueRange = 0f..state.duration.toFloat().coerceAtLeast(1f),
|
||||
)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Button(onClick = { host.setPaused(!state.paused) }) { Text(if (state.paused) "Play" else "Pause") }
|
||||
IconButton(onClick = { host.setPaused(!state.paused) }) {
|
||||
Icon(if (state.paused) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = "Play/Pause", tint = Color.White)
|
||||
}
|
||||
Text("${formatTime(state.position)} / ${formatTime(state.duration)}", color = Color.White)
|
||||
Button(onClick = { host.setMuted(!state.muted) }) { Text(if (state.muted) "Unmute" else "Mute") }
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
IconButton(onClick = { host.setMuted(!state.muted) }) {
|
||||
Icon(if (state.muted) Icons.Default.VolumeOff else Icons.Default.VolumeUp, contentDescription = "Mute", tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = onToggleFullscreen) {
|
||||
Icon(if (isFullscreen) Icons.Default.FullscreenExit else Icons.Default.Fullscreen, contentDescription = "Fullscreen", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { choosing = "audio" }) {
|
||||
Text("Audio: ${tracks.firstOrNull { it.type == "audio" && it.selected }?.label ?: "default"}", maxLines = 1)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { choosing = "audio" }) {
|
||||
Icon(Icons.Default.Audiotrack, contentDescription = "Audio", tint = Color.White)
|
||||
}
|
||||
Button(onClick = { choosing = "sub" }) {
|
||||
Text("Subtitles: ${tracks.firstOrNull { it.type == "sub" && it.selected }?.label ?: "off"}", maxLines = 1)
|
||||
IconButton(onClick = { choosing = "sub" }) {
|
||||
Icon(Icons.Default.Subtitles, contentDescription = "Subtitles", tint = Color.White)
|
||||
}
|
||||
Text(
|
||||
"Audio: ${tracks.firstOrNull { it.type == "audio" && it.selected }?.label ?: "default"} • " +
|
||||
"Subtitles: ${tracks.firstOrNull { it.type == "sub" && it.selected }?.label ?: "off"}",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
choosing?.let { type ->
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.narl.protonstream.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -41,8 +43,20 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.VideoLibrary
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -54,6 +68,8 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
@@ -82,11 +98,11 @@ import uniffi.pstr_android.MetadataProvider
|
||||
import io.narl.protonstream.playback.NativeMpvHost
|
||||
import io.narl.protonstream.playback.LibmpvPlayerSurface
|
||||
|
||||
private enum class Destination(val label: String, val glyph: String) {
|
||||
Library("Library", "▦"),
|
||||
Shares("Shares", "⇄"),
|
||||
Downloads("Downloads", "⇩"),
|
||||
Settings("Settings", "⚙"),
|
||||
private enum class Destination(val label: String, val icon: ImageVector) {
|
||||
Library("Library", Icons.Default.VideoLibrary),
|
||||
Shares("Shares", Icons.Default.Share),
|
||||
Downloads("Downloads", Icons.Default.Download),
|
||||
Settings("Settings", Icons.Default.Settings),
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -123,7 +139,7 @@ fun ProtonStreamApp(
|
||||
item(
|
||||
selected = destination == item,
|
||||
onClick = { destination = item; selectedTitle = null },
|
||||
icon = { Text(item.glyph) },
|
||||
icon = { Icon(item.icon, contentDescription = item.label) },
|
||||
label = { Text(item.label) },
|
||||
)
|
||||
}
|
||||
@@ -136,10 +152,14 @@ fun ProtonStreamApp(
|
||||
title = { Text(selectedTitle?.name ?: "proton-stream") },
|
||||
actions = {
|
||||
if (destination == Destination.Library) {
|
||||
FilledTonalButton(onClick = model::refresh, enabled = !state.refreshing) {
|
||||
Text(if (state.refreshing) "Refreshing…" else "Refresh")
|
||||
if (state.refreshing) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
Spacer(Modifier.width(16.dp))
|
||||
} else {
|
||||
IconButton(onClick = model::refresh) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -190,6 +210,7 @@ private fun LibraryScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
placeholder = { Text("Search library") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (state.loading) {
|
||||
@@ -252,9 +273,73 @@ private fun TitleScreen(
|
||||
}.onFailure(onPreferenceError)
|
||||
}
|
||||
if (playing != null) {
|
||||
Box(Modifier.fillMaxSize().padding(padding).background(Color.Black)) {
|
||||
LibmpvPlayerSurface(title.key, playing!!, playerHost)
|
||||
FilledTonalButton(onClick = { playerHost?.stop(); playing = null }, Modifier.padding(16.dp)) { Text("Back") }
|
||||
androidx.compose.ui.window.Dialog(
|
||||
onDismissRequest = {
|
||||
playerHost?.stop()
|
||||
playing = null
|
||||
},
|
||||
properties = androidx.compose.ui.window.DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false
|
||||
)
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
var isFullscreen by remember { mutableStateOf(isLandscape) }
|
||||
val activity = context as? android.app.Activity
|
||||
|
||||
LaunchedEffect(isLandscape, isFullscreen) {
|
||||
val window = activity?.window ?: return@LaunchedEffect
|
||||
val controller = androidx.core.view.WindowInsetsControllerCompat(window, window.decorView)
|
||||
if (isLandscape || isFullscreen) {
|
||||
controller.hide(androidx.core.view.WindowInsetsCompat.Type.systemBars())
|
||||
controller.systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
if (isFullscreen && !isLandscape) {
|
||||
activity.requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
}
|
||||
} else {
|
||||
controller.show(androidx.core.view.WindowInsetsCompat.Type.systemBars())
|
||||
activity.requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
val window = activity?.window
|
||||
if (window != null) {
|
||||
val controller = androidx.core.view.WindowInsetsControllerCompat(window, window.decorView)
|
||||
controller.show(androidx.core.view.WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
activity?.requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
if (isFullscreen && !isLandscape) {
|
||||
isFullscreen = false
|
||||
} else {
|
||||
playerHost?.stop()
|
||||
playing = null
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize().background(Color.Black)) {
|
||||
LibmpvPlayerSurface(
|
||||
title.key,
|
||||
playing!!,
|
||||
playerHost,
|
||||
isFullscreen = isLandscape || isFullscreen,
|
||||
onToggleFullscreen = { isFullscreen = !isFullscreen }
|
||||
)
|
||||
if (!isLandscape && !isFullscreen) {
|
||||
IconButton(
|
||||
onClick = { playerHost?.stop(); playing = null },
|
||||
Modifier.padding(padding).padding(16.dp).background(Color.Black.copy(alpha = 0.5f), androidx.compose.foundation.shape.CircleShape)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close player", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -264,7 +349,11 @@ private fun TitleScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
item {
|
||||
FilledTonalButton(onClick = onBack) { Text("Back to library") }
|
||||
TextButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Back to library")
|
||||
}
|
||||
RemoteArtwork(
|
||||
title.backdropUrl ?: title.posterUrl,
|
||||
title.canonicalName ?: title.name,
|
||||
@@ -286,9 +375,15 @@ private fun TitleScreen(
|
||||
title.overview?.let { Text(it, modifier = Modifier.padding(top = 10.dp)) }
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 12.dp)) {
|
||||
Button(onClick = { DownloadCoordinator.enqueue(context, title.seasons.flatMap { it.episodes }) }) {
|
||||
Icon(Icons.Default.Download, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Download show")
|
||||
}
|
||||
FilledTonalButton(onClick = { showMatch = true }) { Text("Change match") }
|
||||
FilledTonalButton(onClick = { showMatch = true }) {
|
||||
Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Change match")
|
||||
}
|
||||
}
|
||||
Text("${title.watchedCount} of ${title.episodeCount} watched", Modifier.padding(vertical = 12.dp))
|
||||
Text("Preferred tracks", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
@@ -28,6 +28,7 @@ androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime
|
||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
|
||||
androidx-compose-material3-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "adaptive" }
|
||||
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
rustls-platform-verifier = { module = "rustls:rustls-platform-verifier", version = "0.1.1" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
|
||||
Reference in new issue
Block a user