mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
updated readme + renamed buttons for android client
This commit is contained in:
1 parent
b399f84f54
commit
359d397a69
2 files changed
+54
-22
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
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`.
|
||||
|
||||
A native Android 12+ client is under `android/`, sharing the Rust catalog and
|
||||
streaming implementation through UniFFI. Its toolchain, emulator, signing, and
|
||||
release instructions are in [`docs/ANDROID.md`](docs/ANDROID.md).
|
||||
streaming implementation through UniFFI. It features a modern Material 3 interface,
|
||||
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
|
||||
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. |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.LazyVerticalGrid
|
||||
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.Search
|
||||
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.material3.adaptive.navigationsuite.NavigationSuiteScaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -261,6 +266,10 @@ private fun TitleScreen(
|
||||
var subtitlesEnabled by remember(title.key) { mutableStateOf(false) }
|
||||
var playing by remember(title.key) { mutableStateOf<EpisodeRecord?>(null) }
|
||||
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) {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -422,32 +431,50 @@ private fun TitleScreen(
|
||||
}) { Text("Save track preferences") }
|
||||
}
|
||||
title.seasons.forEach { season ->
|
||||
val isExpanded = expandedSeasons.contains(season.label)
|
||||
item {
|
||||
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,
|
||||
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) }) {
|
||||
Icon(Icons.Default.Download, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Download season")
|
||||
}
|
||||
}
|
||||
}
|
||||
items(season.episodes, key = { it.linkId }) { episode ->
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Button(onClick = { playing = episode }, enabled = playerHost != null) {
|
||||
Text(if (playerHost == null) "Player loading…" else "Play")
|
||||
if (isExpanded) {
|
||||
items(season.episodes, key = { it.linkId }) { episode ->
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
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
|
||||
// paused/failed/cancelled entries after WorkManager history is pruned.
|
||||
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)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (retained.isEmpty() && state.offline.isEmpty()) {
|
||||
@@ -625,7 +652,11 @@ private fun DownloadsScreen(
|
||||
Text(episode?.label ?: file.linkId, fontWeight = FontWeight.SemiBold)
|
||||
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 showMetadata by remember { mutableStateOf(false) }
|
||||
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)
|
||||
SettingToggle("Download on Wi-Fi only", wifiOnly) { wifiOnly = it; settings.wifiOnly = it }
|
||||
HorizontalDivider()
|
||||
|
||||
Reference in new issue
Block a user