updated readme + renamed buttons for android client

This commit is contained in:
nvrl committed 2026-08-05 14:07:52 +02:00
1 parent b399f84f54
commit 359d397a69
2 files changed
+54 -22

No files matched your search

+5 -4
View File
@@ -1,6 +1,6 @@
# proton-stream # proton-stream
A Netflix-style desktop client for **Proton Drive public links**. A Netflix-style cross-platform client for **Proton Drive public links**.
Paste a share URL and its password; get a browsable, streamable library. No Paste a share URL and its password; get a browsable, streamable library. No
Proton account, no server to host, no download step — episodes start playing in Proton account, no server to host, no download step — episodes start playing in
@@ -29,8 +29,9 @@ countdown, optional metadata, themes and share management. What is left is mostl
macOS packaging on a real Mac. See `docs/DEVELOPMENT.md`. macOS packaging on a real Mac. See `docs/DEVELOPMENT.md`.
A native Android 12+ client is under `android/`, sharing the Rust catalog and A native Android 12+ client is under `android/`, sharing the Rust catalog and
streaming implementation through UniFFI. Its toolchain, emulator, signing, and streaming implementation through UniFFI. It features a modern Material 3 interface,
release instructions are in [`docs/ANDROID.md`](docs/ANDROID.md). an immersive fullscreen player with auto-hiding controls, and offline downloads.
Its toolchain, emulator, signing, and release instructions are in [`docs/ANDROID.md`](docs/ANDROID.md).
```bash ```bash
cargo run --release -p pstr-app # the app cargo run --release -p pstr-app # the app
@@ -68,7 +69,7 @@ against the SDK's own range download.
| UI | egui/eframe on glow; mpv renders into the same GL context. | | UI | egui/eframe on glow; mpv renders into the same GL context. |
| Themes | The shipped near-black palette plus Catppuccin Latte, Frappé, Macchiato and Mocha, each with a choice of eight accents — pink and sky among them — drawn as a gradient unless you turn that off. Picked on the Shares page, applied without a restart. | | Themes | The shipped near-black palette plus Catppuccin Latte, Frappé, Macchiato and Mocha, each with a choice of eight accents — pink and sky among them — drawn as a gradient unless you turn that off. Picked on the Shares page, applied without a restart. |
| Metadata | Filename parsing, with optional AniList/TMDB enrichment — **off by default**, because enabling it sends your library's titles to a third party. It brings posters, synopses and per-episode titles; a title the scorer refuses to guess about can be pinned by hand, and a pinned match is never overwritten. | | Metadata | Filename parsing, with optional AniList/TMDB enrichment — **off by default**, because enabling it sends your library's titles to a third party. It brings posters, synopses and per-episode titles; a title the scorer refuses to guess about can be pinned by hand, and a pinned match is never overwritten. |
| Platforms | Linux and Windows; macOS packaging is written but has not been run on a Mac. The Android 12+ phone/tablet client is under active development. | | Platforms | Linux and Windows; macOS packaging is written but has not been run on a Mac. A native Android 12+ client is available featuring a Material 3 UI and immersive fullscreen playback. |
## Building ## Building
@@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
@@ -53,6 +55,9 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -261,6 +266,10 @@ private fun TitleScreen(
var subtitlesEnabled by remember(title.key) { mutableStateOf(false) } var subtitlesEnabled by remember(title.key) { mutableStateOf(false) }
var playing by remember(title.key) { mutableStateOf<EpisodeRecord?>(null) } var playing by remember(title.key) { mutableStateOf<EpisodeRecord?>(null) }
var showMatch by remember(title.key) { mutableStateOf(false) } var showMatch by remember(title.key) { mutableStateOf(false) }
var expandedSeasons by remember(title.key) { mutableStateOf(setOf(title.seasons.firstOrNull()?.label)) }
BackHandler(enabled = playing == null, onBack = onBack)
LaunchedEffect(title.key) { LaunchedEffect(title.key) {
runCatching { runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -422,32 +431,50 @@ private fun TitleScreen(
}) { Text("Save track preferences") } }) { Text("Save track preferences") }
} }
title.seasons.forEach { season -> title.seasons.forEach { season ->
val isExpanded = expandedSeasons.contains(season.label)
item { item {
Row( Row(
Modifier.fillMaxWidth().padding(top = 12.dp), Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.clickable {
expandedSeasons = if (isExpanded) expandedSeasons - season.label else expandedSeasons + season.label
}
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(season.label, style = MaterialTheme.typography.titleLarge) Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (isExpanded) "Collapse" else "Expand",
)
Spacer(Modifier.width(8.dp))
Text(season.label, style = MaterialTheme.typography.titleLarge)
}
FilledTonalButton(onClick = { DownloadCoordinator.enqueue(context, season.episodes) }) { FilledTonalButton(onClick = { DownloadCoordinator.enqueue(context, season.episodes) }) {
Icon(Icons.Default.Download, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Download season") Text("Download season")
} }
} }
} }
items(season.episodes, key = { it.linkId }) { episode -> if (isExpanded) {
Card(Modifier.fillMaxWidth()) { items(season.episodes, key = { it.linkId }) { episode ->
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) { Card(Modifier.fillMaxWidth()) {
Button(onClick = { playing = episode }, enabled = playerHost != null) { Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Text(if (playerHost == null) "Player loading…" else "Play") Button(onClick = { playing = episode }, enabled = playerHost != null) {
Text(if (playerHost == null) "Player loading…" else "Play")
}
Column(Modifier.weight(1f).padding(horizontal = 12.dp)) {
Text(episode.label, fontWeight = FontWeight.SemiBold)
Text(episode.detail, style = MaterialTheme.typography.bodySmall, maxLines = 1)
}
FilledTonalButton(
onClick = { DownloadCoordinator.enqueue(context, episode) },
enabled = !episode.offline,
) { Text(if (episode.offline) "Offline" else "Download") }
} }
Column(Modifier.weight(1f).padding(horizontal = 12.dp)) {
Text(episode.label, fontWeight = FontWeight.SemiBold)
Text(episode.detail, style = MaterialTheme.typography.bodySmall, maxLines = 1)
}
FilledTonalButton(
onClick = { DownloadCoordinator.enqueue(context, episode) },
enabled = !episode.offline,
) { Text(if (episode.offline) "Offline" else "Download") }
} }
} }
} }
@@ -611,7 +638,7 @@ private fun DownloadsScreen(
// Reading retained metadata on every WorkInfo transition also hydrates // Reading retained metadata on every WorkInfo transition also hydrates
// paused/failed/cancelled entries after WorkManager history is pruned. // paused/failed/cancelled entries after WorkManager history is pruned.
val retained = remember(downloads) { DownloadStateStore(context).records() } val retained = remember(downloads) { DownloadStateStore(context).records() }
Column(Modifier.fillMaxSize().padding(padding).padding(16.dp)) { Column(Modifier.fillMaxSize().padding(padding).padding(16.dp).verticalScroll(rememberScrollState())) {
Text("Offline downloads", style = MaterialTheme.typography.titleLarge) Text("Offline downloads", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
if (retained.isEmpty() && state.offline.isEmpty()) { if (retained.isEmpty() && state.offline.isEmpty()) {
@@ -625,7 +652,11 @@ private fun DownloadsScreen(
Text(episode?.label ?: file.linkId, fontWeight = FontWeight.SemiBold) Text(episode?.label ?: file.linkId, fontWeight = FontWeight.SemiBold)
Text(formatBytes(file.size)) Text(formatBytes(file.size))
} }
FilledTonalButton(onClick = { onRemove(file) }) { Text("Make online only") } FilledTonalButton(onClick = { onRemove(file) }) {
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Delete")
}
} }
} }
} }
@@ -674,7 +705,7 @@ private fun SettingsScreen(
var backgroundAudio by remember { mutableStateOf(settings.backgroundAudio) } var backgroundAudio by remember { mutableStateOf(settings.backgroundAudio) }
var showMetadata by remember { mutableStateOf(false) } var showMetadata by remember { mutableStateOf(false) }
var legalDocument by remember { mutableStateOf<LegalDocument?>(null) } var legalDocument by remember { mutableStateOf<LegalDocument?>(null) }
Column(Modifier.fillMaxSize().padding(padding).padding(16.dp)) { Column(Modifier.fillMaxSize().padding(padding).padding(16.dp).verticalScroll(rememberScrollState())) {
Text("Settings", style = MaterialTheme.typography.titleLarge) Text("Settings", style = MaterialTheme.typography.titleLarge)
SettingToggle("Download on Wi-Fi only", wifiOnly) { wifiOnly = it; settings.wifiOnly = it } SettingToggle("Download on Wi-Fi only", wifiOnly) { wifiOnly = it; settings.wifiOnly = it }
HorizontalDivider() HorizontalDivider()