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
+40
-8
No files matched your search
@@ -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,18 +431,35 @@ 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,
|
||||||
) {
|
) {
|
||||||
|
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)
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isExpanded) {
|
||||||
items(season.episodes, key = { it.linkId }) { episode ->
|
items(season.episodes, key = { it.linkId }) { episode ->
|
||||||
Card(Modifier.fillMaxWidth()) {
|
Card(Modifier.fillMaxWidth()) {
|
||||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
@@ -453,6 +479,7 @@ private fun TitleScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (showMatch) {
|
if (showMatch) {
|
||||||
ChangeMatchDialog(
|
ChangeMatchDialog(
|
||||||
title = title,
|
title = title,
|
||||||
@@ -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()
|
||||||
|
|||||||
Reference in new issue
Block a user