mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(android): screens on the shared tokens; shelf scrolls with the grid
Player and library screens drop hardcoded blacks, whites and radii for scheme roles and shape tokens, and use the shared button wrappers. Continue watching was pinned above the poster grid, holding a fixed slice of the screen on every scroll. It is now a full-span item at the top of the grid, so it scrolls away with the rest.
This commit is contained in:
1 parent
1a9a4624f8
commit
4ef62255c0
2 files changed
+148
-116
No files matched your search
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Toc
|
||||
@@ -47,16 +46,13 @@ import androidx.compose.material.icons.filled.SkipNext
|
||||
import androidx.compose.material.icons.filled.SkipPrevious
|
||||
import androidx.compose.material.icons.filled.Subtitles
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -81,6 +77,11 @@ import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import io.narl.protonstream.native.NativeRuntime
|
||||
import io.narl.protonstream.ui.theme.AccentButton
|
||||
import io.narl.protonstream.ui.theme.AccentTrack
|
||||
import io.narl.protonstream.ui.theme.EdgedButton
|
||||
import io.narl.protonstream.ui.theme.QuietButton
|
||||
import io.narl.protonstream.ui.theme.TonalButton
|
||||
import io.narl.protonstream.ui.thumbnailSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -379,7 +380,7 @@ fun PlayerScreen(
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(alpha = 0.45f))
|
||||
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.45f))
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -452,7 +453,7 @@ fun PlayerScreen(
|
||||
// ninety seconds, and a button you have to wake with a tap is one nobody
|
||||
// reaches in time.
|
||||
offer?.takeIf { overlays && !autoSkip }?.let { skip ->
|
||||
FilledTonalButton(
|
||||
TonalButton(
|
||||
onClick = { nativeHost.seek(skip.target) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
@@ -479,16 +480,19 @@ fun PlayerScreen(
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(Color.Black.copy(alpha = 0.55f), RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.shapes.small)
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(36.dp))
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(36.dp),
|
||||
)
|
||||
if (state.buffering) {
|
||||
Text(
|
||||
state.cachePercent.takeIf { it > 0.0 }
|
||||
?.let { "Buffering… ${it.roundToInt()}%" } ?: "Buffering…",
|
||||
color = Color.White,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
)
|
||||
@@ -500,16 +504,16 @@ fun PlayerScreen(
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(Color.Black.copy(alpha = 0.85f), RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.errorContainer, MaterialTheme.shapes.small)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(message, color = Color.White)
|
||||
Text(message, color = MaterialTheme.colorScheme.onErrorContainer)
|
||||
// Dismissible, because the message is not always fatal — a
|
||||
// subtitle track that failed to load leaves an episode that
|
||||
// plays perfectly well behind an error that used to be permanent.
|
||||
TextButton(onClick = { playbackError = null; nativeHost.clearProblem() }) {
|
||||
Text("Dismiss", color = Color.White)
|
||||
QuietButton(onClick = { playbackError = null; nativeHost.clearProblem() }) {
|
||||
Text("Dismiss", color = MaterialTheme.colorScheme.onErrorContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,6 +579,7 @@ private fun Activity.toggleLandscape(): Boolean {
|
||||
* icons: the transport cluster on the left, where a thumb already is, and the
|
||||
* pickers on the right. Only the seek bar gets a full row of its own.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PlayerControls(
|
||||
titleKey: String,
|
||||
@@ -608,7 +613,7 @@ private fun PlayerControls(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(alpha = 0.68f))
|
||||
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.68f))
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
) {
|
||||
@@ -633,6 +638,7 @@ private fun PlayerControls(
|
||||
scrubbing = null
|
||||
},
|
||||
valueRange = 0f..duration,
|
||||
track = { AccentTrack(it) },
|
||||
)
|
||||
ChapterMarks(chapters, state.duration, Modifier.matchParentSize())
|
||||
}
|
||||
@@ -798,7 +804,7 @@ private fun SpeedChooser(current: Double, onDismiss: () -> Unit, onSelect: (Doub
|
||||
text = {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
SPEEDS.forEach { rate ->
|
||||
OutlinedButton(
|
||||
EdgedButton(
|
||||
onClick = { onSelect(rate) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
) {
|
||||
@@ -810,7 +816,7 @@ private fun SpeedChooser(current: Double, onDismiss: () -> Unit, onSelect: (Doub
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { Button(onClick = onDismiss) { Text("Close") } },
|
||||
confirmButton = { AccentButton(onClick = onDismiss) { Text("Close") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -846,7 +852,7 @@ private fun ChapterChooser(
|
||||
text = {
|
||||
LazyColumn(Modifier.fillMaxWidth().heightIn(max = 420.dp)) {
|
||||
items(chapters, key = ChapterEntry::index) { chapter ->
|
||||
OutlinedButton(
|
||||
EdgedButton(
|
||||
onClick = { onSelect(chapter) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
) {
|
||||
@@ -860,7 +866,7 @@ private fun ChapterChooser(
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { Button(onClick = onDismiss) { Text("Close") } },
|
||||
confirmButton = { AccentButton(onClick = onDismiss) { Text("Close") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -894,20 +900,24 @@ private fun UpNextCard(
|
||||
if (!showing) return
|
||||
Column(
|
||||
modifier
|
||||
.background(Color.Black.copy(alpha = 0.85f), RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.shapes.small)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text("Up next in ${remaining}s", color = Color.White, style = MaterialTheme.typography.labelMedium)
|
||||
Text(
|
||||
"Up next in ${remaining}s",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
Text(
|
||||
upNext.label,
|
||||
color = Color.White,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 8.dp)) {
|
||||
Button(onClick = onPlayNow) { Text("Play now") }
|
||||
TextButton(onClick = { held = true }) { Text("Watch till the end", color = Color.White) }
|
||||
AccentButton(onClick = onPlayNow) { Text("Play now") }
|
||||
QuietButton(onClick = { held = true }) { Text("Watch till the end") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -926,19 +936,19 @@ private fun TrackChooser(
|
||||
LazyColumn(Modifier.fillMaxWidth().heightIn(max = 420.dp)) {
|
||||
if (type == "sub") {
|
||||
item {
|
||||
OutlinedButton(onClick = { onSelect(null) }, Modifier.fillMaxWidth()) {
|
||||
EdgedButton(onClick = { onSelect(null) }, Modifier.fillMaxWidth()) {
|
||||
Text(if (tracks.none(MpvTrack::selected)) "✓ Off" else "Off")
|
||||
}
|
||||
}
|
||||
}
|
||||
items(tracks, key = MpvTrack::id) { track ->
|
||||
OutlinedButton(onClick = { onSelect(track) }, Modifier.fillMaxWidth()) {
|
||||
EdgedButton(onClick = { onSelect(track) }, Modifier.fillMaxWidth()) {
|
||||
Text(if (track.selected) "✓ ${track.label}" else track.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { Button(onClick = onDismiss) { Text("Close") } },
|
||||
confirmButton = { AccentButton(onClick = onDismiss) { Text("Close") } },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,16 +31,14 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -48,8 +46,6 @@ import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Switch
|
||||
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
|
||||
@@ -108,7 +104,14 @@ import io.narl.protonstream.download.DownloadCoordinator
|
||||
import io.narl.protonstream.download.DownloadStateStore
|
||||
import io.narl.protonstream.download.RetainedDownload
|
||||
import io.narl.protonstream.native.NativeRuntime
|
||||
import io.narl.protonstream.ui.theme.AccentButton
|
||||
import io.narl.protonstream.ui.theme.AccentProgress
|
||||
import io.narl.protonstream.ui.theme.AppearanceState
|
||||
import io.narl.protonstream.ui.theme.EdgedButton
|
||||
import io.narl.protonstream.ui.theme.QuietButton
|
||||
import io.narl.protonstream.ui.theme.TonalButton
|
||||
import io.narl.protonstream.ui.theme.accentNavigationColors
|
||||
import io.narl.protonstream.ui.theme.solid
|
||||
import uniffi.pstr_android.ShareRecord
|
||||
import uniffi.pstr_android.TitleRecord
|
||||
import uniffi.pstr_android.TrackPreferencesRecord
|
||||
@@ -191,6 +194,8 @@ fun ProtonStreamApp(
|
||||
model.dismissMessage()
|
||||
}
|
||||
}
|
||||
// Read out here because `navigationSuiteItems` is not a composable scope.
|
||||
val navigation = accentNavigationColors()
|
||||
NavigationSuiteScaffold(
|
||||
navigationSuiteItems = {
|
||||
Destination.entries.forEach { item ->
|
||||
@@ -199,6 +204,7 @@ fun ProtonStreamApp(
|
||||
onClick = { destination = item; selectedTitleKey = null },
|
||||
icon = { Icon(item.icon, contentDescription = item.label) },
|
||||
label = { Text(item.label) },
|
||||
colors = navigation,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -417,49 +423,6 @@ private fun LibraryScreen(
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
// Above the grid, and only when there is something in it: what a viewer
|
||||
// opening the app wants is almost always the thing they were part way
|
||||
// through, and finding it in an alphabetical grid is the long way round.
|
||||
if (!state.loading && state.query.isBlank() && resumable.isNotEmpty()) {
|
||||
Text("Continue watching", style = MaterialTheme.typography.titleMedium)
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(vertical = 12.dp),
|
||||
) {
|
||||
items(resumable, key = { "${it.episode.shareId}/${it.episode.linkId}" }) { entry ->
|
||||
Card(
|
||||
Modifier.width(200.dp).clickable { onResume(entry.title, entry.index) },
|
||||
) {
|
||||
RemoteArtwork(
|
||||
entry.episode.stillUrl ?: entry.title.backdropUrl,
|
||||
entry.title.canonicalName ?: entry.title.name,
|
||||
Modifier.fillMaxWidth().aspectRatio(16f / 9f),
|
||||
fallback = entry.episode.thumbnailSource,
|
||||
)
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Text(
|
||||
entry.title.canonicalName ?: entry.title.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
entry.episode.label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
entry.episode.progress?.let { progress ->
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.toFloat().coerceIn(0f, 1f) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.loading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
|
||||
} else if (state.titles.isEmpty()) {
|
||||
@@ -471,6 +434,65 @@ private fun LibraryScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(bottom = 24.dp),
|
||||
) {
|
||||
// Above the grid, and only when there is something in it: what a
|
||||
// viewer opening the app wants is almost always the thing they
|
||||
// were part way through, and finding it in an alphabetical grid
|
||||
// is the long way round.
|
||||
//
|
||||
// Inside the grid rather than pinned over it, so the shelf
|
||||
// scrolls away with the rest instead of holding a fixed slice of
|
||||
// a phone screen for itself.
|
||||
if (state.query.isBlank() && resumable.isNotEmpty()) {
|
||||
item(key = "continue-watching", span = { GridItemSpan(maxLineSpan) }) {
|
||||
Column {
|
||||
Text("Continue watching", style = MaterialTheme.typography.titleMedium)
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(vertical = 12.dp),
|
||||
) {
|
||||
items(
|
||||
resumable,
|
||||
key = { "${it.episode.shareId}/${it.episode.linkId}" },
|
||||
) { entry ->
|
||||
Card(
|
||||
Modifier.width(200.dp)
|
||||
.clickable { onResume(entry.title, entry.index) },
|
||||
) {
|
||||
RemoteArtwork(
|
||||
entry.episode.stillUrl ?: entry.title.backdropUrl,
|
||||
entry.title.canonicalName ?: entry.title.name,
|
||||
Modifier.fillMaxWidth().aspectRatio(16f / 9f),
|
||||
fallback = entry.episode.thumbnailSource,
|
||||
)
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Text(
|
||||
entry.title.canonicalName ?: entry.title.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
entry.episode.label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
entry.episode.progress?.let { progress ->
|
||||
AccentProgress(
|
||||
progress = {
|
||||
progress.toFloat().coerceIn(0f, 1f)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items(state.titles, key = { it.key }) { title ->
|
||||
Card(Modifier.fillMaxWidth().clickable { onTitle(title) }) {
|
||||
RemoteArtwork(
|
||||
@@ -550,7 +572,7 @@ private fun TitleScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
item {
|
||||
TextButton(onClick = onBack) {
|
||||
QuietButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Back to library")
|
||||
@@ -586,7 +608,7 @@ private fun TitleScreen(
|
||||
// What a press on the poster should play: whatever was left
|
||||
// part-watched, else the first unwatched episode, else the
|
||||
// first — the desktop client's `next_up`.
|
||||
Button(onClick = { onPlay(playlist, nextUp) }, enabled = playerReady && playlist.isNotEmpty()) {
|
||||
AccentButton(onClick = { onPlay(playlist, nextUp) }, enabled = playerReady && playlist.isNotEmpty()) {
|
||||
Text(
|
||||
when {
|
||||
!playerReady -> "Player loading…"
|
||||
@@ -599,7 +621,7 @@ private fun TitleScreen(
|
||||
// Only where it would do something different: an unstarted show
|
||||
// already starts at the beginning.
|
||||
if (playlist.getOrNull(nextUp)?.resumeAt != null || title.watchedCount > 0uL) {
|
||||
FilledTonalButton(
|
||||
TonalButton(
|
||||
onClick = { onPlay(playlist, 0) },
|
||||
enabled = playerReady && playlist.isNotEmpty(),
|
||||
) { Text("Start over") }
|
||||
@@ -608,12 +630,12 @@ private fun TitleScreen(
|
||||
// one filled primary for the thing the page is for, tonal for
|
||||
// everything else. A text link among filled buttons was what
|
||||
// made this block read as three unrelated rows of controls.
|
||||
FilledTonalButton(onClick = { DownloadCoordinator.enqueue(context, playlist) }) {
|
||||
TonalButton(onClick = { DownloadCoordinator.enqueue(context, playlist) }) {
|
||||
Icon(Icons.Default.Download, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Download show")
|
||||
}
|
||||
FilledTonalButton(onClick = { showMatch = true }) {
|
||||
TonalButton(onClick = { showMatch = true }) {
|
||||
Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Change match")
|
||||
@@ -621,7 +643,7 @@ private fun TitleScreen(
|
||||
// The provider's own page for this title: where a viewer goes to
|
||||
// check that the thing the app matched is the thing they have.
|
||||
title.externalUrl?.let { url ->
|
||||
OutlinedButton(onClick = {
|
||||
EdgedButton(onClick = {
|
||||
runCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
}.onFailure(onPreferenceError)
|
||||
@@ -656,7 +678,7 @@ private fun TitleScreen(
|
||||
)
|
||||
}
|
||||
SettingToggle("Enable subtitles", subtitlesEnabled) { subtitlesEnabled = it }
|
||||
FilledTonalButton(onClick = {
|
||||
TonalButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -904,16 +926,16 @@ private fun EpisodeRow(
|
||||
}
|
||||
// Where the episode was left, under the row it belongs to.
|
||||
episode.progress?.takeIf { !episode.watched }?.let { progress ->
|
||||
LinearProgressIndicator(
|
||||
AccentProgress(
|
||||
progress = { progress.toFloat().coerceIn(0f, 1f) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
download?.takeIf { it.total > 0L && !episode.offline }?.let { active ->
|
||||
LinearProgressIndicator(
|
||||
AccentProgress(
|
||||
progress = { (active.downloaded.toFloat() / active.total).coerceIn(0f, 1f) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
fill = solid(MaterialTheme.colorScheme.tertiary),
|
||||
)
|
||||
Text(
|
||||
"${active.status.replaceFirstChar { it.uppercase() }} · " +
|
||||
@@ -944,7 +966,7 @@ private fun ChangeMatchDialog(
|
||||
Column {
|
||||
Text("Search the configured metadata provider. Nothing is stored until you choose an entry.")
|
||||
OutlinedTextField(term, { term = it }, Modifier.fillMaxWidth().padding(vertical = 8.dp), singleLine = true)
|
||||
Button(enabled = term.isNotBlank() && !searching, onClick = {
|
||||
AccentButton(enabled = term.isNotBlank() && !searching, onClick = {
|
||||
searching = true
|
||||
scope.launch {
|
||||
runCatching {
|
||||
@@ -955,7 +977,7 @@ private fun ChangeMatchDialog(
|
||||
}) { Text(if (searching) "Searching…" else "Search") }
|
||||
LazyColumn(Modifier.fillMaxWidth().heightIn(max = 300.dp)) {
|
||||
items(options, key = { "${it.provider}:${it.remoteId}" }) { option ->
|
||||
OutlinedButton(
|
||||
EdgedButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
@@ -975,14 +997,14 @@ private fun ChangeMatchDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
QuietButton(onClick = {
|
||||
scope.launch {
|
||||
runCatching { withContext(Dispatchers.IO) { NativeRuntime.engine().forgetMatch(title.key) } }
|
||||
.onSuccess { onChanged() }.onFailure(onError)
|
||||
}
|
||||
}) { Text("Forget match") }
|
||||
},
|
||||
dismissButton = { FilledTonalButton(onClick = onDismiss) { Text("Close") } },
|
||||
dismissButton = { TonalButton(onClick = onDismiss) { Text("Close") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1000,7 +1022,7 @@ private fun SharesScreen(
|
||||
Column(Modifier.fillMaxSize().padding(padding).padding(16.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Proton Drive public links", style = MaterialTheme.typography.titleLarge)
|
||||
Button(onClick = { showAdd = true }) { Text("Add share") }
|
||||
AccentButton(onClick = { showAdd = true }) { Text("Add share") }
|
||||
}
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 16.dp)) {
|
||||
items(shares, key = { it.id }) { share ->
|
||||
@@ -1020,8 +1042,8 @@ private fun SharesScreen(
|
||||
// Not a remove: a share whose secret has become
|
||||
// unreadable cannot be removed either, since removal
|
||||
// deletes a secret the store can no longer touch.
|
||||
TextButton(onClick = { repairing = share }) { Text("Re-enter link") }
|
||||
FilledTonalButton(onClick = { onRemove(share.id) }) { Text("Remove") }
|
||||
QuietButton(onClick = { repairing = share }) { Text("Re-enter link") }
|
||||
TonalButton(onClick = { onRemove(share.id) }) { Text("Remove") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1083,12 +1105,12 @@ private fun RepairShareDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
AccentButton(
|
||||
onClick = { onRepair(url.trim(), password); onDismiss() },
|
||||
enabled = url.isNotBlank(),
|
||||
) { Text("Restore") }
|
||||
},
|
||||
dismissButton = { FilledTonalButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
dismissButton = { TonalButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1126,12 +1148,12 @@ private fun AddShareDialog(onDismiss: () -> Unit, onAdd: (String, String, String
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
AccentButton(
|
||||
onClick = { onAdd(name.trim(), url.trim(), password); onDismiss() },
|
||||
enabled = name.isNotBlank() && url.isNotBlank(),
|
||||
) { Text("Add") }
|
||||
},
|
||||
dismissButton = { FilledTonalButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
dismissButton = { TonalButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1193,7 +1215,7 @@ private fun DownloadsScreen(
|
||||
Text(episode?.label ?: file.linkId, fontWeight = FontWeight.SemiBold)
|
||||
Text(formatBytes(file.size))
|
||||
}
|
||||
FilledTonalButton(onClick = { onRemove(file) }) {
|
||||
TonalButton(onClick = { onRemove(file) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Delete")
|
||||
@@ -1209,7 +1231,7 @@ private fun DownloadsScreen(
|
||||
Text(download.label, fontWeight = FontWeight.SemiBold)
|
||||
Text(download.status.replaceFirstChar { it.uppercase() })
|
||||
if (download.total > 0L) {
|
||||
LinearProgressIndicator(
|
||||
AccentProgress(
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
@@ -1223,11 +1245,11 @@ private fun DownloadsScreen(
|
||||
if (download.status == RetainedDownload.STATUS_RUNNING ||
|
||||
download.status == RetainedDownload.STATUS_QUEUED
|
||||
) {
|
||||
FilledTonalButton(onClick = { onPause(download) }) { Text("Pause") }
|
||||
TonalButton(onClick = { onPause(download) }) { Text("Pause") }
|
||||
} else {
|
||||
FilledTonalButton(onClick = { onResume(download) }) { Text("Resume") }
|
||||
TonalButton(onClick = { onResume(download) }) { Text("Resume") }
|
||||
}
|
||||
TextButton(onClick = { onDeletePartial(download) }) { Text("Delete partial") }
|
||||
QuietButton(onClick = { onDeletePartial(download) }) { Text("Delete partial") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1335,11 +1357,11 @@ private fun SettingsScreen(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(vertical = 10.dp)) {
|
||||
FilledTonalButton(onClick = { showMetadata = true }) { Text("Configure metadata") }
|
||||
TonalButton(onClick = { showMetadata = true }) { Text("Configure metadata") }
|
||||
// Every title looked up again, matched ones included: the way out of
|
||||
// a library the provider answered wrong, which otherwise stays wrong
|
||||
// for as long as the match is remembered.
|
||||
FilledTonalButton(
|
||||
TonalButton(
|
||||
onClick = { onMatchAgain() },
|
||||
enabled = state.metadataSettings.enabled && !state.refreshing,
|
||||
) { Text("Match everything again") }
|
||||
@@ -1364,8 +1386,8 @@ private fun SettingsScreen(
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 10.dp)) {
|
||||
// The cache is rebuildable, so it goes without asking. Offline
|
||||
// episodes are a choice the viewer made, so that one asks.
|
||||
FilledTonalButton(onClick = onClearCache) { Text("Clear cache") }
|
||||
FilledTonalButton(
|
||||
TonalButton(onClick = onClearCache) { Text("Clear cache") }
|
||||
TonalButton(
|
||||
onClick = { confirmDelete = true },
|
||||
enabled = state.storage.offlineCount > 0uL,
|
||||
) { Text("Delete all offline") }
|
||||
@@ -1377,10 +1399,10 @@ private fun SettingsScreen(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 12.dp)) {
|
||||
FilledTonalButton(onClick = {
|
||||
TonalButton(onClick = {
|
||||
legalDocument = LegalDocument("GNU GPL v3", "licenses/GPL-3.0.txt")
|
||||
}) { Text("View license") }
|
||||
FilledTonalButton(onClick = {
|
||||
TonalButton(onClick = {
|
||||
legalDocument = LegalDocument("Third-party notices", "licenses/THIRD_PARTY_NOTICES.md")
|
||||
}) { Text("View notices") }
|
||||
}
|
||||
@@ -1397,9 +1419,9 @@ private fun SettingsScreen(
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = { confirmDelete = false; onRemoveAllOffline() }) { Text("Delete") }
|
||||
AccentButton(onClick = { confirmDelete = false; onRemoveAllOffline() }) { Text("Delete") }
|
||||
},
|
||||
dismissButton = { FilledTonalButton(onClick = { confirmDelete = false }) { Text("Cancel") } },
|
||||
dismissButton = { TonalButton(onClick = { confirmDelete = false }) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
legalDocument?.let { document ->
|
||||
@@ -1437,8 +1459,8 @@ private fun MetadataSettingsDialog(
|
||||
SettingToggle("Enable enrichment", enabled) { enabled = it }
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
MetadataProvider.entries.forEach { option ->
|
||||
if (option == provider) Button(onClick = { provider = option }) { Text(option.displayName()) }
|
||||
else OutlinedButton(onClick = { provider = option }) { Text(option.displayName()) }
|
||||
if (option == provider) AccentButton(onClick = { provider = option }) { Text(option.displayName()) }
|
||||
else EdgedButton(onClick = { provider = option }) { Text(option.displayName()) }
|
||||
}
|
||||
}
|
||||
Text(
|
||||
@@ -1460,12 +1482,12 @@ private fun MetadataSettingsDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
AccentButton(
|
||||
enabled = !enabled || provider != MetadataProvider.TMDB || current.ready || apiKey.isNotBlank(),
|
||||
onClick = { onSave(enabled, provider, language.ifBlank { "en" }, apiKey) },
|
||||
) { Text("Save") }
|
||||
},
|
||||
dismissButton = { FilledTonalButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
dismissButton = { TonalButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1495,7 +1517,7 @@ private fun LegalDocumentDialog(document: LegalDocument, onDismiss: () -> Unit)
|
||||
item { Text(contents, style = MaterialTheme.typography.bodySmall) }
|
||||
}
|
||||
},
|
||||
confirmButton = { Button(onClick = onDismiss) { Text("Close") } },
|
||||
confirmButton = { AccentButton(onClick = onDismiss) { Text("Close") } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1552,9 +1574,9 @@ private fun AppearancePicker() {
|
||||
items(FlavorChoice.entries.toList(), key = { it.name }) { flavor ->
|
||||
val selected = flavor == current.flavor
|
||||
if (selected) {
|
||||
Button(onClick = {}) { Text(flavor.label()) }
|
||||
AccentButton(onClick = {}) { Text(flavor.label()) }
|
||||
} else {
|
||||
OutlinedButton(onClick = {
|
||||
EdgedButton(onClick = {
|
||||
scope.launch { repaint(current.copy(flavor = flavor), store = true) }
|
||||
}) { Text(flavor.label()) }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user