refactor: replace currentWindowAdaptiveInfo with currentWindowWidthBreakpoint for adaptive layouts

This commit is contained in:
Kingkor Roy Tirtho 2026-10-11 21:08:11 +06:00
parent bb9f225fd7
commit caad76ba95
23 changed files with 289 additions and 366 deletions

View File

@ -16,13 +16,18 @@
-->
<resources>
<string name="queue_title">Queue</string>
<string name="queue_clear_queue">Clear Queue</string>
<string name="queue_filter_queue">Filter queue...</string>
<string name="queue_no_queue_entries">No queue entries</string>
<string name="settings_screen_title">Settings</string>
<string name="settings_section_plugins">Plugins</string>
<string name="settings_section_language_region">Language and Region</string>
<string name="settings_section_appearance">Appearance</string>
<string name="settings_section_playback">Playback</string>
<string name="settings_section_downloads">Downloads</string>
<string name="settings_section_downloads">Downloads</string>f
<string name="settings_section_caching">Caching</string>
<string name="settings_section_desktop">Desktop</string>
<string name="settings_section_updates">Updates</string>

View File

@ -76,7 +76,8 @@ interface NavigationItem {
private val tabs = listOf<NavigationItem>(
NavigationItem.Tab<Any>("Home", Iconsax.IconsaxHome, Routes.Home),
NavigationItem.Tab<Any>("Search", Iconsax.IconsaxSearch, Routes.Search))
NavigationItem.Tab<Any>("Search", Iconsax.IconsaxSearch, Routes.Search)
)
val sidebarTabs = tabs + listOf(
NavigationItem.Group(
@ -89,7 +90,8 @@ val sidebarTabs = tabs + listOf(
LibraryTab.Playlists
),
NavigationItem.Tab("Albums", Iconsax.IconsaxCd, Routes.Library, LibraryTab.Albums),
NavigationItem.Tab("Artists", Iconsax.User, Routes.Library, LibraryTab.Artists))
NavigationItem.Tab("Artists", Iconsax.User, Routes.Library, LibraryTab.Artists)
)
),
NavigationItem.Group(
"On Device",
@ -105,23 +107,23 @@ val sidebarTabs = tabs + listOf(
Iconsax.IconsaxDirectboxReceive,
Routes.Library,
LibraryTab.Downloads
))
)
)
),
NavigationItem.Group(
"Connect",
items = listOf(
NavigationItem.Tab("Devices", Iconsax.IconsaxMirrorScreenRegular, Routes.Devices),
NavigationItem.Tab("Group Jam", Iconsax.IconsaxSoundTwotone, Routes.Jam))))
NavigationItem.Tab("Group Jam", Iconsax.IconsaxSoundTwotone, Routes.Jam)
)
)
)
val bottombarTabs = tabs + listOf(
NavigationItem.Tab<Any>("Library", Iconsax.IconsaxMusicLibrary, Routes.Library),
NavigationItem.Tab("Settings", Iconsax.IconsaxSetting2, Routes.Settings)
)
@OptIn(
KoinExperimentalAPI::class,
ExperimentalCoroutinesApi::class,
ExperimentalSharedTransitionApi::class
)
@OptIn(KoinExperimentalAPI::class)
@Composable
fun App(
content: @Composable () -> Unit = {}
@ -156,7 +158,8 @@ fun App(
AppShell(navigator, navigationState) {
SharedTransitionLayout {
CompositionLocalProvider(
LocalSharedTransitionScope provides this@SharedTransitionLayout) {
LocalSharedTransitionScope provides this@SharedTransitionLayout
) {
Column {
NavDisplay(
modifier = Modifier.fillMaxSize(),

View File

@ -32,16 +32,14 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.SheetDetent
import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.resources.iconsax.FluentDismiss
import dev.krtirtho.spotube.resources.iconsax.Iconsax
@ -51,11 +49,9 @@ fun AdaptiveDialogBottomSheet(
onDismiss: () -> Unit,
title: @Composable (() -> Unit)? = null,
modifier: Modifier = Modifier,
breakpointDp: Float = 600f,
actions: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit) {
val adaptiveInfo = currentWindowAdaptiveInfo()
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
if (isLargeScreen) {
MillenniumDialog(

View File

@ -18,17 +18,8 @@
package dev.krtirtho.spotube.core.ui.component
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumModalBottomSheet
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumTextField
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
@ -42,23 +33,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.DropdownMenuPanel
import com.composeunstyled.SheetDetent
import com.composeunstyled.Text
import com.composeunstyled.UnstyledDropdownMenu
import com.composeunstyled.UnstyledDropdownMenuItem
import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.gelBrush
import org.spotube.millennium.theme.millenniumPanelBrush
import org.spotube.millennium.theme.millenniumSurface
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -69,21 +46,37 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.composeunstyled.DropdownMenuPanel
import com.composeunstyled.SheetDetent
import com.composeunstyled.Text
import com.composeunstyled.UnstyledDropdownMenu
import com.composeunstyled.UnstyledDropdownMenuItem
import com.composeunstyled.rememberModalBottomSheetState
import compose.icons.FeatherIcons
import compose.icons.feathericons.Check
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumModalBottomSheet
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumTextField
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.gelBrush
import org.spotube.millennium.theme.millenniumPanelBrush
import org.spotube.millennium.theme.millenniumSurface
data class AdaptiveMenuItem(
val icon: ImageVector? = null,
@ -106,14 +99,12 @@ fun AdaptiveDropdownBottomSheet(
items: List<AdaptiveMenuItem>,
trigger: @Composable (onClick: () -> Unit) -> Unit,
modifier: Modifier = Modifier,
breakpointDp: Float = 600f,
menuMinWidth: Dp = 200.dp,
header: @Composable (() -> Unit)? = null,
headerDisplayMode: HeaderDisplayMode = HeaderDisplayMode.Always,
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
var expanded by remember { mutableStateOf(false) }
val adaptiveInfo = currentWindowAdaptiveInfo()
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
Box(modifier = modifier) {
if (isLargeScreen) {

View File

@ -31,7 +31,8 @@ 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.BoxWithConstraints
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -89,8 +90,8 @@ fun CollectionDetails(
showFollowButton: Boolean = true,
onEdit: (() -> Unit)? = null,
sharedElementKey: String? = null) {
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
val isCompact = maxWidth < 600.dp
Box(modifier = modifier.fillMaxWidth()) {
val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current

View File

@ -41,7 +41,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@ -104,8 +105,7 @@ private fun FeaturedCarouselContent(
val centerIndex = items.size / 2
var selectedIndex by remember { mutableIntStateOf(centerIndex) }
val state = rememberCoverflowState(centerIndex) { index -> selectedIndex = index }
val adaptiveInfo = currentWindowAdaptiveInfo()
val isSmallScreen = adaptiveInfo.windowSizeClass.minWidthDp <= 600
val isSmallScreen = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
Box(
modifier = modifier

View File

@ -1,133 +0,0 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.core.ui.component
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIcon
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import com.composeunstyled.LocalTextStyle
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
/**
* The shared navigation item used by every sidebar in the app (the main app sidebar
* and the settings sidebar), so they stay visually identical.
*
* @param icon leading icon of the item.
* @param badge optional overlay drawn on the icon (for example a download indicator).
*/
@Composable
fun SidebarItem(
label: String,
icon: ImageVector,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
expanded: Boolean = true,
badge: (@Composable BoxScope.() -> Unit)? = null) {
val itemContent: @Composable () -> Unit = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(16.dp)) {
MillenniumIcon(
imageVector = icon,
contentDescription = label,
tint = if (selected) {
AppTheme.colors.primary
} else {
AppTheme.colors.onSurfaceVariant
})
badge?.invoke(this)
}
AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
Text(
text = label,
singleLine = true,
style = LocalTextStyle.current.copy(
fontSize = AppTheme.typography.labelMedium.fontSize,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
color = if (selected) {
AppTheme.colors.onSecondaryContainer
} else {
AppTheme.colors.onSurfaceVariant
}
)
)
}
Spacer(modifier = if (expanded) Modifier.weight(1f) else Modifier)
}
}
val buttonModifier = modifier
.fillMaxWidth()
.padding(horizontal = 7.dp, vertical = 2.dp)
if (selected) {
MillenniumButton(
onClick = onClick,
modifier = buttonModifier,
style = MillenniumButtonStyle.Secondary,
content = itemContent)
} else {
MillenniumButton(
onClick = onClick,
modifier = buttonModifier,
style = MillenniumButtonStyle.Quiet,
content = itemContent)
}
}
/**
* The shared section heading used above groups of [SidebarItem]s.
*/
@Composable
fun SidebarSectionLabel(
text: String,
expanded: Boolean = true) {
AnimatedVisibility(visible = expanded) {
Text(
text = text.uppercase(),
style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 8.dp))
}
}

View File

@ -18,6 +18,9 @@
package dev.krtirtho.spotube.core.ui.component
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -78,7 +81,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
@ -207,11 +209,10 @@ fun TrackList(
}
}
val windowInfo = LocalWindowInfo.current
val maxWidth = with(density) { windowInfo.containerSize.width.toDp() }
val isCompact = maxWidth < CompactTrackListBreakpoint
val widthBreakpoint = currentWindowWidthBreakpoint()
val isCompact = widthBreakpoint isBelow WindowBreakpoints.Medium
val isDesktop = remember { getPlatform().isDesktop() }
val showIndex = maxWidth >= LargeTrackListBreakpoint
val showIndex = widthBreakpoint isAtLeast WindowBreakpoints.Expanded
val showAlbum = !isCompact
val useDropdownForOptions = isDesktop && !isCompact

View File

@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp
import dev.krtirtho.spotube.modules.settings.Theme
import dev.krtirtho.spotube.modules.settings.UserSettings
import org.spotube.millennium.composables.MillenniumTooltipHost
import org.spotube.millennium.theme.MillenniumColors
import org.spotube.millennium.theme.MillenniumPalette
import org.spotube.millennium.theme.MillenniumTheme
@ -75,7 +76,8 @@ data class AppColors(
val onError: Color,
val errorContainer: Color,
val onErrorContainer: Color,
val scrim: Color)
val scrim: Color
)
/** Maps Millennium's semantic palette onto the app's role names. */
fun appColorsFrom(m: MillenniumColors): AppColors = AppColors(
@ -106,7 +108,8 @@ fun appColorsFrom(m: MillenniumColors): AppColors = AppColors(
onError = m.background,
errorContainer = m.danger.copy(alpha = 0.16f),
onErrorContainer = m.danger,
scrim = m.scrim)
scrim = m.scrim
)
/** Named text styles used by the app, independent of Material typography. */
@Immutable
@ -123,16 +126,19 @@ data class AppTypography(
val bodySmall: TextStyle,
val labelLarge: TextStyle,
val labelMedium: TextStyle,
val labelSmall: TextStyle)
val labelSmall: TextStyle
)
private fun style(
size: Int,
lineHeight: Int,
weight: FontWeight = FontWeight.Normal): TextStyle = TextStyle(
weight: FontWeight = FontWeight.Normal
): TextStyle = TextStyle(
fontFamily = FontFamily.SansSerif,
fontSize = size.sp,
lineHeight = lineHeight.sp,
fontWeight = weight)
fontWeight = weight
)
val AppDefaultTypography = AppTypography(
displaySmall = style(36, 44),
@ -147,21 +153,25 @@ val AppDefaultTypography = AppTypography(
bodySmall = style(12, 16),
labelLarge = style(14, 20, FontWeight.Medium),
labelMedium = style(12, 16, FontWeight.Medium),
labelSmall = style(11, 16, FontWeight.Medium))
labelSmall = style(11, 16, FontWeight.Medium)
)
/** Corner shapes used by the app. */
@Immutable
data class AppShapes(
val small: Shape,
val medium: Shape,
val large: Shape)
val large: Shape
)
val AppDefaultShapes = AppShapes(
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp))
large = RoundedCornerShape(16.dp)
)
val LocalAppColors = staticCompositionLocalOf { appColorsFrom(org.spotube.millennium.theme.MillenniumLightColors) }
val LocalAppColors =
staticCompositionLocalOf { appColorsFrom(org.spotube.millennium.theme.MillenniumLightColors) }
val LocalAppTypography = staticCompositionLocalOf { AppDefaultTypography }
val LocalAppShapes = staticCompositionLocalOf { AppDefaultShapes }
@ -182,7 +192,8 @@ object AppTheme {
@Composable
fun SpotubeTheme(
settings: UserSettings,
content: @Composable () -> Unit) {
content: @Composable () -> Unit
) {
val isDarkTheme = when (settings.theme) {
Theme.LIGHT -> false
Theme.DARK -> true
@ -192,11 +203,14 @@ fun SpotubeTheme(
MillenniumTheme(dark = isDarkTheme, config = MillenniumThemeConfig(palette = palette)) {
val colors = appColorsFrom(MillenniumTheme.colors)
CompositionLocalProvider(
LocalAppColors provides colors,
LocalAppTypography provides AppDefaultTypography,
LocalAppShapes provides AppDefaultShapes,
content = content)
MillenniumTooltipHost {
CompositionLocalProvider(
LocalAppColors provides colors,
LocalAppTypography provides AppDefaultTypography,
LocalAppShapes provides AppDefaultShapes,
content = content
)
}
}
}

View File

@ -29,7 +29,8 @@ import org.spotube.millennium.theme.MillenniumTheme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -327,8 +328,8 @@ private fun ArtistHeaderCard(
onFollowClick: () -> Unit,
isBlacklisted: Boolean,
onBlacklistClick: () -> Unit) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val isCompact = maxWidth < 600.dp
Box(modifier = Modifier.fillMaxWidth()) {
val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
MillenniumCard(
modifier = Modifier

View File

@ -42,7 +42,8 @@ import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.SheetDetent
import com.composeunstyled.Text
import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -74,12 +75,10 @@ fun AddToPlaylistPicker(
onDismiss: () -> Unit,
onPlaylistSelected: (String) -> Unit,
modifier: Modifier = Modifier,
libraryRepository: LibraryRepository = koinInject(),
breakpointDp: Float = 600f) {
libraryRepository: LibraryRepository = koinInject()) {
if (!visible) return
val adaptiveInfo = currentWindowAdaptiveInfo()
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
var playlists by remember { mutableStateOf<List<MetadataPlaylist>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }

View File

@ -41,7 +41,8 @@ import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.SheetDetent
import com.composeunstyled.Text
import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
@ -82,12 +83,10 @@ fun PlaylistFormSheet(
onSubmit: (PlaylistFormData) -> Unit,
modifier: Modifier = Modifier,
initialData: PlaylistFormData? = null,
isEditing: Boolean = false,
breakpointDp: Float = 600f) {
isEditing: Boolean = false) {
if (!visible) return
val adaptiveInfo = currentWindowAdaptiveInfo()
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
var name by remember(initialData) { mutableStateOf(initialData?.name ?: "") }
var description by remember(initialData) { mutableStateOf(initialData?.description ?: "") }

View File

@ -44,7 +44,8 @@ import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.SheetDetent
import com.composeunstyled.Text
import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -79,12 +80,10 @@ fun AddTracksToPlaylistDialog(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
searchRepository: SearchRepository = koinInject(),
libraryRepository: LibraryRepository = koinInject(),
breakpointDp: Float = 600f) {
libraryRepository: LibraryRepository = koinInject()) {
if (!visible) return
val adaptiveInfo = currentWindowAdaptiveInfo()
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
var query by remember { mutableStateOf("") }
var searchResults by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }

View File

@ -17,8 +17,10 @@
package dev.krtirtho.spotube.modules.settings
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@ -67,8 +69,8 @@ private val SettingsDialogMinWidth = 720.dp
fun SettingsScreen(settingsViewModel: SettingsViewModel) {
val navigatorCommands: NavigationCommands = koinInject()
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
if (maxWidth >= SettingsDialogMinWidth) {
Box(modifier = Modifier.fillMaxSize()) {
if (currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded) {
SettingsDialog(
settingsViewModel = settingsViewModel,
onDismiss = { navigatorCommands.pop(Routes.Settings) })

View File

@ -23,6 +23,7 @@ import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSidebarItem
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumPanelBrush
import org.spotube.millennium.theme.millenniumSurface
@ -62,7 +63,6 @@ import dev.krtirtho.spotube.PlatformType
import dev.krtirtho.spotube.getPlatform
import dev.krtirtho.spotube.core.discovery.rememberLocalNetworkPermissionRequester
import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.ui.component.SidebarItem
import dev.krtirtho.spotube.modules.blacklist.BlacklistContent
import dev.krtirtho.spotube.modules.plugin.PluginScreenContent
import dev.krtirtho.spotube.modules.settings.sections.appearanceSection
@ -247,11 +247,12 @@ private fun SettingsSidebar(
)
}
items(tabs, key = { it.name }) { tab ->
SidebarItem(
MillenniumSidebarItem(
label = stringResource(tab.titleRes),
icon = tab.icon,
onClick = { onSelect(tab) },
selected = tab == selectedTab,
onClick = { onSelect(tab) })
contentDescription = stringResource(tab.titleRes),
icon = tab.icon)
}
}
}

View File

@ -18,6 +18,11 @@
package dev.krtirtho.spotube.modules.settings.components
import androidx.compose.foundation.layout.Box
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.Alignment
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
@ -27,7 +32,6 @@ import org.spotube.millennium.composables.MillenniumRadioGroup
import org.spotube.millennium.composables.MillenniumSwitch
import org.spotube.millennium.composables.MillenniumTextField
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -92,8 +96,8 @@ internal fun <T> SelectionSettingCard(
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
var isDialogOpen by remember { mutableStateOf(false) }
BoxWithConstraints {
val isWideLayout = maxWidth >= inlineSelectorMinWidth
Box {
val isWideLayout = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
SettingCardItem(
title = title,
@ -173,8 +177,8 @@ internal fun TextInputSettingCard(
enabled: Boolean = true) {
var isDialogOpen by remember { mutableStateOf(false) }
BoxWithConstraints {
val isWideLayout = maxWidth >= inlineTextFieldMinWidth
Box {
val isWideLayout = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val inlineState = rememberTextFieldState(value)
LaunchedEffect(inlineState, isWideLayout) {

View File

@ -23,8 +23,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
@ -32,43 +30,29 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import org.spotube.millennium.composables.MillenniumSnackbarHost
import org.spotube.millennium.composables.MillenniumSnackbarHostState
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.theme.MillenniumTheme
import com.composeunstyled.Sheet
import com.composeunstyled.SheetDetent
import com.composeunstyled.UnstyledBottomSheet
import com.composeunstyled.rememberBottomSheetState
import com.composeunstyled.Text
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.composeunstyled.Sheet
import com.composeunstyled.SheetDetent
import com.composeunstyled.UnstyledBottomSheet
import com.composeunstyled.rememberBottomSheetState
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator
@ -86,10 +70,13 @@ import dev.krtirtho.spotube.modules.shell.player_queue.QueueSheet
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSnackbarHost
import org.spotube.millennium.composables.MillenniumSnackbarHostState
import org.spotube.millennium.theme.MillenniumTheme
val LocalAppShellBottomInset = staticCompositionLocalOf { 0.dp }
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalFoundationApi::class)
@Composable
fun AppShell(
navigator: Navigator,
@ -158,7 +145,6 @@ fun AppShell(
Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.weight(1f)) {
AppSidebar(
hazeState = bgHazeState,
navigator = navigator,
navigationState = navigationState
)
@ -214,7 +200,7 @@ fun AppShell(
}
}
} else {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
@ -278,7 +264,7 @@ fun AppShell(
@OptIn(
ExperimentalSharedTransitionApi::class, ExperimentalFoundationApi::class)
@Composable
private fun BoxWithConstraintsScope.CompactPlayerOverlay(
private fun CompactPlayerOverlay(
navigator: Navigator,
navigationState: NavigationState,
viewModel: AppShellViewModel,

View File

@ -17,9 +17,10 @@
package dev.krtirtho.spotube.modules.shell
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.lifecycle.ViewModel
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -63,9 +64,7 @@ class AppShellViewModel : ViewModel() {
if (maxWidth != null) {
return maxWidth >= sidebarMinWidth
}
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
return with(density) { windowInfo.containerSize.width.toDp() >= sidebarMinWidth }
return currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded
}
fun bottomOverlayInset(useSidebar: Boolean): Dp =

View File

@ -22,7 +22,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
@ -35,11 +34,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.composeunstyled.Text
import dev.chrisbanes.haze.HazeState
import dev.krtirtho.spotube.NavigationItem
import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator
@ -67,7 +66,6 @@ import spotube.composeapp.generated.resources.cookie_regular
@Composable
fun AppSidebar(
hazeState: HazeState,
navigator: Navigator,
navigationState: NavigationState,
modifier: Modifier = Modifier,
@ -80,6 +78,7 @@ fun AppSidebar(
MillenniumSidebar(
modifier = modifier,
collapsed = !expanded,
shape = RectangleShape,
onCollapsedChange = { expanded = !it },
) {
MillenniumSidebarHeader {

View File

@ -17,7 +17,11 @@
package dev.krtirtho.spotube.modules.shell.alternative_track
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Box
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -32,8 +36,8 @@ fun AlternativeTrackSheet(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit) {
BoxWithConstraints(modifier = modifier) {
if (maxWidth >= SlidingSheetBreakpoint) {
Box(modifier = modifier) {
if (currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded) {
EndDrawerSheet(isVisible, onDismiss, content)
} else {
BottomSheetContent(isVisible, onDismiss, content)

View File

@ -18,18 +18,8 @@
package dev.krtirtho.spotube.modules.shell.player_queue
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumCard
import org.spotube.millennium.composables.MillenniumDropdownMenu
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumTextField
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.animation.core.animateDpAsState
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
@ -45,14 +35,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.shape.CircleShape
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -60,29 +46,47 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.composeunstyled.Text
import dev.krtirtho.spotube.core.jam.JamParticipant
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
import dev.krtirtho.spotube.resources.iconsax.IconsaxDragHandle
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxDragHandle
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import dev.krtirtho.spotube.resources.iconsax.IconsaxUserRemove
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.spotube.millennium.composables.MillenniumDropdownMenu
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumTextField
import org.spotube.millennium.composables.MillenniumTooltip
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.queue_clear_queue
import spotube.composeapp.generated.resources.queue_filter_queue
import spotube.composeapp.generated.resources.queue_no_queue_entries
import spotube.composeapp.generated.resources.queue_title
@Composable
fun PlayerQueueContent(
viewModel: PlayerQueueContentViewModel = koinViewModel<PlayerQueueContentViewModel>(),
modifier: Modifier = Modifier) {
modifier: Modifier = Modifier
) {
val state by viewModel.queueContentUiState.collectAsState()
val displayItems = state.displayItems
val filterQuery = state.filterQuery
@ -103,14 +107,17 @@ fun PlayerQueueContent(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)) {
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = "Queue",
style = AppTheme.typography.titleLarge)
text = stringResource(Res.string.queue_title),
style = AppTheme.typography.titleLarge
)
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalAlignment = Alignment.CenterVertically) {
verticalAlignment = Alignment.CenterVertically
) {
val filterState = rememberTextFieldState(filterQuery)
LaunchedEffect(filterState.text) {
viewModel.setQueueFilter(filterState.text.toString())
@ -118,42 +125,49 @@ fun PlayerQueueContent(
MillenniumTextField(
state = filterState,
label = "",
placeholder = "Filter queue...",
modifier = Modifier.weight(1f))
placeholder = stringResource(Res.string.queue_filter_queue),
modifier = Modifier.weight(1f)
)
if (!isReadOnly) {
MillenniumIconButton(
onClick = viewModel::clearQueue,
style = MillenniumIconButtonStyle.Quiet) {
MillenniumIcon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
MillenniumTooltip(stringResource(Res.string.queue_clear_queue)) {
MillenniumIconButton(
onClick = viewModel::clearQueue,
style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon(
Iconsax.IconsaxTrash,
contentDescription = stringResource(Res.string.queue_clear_queue)
)
}
}
}
}
if (displayItems.isEmpty()) {
Text(
text = "No queue entries",
text = stringResource(Res.string.queue_no_queue_entries),
style = AppTheme.typography.bodyMedium,
color = AppTheme.colors.onSurfaceVariant)
color = AppTheme.colors.onSurfaceVariant
)
} else {
MillenniumCard {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = PaddingValues(bottom = 8.dp)) {
items(displayItems, key = { it.id }) { item ->
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
QueueItemRow(
item = item,
reorderScope = if (isFiltered || isReadOnly) null else this,
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
onDragStarted = { viewModel.onDragStart() },
onDragStopped = { viewModel.onDragStop() },
showOptions = !isReadOnly,
enabled = !isReadOnly,
onParticipantClick = { selectedParticipant = it })
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = PaddingValues(bottom = 8.dp)
) {
items(displayItems, key = { it.id }) { item ->
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
QueueItemRow(
item = item,
reorderScope = if (isFiltered || isReadOnly) null else this,
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
onDragStarted = { viewModel.onDragStart() },
onDragStopped = { viewModel.onDragStop() },
showOptions = !isReadOnly,
enabled = !isReadOnly,
onParticipantClick = { selectedParticipant = it })
}
}
}
@ -179,32 +193,38 @@ private fun ParticipantDialog(
onDismiss: () -> Unit,
onKick: () -> Unit,
onBan: () -> Unit,
onRemoveSuggestions: () -> Unit) {
onRemoveSuggestions: () -> Unit
) {
AdaptiveDialogBottomSheet(
onDismiss = onDismiss,
title = { Text(participant.displayName, style = AppTheme.typography.titleLarge) }) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)) {
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(vertical = 8.dp)) {
modifier = Modifier.padding(vertical = 8.dp)
) {
ParticipantAvatar(participant, size = 40)
Text(
text = participant.displayName,
style = AppTheme.typography.bodyLarge)
style = AppTheme.typography.bodyLarge
)
if (participant.isHost) {
Text(
text = "Host",
style = AppTheme.typography.labelSmall,
color = AppTheme.colors.primary)
color = AppTheme.colors.primary
)
}
}
if (isJamHost && !participant.isHost) {
MillenniumSeparator(
modifier = Modifier.fillMaxWidth())
modifier = Modifier.fillMaxWidth()
)
MillenniumListTile(
onClick = {
onKick()
@ -214,7 +234,8 @@ private fun ParticipantDialog(
MillenniumIcon(
imageVector = Iconsax.IconsaxCloseSquare,
contentDescription = null,
tint = AppTheme.colors.error)
tint = AppTheme.colors.error
)
},
title = { Text("Kick") },
subtitle = { Text("Remove them from the session") })
@ -227,7 +248,8 @@ private fun ParticipantDialog(
MillenniumIcon(
imageVector = Iconsax.IconsaxUserRemove,
contentDescription = null,
tint = AppTheme.colors.error)
tint = AppTheme.colors.error
)
},
title = { Text("Ban") },
subtitle = { Text("Kick and prevent them from rejoining") })
@ -239,7 +261,8 @@ private fun ParticipantDialog(
leading = {
MillenniumIcon(
imageVector = Iconsax.IconsaxMusicSquareRemove,
contentDescription = null)
contentDescription = null
)
},
title = { Text("Remove suggestions") },
subtitle = { Text("Remove every track they added to the queue") })
@ -255,11 +278,13 @@ private fun ParticipantAvatar(participant: JamParticipant, size: Int) {
.size(size.dp)
.clip(CircleShape)
.background(AppTheme.colors.primaryContainer),
contentAlignment = Alignment.Center) {
contentAlignment = Alignment.Center
) {
Text(
text = participant.displayName.firstOrNull()?.uppercase()?.take(1) ?: "?",
style = AppTheme.typography.labelMedium,
color = AppTheme.colors.onPrimaryContainer)
color = AppTheme.colors.onPrimaryContainer
)
}
}
@ -273,7 +298,8 @@ private fun QueueItemRow(
onDragStopped: () -> Unit,
showOptions: Boolean = true,
enabled: Boolean = true,
onParticipantClick: (JamParticipant) -> Unit = {}) {
onParticipantClick: (JamParticipant) -> Unit = {}
) {
var showMenu by remember { mutableStateOf(false) }
MillenniumListTile(
@ -284,8 +310,9 @@ private fun QueueItemRow(
leading = {
Row(
modifier = Modifier
.height(72.dp),
verticalAlignment = Alignment.CenterVertically) {
.height(56.dp),
verticalAlignment = Alignment.CenterVertically
) {
MillenniumIcon(
Iconsax.IconsaxDragHandle,
contentDescription = if (reorderScope != null) "Reorder" else null,
@ -296,7 +323,8 @@ private fun QueueItemRow(
with(reorderScope) {
Modifier.draggableHandle(
onDragStarted = { onDragStarted() },
onDragStopped = onDragStopped)
onDragStopped = onDragStopped
)
}
} else {
Modifier
@ -305,21 +333,24 @@ private fun QueueItemRow(
Box(
modifier = Modifier
.size(48.dp)
.size(40.dp)
.clip(AppTheme.shapes.small)
.background(AppTheme.colors.surfaceVariant),
contentAlignment = Alignment.Center) {
contentAlignment = Alignment.Center
) {
if (item.imageUrl != null) {
AsyncImage(
model = item.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop)
contentScale = ContentScale.Crop
)
} else {
Text(
text = "${item.originalIndex + 1}",
style = AppTheme.typography.labelMedium,
color = AppTheme.colors.onSurfaceVariant)
color = AppTheme.colors.onSurfaceVariant
)
}
}
}
@ -334,7 +365,8 @@ private fun QueueItemRow(
AppTheme.colors.onSecondaryContainer
} else {
AppTheme.colors.onSurface
})
}
)
},
subtitle = {
Text(
@ -342,13 +374,15 @@ private fun QueueItemRow(
style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis)
overflow = TextOverflow.Ellipsis
)
},
trailing = {
Text(
text = item.durationLabel,
style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant)
color = AppTheme.colors.onSurfaceVariant
)
Spacer(modifier = Modifier.width(4.dp))
@ -359,11 +393,13 @@ private fun QueueItemRow(
.clip(CircleShape)
.background(AppTheme.colors.surfaceVariant)
.clickable { onParticipantClick(participant) },
contentAlignment = Alignment.Center) {
contentAlignment = Alignment.Center
) {
Text(
text = participant.displayName.firstOrNull()?.uppercase()?.take(1) ?: "?",
style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant)
color = AppTheme.colors.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(4.dp))
}
@ -380,10 +416,12 @@ private fun QueueItemRow(
anchor = {
MillenniumIconButton(
onClick = { showMenu = true },
style = MillenniumIconButtonStyle.Quiet) {
style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon(
Iconsax.Iconsax3DotsMore,
contentDescription = "More options")
contentDescription = "More options"
)
}
})
}

View File

@ -17,15 +17,17 @@
package dev.krtirtho.spotube.modules.shell.player_queue
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import com.composeunstyled.DrawerPlacement
import com.composeunstyled.SheetDetent
import com.composeunstyled.currentWindowWidthBreakpoint
import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.WindowBreakpoints
import org.spotube.millennium.composables.MillenniumDrawer
import org.spotube.millennium.composables.MillenniumDrawerValue
import org.spotube.millennium.composables.MillenniumModalBottomSheet
@ -38,13 +40,14 @@ fun QueueSheet(
isVisible: Boolean,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit) {
BoxWithConstraints(modifier = modifier) {
if (maxWidth >= SlidingSheetBreakpoint) {
EndDrawerSheet(isVisible, onDismiss, content)
} else {
BottomSheetContent(isVisible, onDismiss, content)
}
content: @Composable () -> Unit
) {
val isExpanded = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded
if (isExpanded) {
EndDrawerSheet(isVisible, onDismiss, content)
} else {
BottomSheetContent(isVisible, onDismiss, content)
}
}
@ -52,7 +55,8 @@ fun QueueSheet(
internal fun EndDrawerSheet(
isVisible: Boolean,
onDismiss: () -> Unit,
content: @Composable () -> Unit) {
content: @Composable () -> Unit
) {
val drawerState = rememberMillenniumDrawerState()
LaunchedEffect(isVisible) {
drawerState.targetValue =
@ -66,7 +70,10 @@ internal fun EndDrawerSheet(
MillenniumDrawer(
state = drawerState,
placement = DrawerPlacement.End,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)) {
shape = RectangleShape,
maximumWidth = 400.dp,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)
) {
content()
}
}
@ -75,7 +82,8 @@ internal fun EndDrawerSheet(
internal fun BottomSheetContent(
isVisible: Boolean,
onDismiss: () -> Unit,
content: @Composable () -> Unit) {
content: @Composable () -> Unit
) {
if (!isVisible) return
val sheetState = rememberModalBottomSheetState(initialDetent = SheetDetent.FullyExpanded)
LaunchedEffect(sheetState.bottomSheetState.currentDetent) {
@ -85,7 +93,8 @@ internal fun BottomSheetContent(
}
MillenniumModalBottomSheet(
state = sheetState,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)) {
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
) {
content()
}
}

View File

@ -1,6 +1,11 @@
package dev.krtirtho.spotube.modules.welcome.pages
import androidx.compose.foundation.layout.Box
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import org.spotube.millennium.composables.MillenniumCard
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
@ -38,8 +43,8 @@ fun HowItWorksPage(
onTogglePlayback: () -> Unit,
modifier: Modifier = Modifier
) {
BoxWithConstraints(modifier.fillMaxSize()) {
val wide = maxWidth >= 760.dp
Box(modifier.fillMaxSize()) {
val wide = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded
if (wide) {
Row(
Modifier.fillMaxSize().padding(40.dp),