From 885398e3bddc89f6e478d5df98f2cd84fd1f5691 Mon Sep 17 00:00:00 2001 From: Kayos Date: Mon, 25 May 2026 17:46:23 +0000 Subject: [PATCH] =?UTF-8?q?vc=3D26:=20look=20+=20feel=20pass=20=E2=80=94?= =?UTF-8?q?=20sulkta.com=20palette=20+=20Material=20Icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Color palette pulled directly from sulkta.com's stylesheet — the same greens used on the website now drive the app theme: #166534 deep green (light-theme primary, top app bar background) #4ade80 bright lime (dark-theme primary, accents in dark mode) #86efac light green (primaryContainer in light theme) #e8f5e8 pale green (secondary container tint) #d97706 amber accent (tertiary) #374137 olive gray (secondary on light, container on dark) Replaces the made-up forest palette from vc=23 with the real Sulkta brand. Same M3 tonal-role mapping so derived surfaces stay consistent. TopAppBar redone NewPipe-style: solid deep-green bar with white "straw" title, white hamburger + search icons. Clear bold header instead of the previous white-with-a-pill-underneath layout. Material Icons swapped in everywhere we had emoji: drawer Person / History / PlaylistPlay / Download / Settings minibar PlayArrow / Pause / Close fullscreen Speed / Headphones / Videocam / Share / PictureInPictureAlt / KeyboardArrowDown Pulled in material-icons-extended (4 MB APK growth, all icons). Consistent renders across vendors; no more emoji font fallback drift. FeedRow gets a NewPipe-style duration pill burned into the bottom-right of every thumbnail (mm:ss / h:mm:ss). Live streams / mixes with no duration leave it off. Audit deferred-MED items addressed: MED-6: dropped the PlayerService STATE_ENDED auto-stop. Service shutdown is now driven only by onTaskRemoved + the minibar's ×. Removes the implicit "we'll never queue" assumption and is correct for a future autoplay/queue feature. LOW-7: DownloadsScreen adaptive poll — 1s while a download is active, 5s when idle. No more wasted DB queries when nothing is running. --- buildSrc/src/main/kotlin/ProjectConfig.kt | 4 +- strawApp/build.gradle.kts | 2 +- .../main/kotlin/com/sulkta/straw/StrawHome.kt | 121 ++++++++++++------ .../kotlin/com/sulkta/straw/StrawTheme.kt | 106 ++++++++------- .../straw/feature/download/DownloadsScreen.kt | 17 ++- .../straw/feature/player/MinibarOverlay.kt | 20 ++- .../straw/feature/player/PlaybackService.kt | 20 +-- .../straw/feature/player/PlayerScreen.kt | 41 ++++-- 8 files changed, 207 insertions(+), 124 deletions(-) diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 12e988a7a..fdfb000d4 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app" // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // NewPipeExtractor in the runtime path. -const val STRAW_VERSION_CODE = 25 -const val STRAW_VERSION_NAME = "0.1.0-AK" +const val STRAW_VERSION_CODE = 26 +const val STRAW_VERSION_NAME = "0.1.0-AL" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/strawApp/build.gradle.kts b/strawApp/build.gradle.kts index 774719420..bb98352e7 100644 --- a/strawApp/build.gradle.kts +++ b/strawApp/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation(libs.jetbrains.compose.foundation) implementation(libs.jetbrains.compose.material3) implementation(libs.jetbrains.compose.ui) - implementation("androidx.compose.material:material-icons-core:1.7.5") + implementation("androidx.compose.material:material-icons-extended:1.7.5") // Lifecycle + ViewModel for Compose implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt index 0512de4aa..85c4b9a6e 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt @@ -8,8 +8,10 @@ package com.sulkta.straw +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -25,7 +27,13 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.PlaylistPlay +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api @@ -36,14 +44,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationDrawerItem -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -68,6 +74,7 @@ import com.sulkta.straw.data.Subscriptions import com.sulkta.straw.data.WatchHistoryItem import com.sulkta.straw.feature.feed.SubscriptionFeedViewModel import com.sulkta.straw.feature.search.StreamItem +import com.sulkta.straw.OverlayDimColor import com.sulkta.straw.util.formatViews import kotlinx.coroutines.launch @@ -109,7 +116,7 @@ fun StrawHome( NavigationDrawerItem( label = { Text("Subscriptions") }, - icon = { Text("👤") }, + icon = { Icon(Icons.Filled.Person, contentDescription = null) }, selected = view == HomeView.Subs, onClick = { view = HomeView.Subs @@ -119,7 +126,7 @@ fun StrawHome( ) NavigationDrawerItem( label = { Text("History") }, - icon = { Text("📺") }, + icon = { Icon(Icons.Filled.History, contentDescription = null) }, selected = view == HomeView.History, onClick = { view = HomeView.History @@ -129,7 +136,7 @@ fun StrawHome( ) NavigationDrawerItem( label = { Text("Playlists") }, - icon = { Text("📃") }, + icon = { Icon(Icons.Filled.PlaylistPlay, contentDescription = null) }, selected = false, onClick = { scope.launch { drawerState.close() } @@ -139,7 +146,7 @@ fun StrawHome( ) NavigationDrawerItem( label = { Text("Downloads") }, - icon = { Text("⬇") }, + icon = { Icon(Icons.Filled.Download, contentDescription = null) }, selected = false, onClick = { scope.launch { drawerState.close() } @@ -150,7 +157,7 @@ fun StrawHome( HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) NavigationDrawerItem( label = { Text("Settings") }, - icon = { Text("⚙") }, + icon = { Icon(Icons.Filled.Settings, contentDescription = null) }, selected = false, onClick = { scope.launch { drawerState.close() } @@ -163,42 +170,33 @@ fun StrawHome( ) { Scaffold( topBar = { + // Green-tinted bar inspired by NewPipe/Tubular's colored + // header, but using our forest-green primary container so + // it sits cleanly with the rest of the Material 3 surfaces. TopAppBar( title = { - // Search-pill in the title slot — tap takes you to the - // full search screen with the field auto-focused. Same - // idea as YT's mobile top bar. - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(end = 8.dp) - .height(40.dp) - .clip(RoundedCornerShape(20.dp)) - .clickable(onClick = onOpenSearch), - color = MaterialTheme.colorScheme.surfaceVariant, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 14.dp), - ) { - Text( - "🔍", - style = MaterialTheme.typography.bodyMedium, - ) - Spacer(modifier = Modifier.width(10.dp)) - Text( - "Search YouTube", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } + Text( + "straw", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) }, navigationIcon = { IconButton(onClick = { scope.launch { drawerState.open() } }) { Icon(Icons.Filled.Menu, contentDescription = "Menu") } }, + actions = { + IconButton(onClick = onOpenSearch) { + Icon(Icons.Filled.Search, contentDescription = "Search") + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + ), ) }, ) { padding -> @@ -352,13 +350,12 @@ private fun FeedRow(item: StreamItem, onClick: () -> Unit) { .padding(vertical = 8.dp), verticalAlignment = Alignment.Top, ) { - AsyncImage( - model = item.thumbnail, - contentDescription = null, + ThumbnailWithDuration( + thumbnail = item.thumbnail, + durationSeconds = item.durationSeconds, modifier = Modifier .width(140.dp) - .height(80.dp) - .clip(RoundedCornerShape(6.dp)), + .height(80.dp), ) Spacer(modifier = Modifier.width(10.dp)) Column(modifier = Modifier.weight(1f)) { @@ -387,6 +384,48 @@ private fun FeedRow(item: StreamItem, onClick: () -> Unit) { } } +/** + * 16:9 thumbnail with a NewPipe-style duration pill burned into the + * bottom-right corner. `durationSeconds == 0` skips the badge (live + * streams, mixes that come back without a duration, etc.). + */ +@Composable +private fun ThumbnailWithDuration( + thumbnail: String?, + durationSeconds: Long, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + AsyncImage( + model = thumbnail, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(6.dp)), + ) + if (durationSeconds > 0) { + Text( + text = formatDurationShort(durationSeconds), + style = MaterialTheme.typography.labelSmall, + color = androidx.compose.ui.graphics.Color.White, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(4.dp) + .clip(RoundedCornerShape(3.dp)) + .background(OverlayDimColor) + .padding(horizontal = 4.dp, vertical = 1.dp), + ) + } + } +} + +private fun formatDurationShort(totalSec: Long): String { + val h = totalSec / 3600 + val m = (totalSec % 3600) / 60 + val s = totalSec % 60 + return if (h > 0) "%d:%02d:%02d".format(h, m, s) else "%d:%02d".format(m, s) +} + @Composable private fun SubChip( ch: ChannelRef, diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawTheme.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawTheme.kt index c3ee116ec..ebbacd377 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawTheme.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawTheme.kt @@ -2,10 +2,19 @@ * SPDX-FileCopyrightText: 2026 Sulkta-Coop * SPDX-License-Identifier: GPL-3.0-or-later * - * Sulkta green palette for Straw. Replaces the M3 default lavender/red - * tints with a clean forest green primary — modern, clean, distinct from - * NewPipe / Tubular's red. Same Tonal Palette structure Material 3 uses - * internally so all the derived surfaces stay in harmony. + * Straw palette pulled directly from sulkta.com's stylesheet: + * #4ade80 primary green (Tailwind green-400, most-used on the site) + * #166534 deep green (green-800, headings + emphasis) + * #22c55e mid green (green-500, links + buttons) + * #86efac light green container (green-300) + * #e8f5e8 pale green tint + * #d97706 amber accent (sulkta.com calls this out for chips) + * #374137 olive-gray secondary + * #0a0a0a near-black text on light + * #111411 near-black with green tint for dark surface + * + * Mapped into Material 3's primary / secondary / tertiary tonal roles + * so all the derived M3 surfaces (containers, outlines, etc.) follow. */ package com.sulkta.straw @@ -15,56 +24,61 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color -private val GreenPrimary = Color(0xFF386A1F) -private val GreenOnPrimary = Color(0xFFFFFFFF) -private val GreenPrimaryContainer = Color(0xFFB6F397) -private val GreenOnPrimaryContainer = Color(0xFF0B2000) -private val GreenSecondary = Color(0xFF55624C) -private val GreenOnSecondary = Color(0xFFFFFFFF) -private val GreenSecondaryContainer = Color(0xFFD8E7CB) -private val GreenOnSecondaryContainer = Color(0xFF131F0D) -private val GreenTertiary = Color(0xFF386666) -private val GreenOnTertiary = Color(0xFFFFFFFF) +// Light theme — primary is sulkta.com's deep green (#166534), strong +// enough for white text and matches the site's heading emphasis. +private val LPrimary = Color(0xFF166534) +private val LOnPrimary = Color(0xFFFFFFFF) +private val LPrimaryContainer = Color(0xFF86EFAC) +private val LOnPrimaryContainer = Color(0xFF0A0A0A) +private val LSecondary = Color(0xFF374137) +private val LOnSecondary = Color(0xFFFFFFFF) +private val LSecondaryContainer = Color(0xFFE8F5E8) +private val LOnSecondaryContainer = Color(0xFF0A0A0A) +private val LTertiary = Color(0xFFD97706) +private val LOnTertiary = Color(0xFFFFFFFF) -private val DarkGreenPrimary = Color(0xFF9BD67D) -private val DarkGreenOnPrimary = Color(0xFF153800) -private val DarkGreenPrimaryContainer = Color(0xFF205107) -private val DarkGreenOnPrimaryContainer = Color(0xFFB6F397) -private val DarkGreenSecondary = Color(0xFFBDCBB0) -private val DarkGreenOnSecondary = Color(0xFF283420) -private val DarkGreenSecondaryContainer = Color(0xFF3E4A35) -private val DarkGreenOnSecondaryContainer = Color(0xFFD8E7CB) -private val DarkGreenTertiary = Color(0xFFA0CFD0) -private val DarkGreenOnTertiary = Color(0xFF003738) +// Dark theme — primary is sulkta.com's bright lime (#4ade80) since dark +// backgrounds need a brighter accent for readability. PrimaryContainer +// is the deep green so emphasis stays consistent across themes. +private val DPrimary = Color(0xFF4ADE80) +private val DOnPrimary = Color(0xFF0A0A0A) +private val DPrimaryContainer = Color(0xFF166534) +private val DOnPrimaryContainer = Color(0xFF86EFAC) +private val DSecondary = Color(0xFF9AB89A) +private val DOnSecondary = Color(0xFF111411) +private val DSecondaryContainer = Color(0xFF374137) +private val DOnSecondaryContainer = Color(0xFFE8F5E8) +private val DTertiary = Color(0xFFD97706) +private val DOnTertiary = Color(0xFF0A0A0A) fun strawLightColors(): ColorScheme = lightColorScheme( - primary = GreenPrimary, - onPrimary = GreenOnPrimary, - primaryContainer = GreenPrimaryContainer, - onPrimaryContainer = GreenOnPrimaryContainer, - secondary = GreenSecondary, - onSecondary = GreenOnSecondary, - secondaryContainer = GreenSecondaryContainer, - onSecondaryContainer = GreenOnSecondaryContainer, - tertiary = GreenTertiary, - onTertiary = GreenOnTertiary, + primary = LPrimary, + onPrimary = LOnPrimary, + primaryContainer = LPrimaryContainer, + onPrimaryContainer = LOnPrimaryContainer, + secondary = LSecondary, + onSecondary = LOnSecondary, + secondaryContainer = LSecondaryContainer, + onSecondaryContainer = LOnSecondaryContainer, + tertiary = LTertiary, + onTertiary = LOnTertiary, ) fun strawDarkColors(): ColorScheme = darkColorScheme( - primary = DarkGreenPrimary, - onPrimary = DarkGreenOnPrimary, - primaryContainer = DarkGreenPrimaryContainer, - onPrimaryContainer = DarkGreenOnPrimaryContainer, - secondary = DarkGreenSecondary, - onSecondary = DarkGreenOnSecondary, - secondaryContainer = DarkGreenSecondaryContainer, - onSecondaryContainer = DarkGreenOnSecondaryContainer, - tertiary = DarkGreenTertiary, - onTertiary = DarkGreenOnTertiary, + primary = DPrimary, + onPrimary = DOnPrimary, + primaryContainer = DPrimaryContainer, + onPrimaryContainer = DOnPrimaryContainer, + secondary = DSecondary, + onSecondary = DOnSecondary, + secondaryContainer = DSecondaryContainer, + onSecondaryContainer = DOnSecondaryContainer, + tertiary = DTertiary, + onTertiary = DOnTertiary, ) // Semi-transparent overlays for chrome (overlay buttons, the SB badge, // the inline-player fullscreen pill) and for the dimmed area behind the // minibar thumbnail. Kept here so a theme tweak touches one place. -val OverlayChromeColor = androidx.compose.ui.graphics.Color(0xCC222222) -val OverlayDimColor = androidx.compose.ui.graphics.Color(0xCC000000) +val OverlayChromeColor = Color(0xCC222222) +val OverlayDimColor = Color(0xCC000000) diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/download/DownloadsScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/download/DownloadsScreen.kt index f3262cd41..5872a5c17 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/download/DownloadsScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/download/DownloadsScreen.kt @@ -86,14 +86,19 @@ fun DownloadsScreen() { val context = LocalContext.current var rows by remember { mutableStateOf>(emptyList()) } - // Poll DownloadManager every second while the screen is visible. - // DownloadManager doesn't broadcast progress, so polling is the - // standard pattern. Cheap query — single cursor across the app's own - // download queue. + // DownloadManager doesn't broadcast progress, so we poll while the + // screen is visible. Fast cadence (1s) when something is actively + // running, slow cadence (5s) when everything is settled — no + // animations to update. LaunchedEffect(Unit) { while (true) { - rows = queryDownloads(context) - delay(1000) + val fresh = queryDownloads(context) + rows = fresh + val active = fresh.any { + it.status == DownloadManager.STATUS_RUNNING || + it.status == DownloadManager.STATUS_PENDING + } + delay(if (active) 1000 else 5000) } } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/MinibarOverlay.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/MinibarOverlay.kt index b1d98bcb6..dea622a58 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/MinibarOverlay.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/MinibarOverlay.kt @@ -27,7 +27,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -113,10 +118,13 @@ fun MinibarOverlay( ) } Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MinibarIconButton(label = if (isPlaying) "⏸" else "▶") { + MinibarIconButton( + icon = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + desc = if (isPlaying) "Pause" else "Play", + ) { if (controller.isPlaying) controller.pause() else controller.play() } - MinibarIconButton(label = "×") { + MinibarIconButton(icon = Icons.Filled.Close, desc = "Stop") { controller.stop() controller.clearMediaItems() NowPlaying.clear() @@ -128,7 +136,11 @@ fun MinibarOverlay( } @Composable -private fun MinibarIconButton(label: String, onClick: () -> Unit) { +private fun MinibarIconButton( + icon: androidx.compose.ui.graphics.vector.ImageVector, + desc: String, + onClick: () -> Unit, +) { Box( modifier = Modifier .size(44.dp) @@ -136,6 +148,6 @@ private fun MinibarIconButton(label: String, onClick: () -> Unit) { .clickable(onClick = onClick), contentAlignment = Alignment.Center, ) { - Text(label, style = MaterialTheme.typography.titleMedium) + Icon(imageVector = icon, contentDescription = desc, modifier = Modifier.size(22.dp)) } } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlaybackService.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlaybackService.kt index 61e0f1c47..95902fa6d 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlaybackService.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlaybackService.kt @@ -79,14 +79,11 @@ class PlaybackService : MediaSessionService() { ) .build() - // Stop ourselves once playback genuinely ends so the foreground slot - // is released. STATE_IDLE + STATE_ENDED both qualify; STATE_BUFFERING - // / STATE_READY mean we're still doing work even if paused. - player.addListener(object : Player.Listener { - override fun onPlaybackStateChanged(state: Int) { - if (state == Player.STATE_ENDED) stopSelfWhenIdle() - } - }) + // Service shutdown is driven by onTaskRemoved (user swiped app away) + // + the user pressing × on the minibar (which clears the queue). + // Don't auto-stop on STATE_ENDED — a future autoplay/queue feature + // expects the service to stay alive between items in the queue. + // Foreground notification fades on its own when nothing is playing. val sessionActivityIntent = PendingIntent.getActivity( this, @@ -132,13 +129,6 @@ class PlaybackService : MediaSessionService() { super.onDestroy() } - private fun stopSelfWhenIdle() { - val p = mediaSession?.player ?: return - if (p.mediaItemCount == 0 || p.playbackState == Player.STATE_IDLE) { - stopSelf() - } - } - companion object { const val MEDIA_SESSION_ID = "straw" diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt index 0f4339d6f..cdc2fa39a 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt @@ -33,11 +33,20 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.PictureInPictureAlt +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Speed +import androidx.compose.material.icons.filled.Videocam import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -209,10 +218,13 @@ fun PlayerScreen( modifier = Modifier.align(Alignment.TopEnd).padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OverlayButton(label = if (playbackSpeed == 1f) "1×" else "${playbackSpeed}×") { + OverlayIconButton(icon = Icons.Filled.Speed, desc = "Playback speed") { showSpeedDialog = true } - OverlayButton(label = if (audioOnly) "📻" else "📺") { + OverlayIconButton( + icon = if (audioOnly) Icons.Filled.Headphones else Icons.Filled.Videocam, + desc = if (audioOnly) "Audio-only on" else "Video on", + ) { audioOnly = !audioOnly controller.trackSelectionParameters = TrackSelectionParameters.Builder(context) .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, audioOnly) @@ -223,7 +235,7 @@ fun PlayerScreen( Toast.LENGTH_SHORT, ).show() } - OverlayButton(label = "↗") { + OverlayIconButton(icon = Icons.Filled.Share, desc = "Share") { val send = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, streamUrl) @@ -231,14 +243,14 @@ fun PlayerScreen( } context.startActivity(Intent.createChooser(send, "Share video")) } - OverlayButton(label = "⊟") { + OverlayIconButton(icon = Icons.Filled.PictureInPictureAlt, desc = "Picture in picture") { if (activity == null) { Toast.makeText(context, "PiP: no activity", Toast.LENGTH_SHORT).show() - return@OverlayButton + return@OverlayIconButton } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Toast.makeText(context, "PiP needs Android 8+", Toast.LENGTH_SHORT).show() - return@OverlayButton + return@OverlayIconButton } val params = PictureInPictureParams.Builder() .setAspectRatio(Rational(16, 9)) @@ -251,7 +263,9 @@ fun PlayerScreen( Toast.makeText(context, "PiP failed: ${t.message}", Toast.LENGTH_LONG).show() } } - OverlayButton(label = "⌄") { onMinimize() } + OverlayIconButton(icon = Icons.Filled.KeyboardArrowDown, desc = "Minimize") { + onMinimize() + } } if (showSpeedDialog) { @@ -271,7 +285,11 @@ fun PlayerScreen( } @Composable -private fun OverlayButton(label: String, onClick: () -> Unit) { +private fun OverlayIconButton( + icon: ImageVector, + desc: String, + onClick: () -> Unit, +) { Box( modifier = Modifier .size(36.dp) @@ -280,7 +298,12 @@ private fun OverlayButton(label: String, onClick: () -> Unit) { .clickable(onClick = onClick), contentAlignment = Alignment.Center, ) { - Text(label, color = Color.White, style = MaterialTheme.typography.titleSmall) + Icon( + imageVector = icon, + contentDescription = desc, + tint = Color.White, + modifier = Modifier.size(20.dp), + ) } }