From 4ef62255c02adcea605996357dd5724ad6b26cc3 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 10 Aug 2026 01:10:08 +0200 Subject: [PATCH] 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. --- .../protonstream/playback/PlayerScreen.kt | 64 +++--- .../narl/protonstream/ui/ProtonStreamApp.kt | 200 ++++++++++-------- 2 files changed, 148 insertions(+), 116 deletions(-) diff --git a/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt b/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt index cfc0305..4fe316d 100644 --- a/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt +++ b/android/app/src/main/java/io/narl/protonstream/playback/PlayerScreen.kt @@ -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") } }, ) } diff --git a/android/app/src/main/java/io/narl/protonstream/ui/ProtonStreamApp.kt b/android/app/src/main/java/io/narl/protonstream/ui/ProtonStreamApp.kt index 31a0abb..b90b3f3 100644 --- a/android/app/src/main/java/io/narl/protonstream/ui/ProtonStreamApp.kt +++ b/android/app/src/main/java/io/narl/protonstream/ui/ProtonStreamApp.kt @@ -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()) } }