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> <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_screen_title">Settings</string>
<string name="settings_section_plugins">Plugins</string> <string name="settings_section_plugins">Plugins</string>
<string name="settings_section_language_region">Language and Region</string> <string name="settings_section_language_region">Language and Region</string>
<string name="settings_section_appearance">Appearance</string> <string name="settings_section_appearance">Appearance</string>
<string name="settings_section_playback">Playback</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_caching">Caching</string>
<string name="settings_section_desktop">Desktop</string> <string name="settings_section_desktop">Desktop</string>
<string name="settings_section_updates">Updates</string> <string name="settings_section_updates">Updates</string>

View File

@ -76,7 +76,8 @@ interface NavigationItem {
private val tabs = listOf<NavigationItem>( private val tabs = listOf<NavigationItem>(
NavigationItem.Tab<Any>("Home", Iconsax.IconsaxHome, Routes.Home), 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( val sidebarTabs = tabs + listOf(
NavigationItem.Group( NavigationItem.Group(
@ -89,7 +90,8 @@ val sidebarTabs = tabs + listOf(
LibraryTab.Playlists LibraryTab.Playlists
), ),
NavigationItem.Tab("Albums", Iconsax.IconsaxCd, Routes.Library, LibraryTab.Albums), 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( NavigationItem.Group(
"On Device", "On Device",
@ -105,23 +107,23 @@ val sidebarTabs = tabs + listOf(
Iconsax.IconsaxDirectboxReceive, Iconsax.IconsaxDirectboxReceive,
Routes.Library, Routes.Library,
LibraryTab.Downloads LibraryTab.Downloads
)) )
)
), ),
NavigationItem.Group( NavigationItem.Group(
"Connect", "Connect",
items = listOf( items = listOf(
NavigationItem.Tab("Devices", Iconsax.IconsaxMirrorScreenRegular, Routes.Devices), 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( val bottombarTabs = tabs + listOf(
NavigationItem.Tab<Any>("Library", Iconsax.IconsaxMusicLibrary, Routes.Library), NavigationItem.Tab<Any>("Library", Iconsax.IconsaxMusicLibrary, Routes.Library),
NavigationItem.Tab("Settings", Iconsax.IconsaxSetting2, Routes.Settings) NavigationItem.Tab("Settings", Iconsax.IconsaxSetting2, Routes.Settings)
) )
@OptIn( @OptIn(KoinExperimentalAPI::class)
KoinExperimentalAPI::class,
ExperimentalCoroutinesApi::class,
ExperimentalSharedTransitionApi::class
)
@Composable @Composable
fun App( fun App(
content: @Composable () -> Unit = {} content: @Composable () -> Unit = {}
@ -156,7 +158,8 @@ fun App(
AppShell(navigator, navigationState) { AppShell(navigator, navigationState) {
SharedTransitionLayout { SharedTransitionLayout {
CompositionLocalProvider( CompositionLocalProvider(
LocalSharedTransitionScope provides this@SharedTransitionLayout) { LocalSharedTransitionScope provides this@SharedTransitionLayout
) {
Column { Column {
NavDisplay( NavDisplay(
modifier = Modifier.fillMaxSize(), 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.SheetDetent
import com.composeunstyled.rememberModalBottomSheetState import com.composeunstyled.rememberModalBottomSheetState
import org.spotube.millennium.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.resources.iconsax.FluentDismiss import dev.krtirtho.spotube.resources.iconsax.FluentDismiss
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
@ -51,11 +49,9 @@ fun AdaptiveDialogBottomSheet(
onDismiss: () -> Unit, onDismiss: () -> Unit,
title: @Composable (() -> Unit)? = null, title: @Composable (() -> Unit)? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
breakpointDp: Float = 600f,
actions: @Composable (() -> Unit)? = null, actions: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit) { content: @Composable () -> Unit) {
val adaptiveInfo = currentWindowAdaptiveInfo() val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
if (isLargeScreen) { if (isLargeScreen) {
MillenniumDialog( MillenniumDialog(

View File

@ -18,17 +18,8 @@
package dev.krtirtho.spotube.core.ui.component 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.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -69,21 +46,37 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector 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.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp 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
import compose.icons.feathericons.Check 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.Iconsax
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore 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.IconsaxHeart
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash 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( data class AdaptiveMenuItem(
val icon: ImageVector? = null, val icon: ImageVector? = null,
@ -106,14 +99,12 @@ fun AdaptiveDropdownBottomSheet(
items: List<AdaptiveMenuItem>, items: List<AdaptiveMenuItem>,
trigger: @Composable (onClick: () -> Unit) -> Unit, trigger: @Composable (onClick: () -> Unit) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
breakpointDp: Float = 600f,
menuMinWidth: Dp = 200.dp, menuMinWidth: Dp = 200.dp,
header: @Composable (() -> Unit)? = null, header: @Composable (() -> Unit)? = null,
headerDisplayMode: HeaderDisplayMode = HeaderDisplayMode.Always, headerDisplayMode: HeaderDisplayMode = HeaderDisplayMode.Always,
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) { filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val adaptiveInfo = currentWindowAdaptiveInfo() val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
Box(modifier = modifier) { Box(modifier = modifier) {
if (isLargeScreen) { if (isLargeScreen) {

View File

@ -31,7 +31,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -89,8 +90,8 @@ fun CollectionDetails(
showFollowButton: Boolean = true, showFollowButton: Boolean = true,
onEdit: (() -> Unit)? = null, onEdit: (() -> Unit)? = null,
sharedElementKey: String? = null) { sharedElementKey: String? = null) {
BoxWithConstraints(modifier = modifier.fillMaxWidth()) { Box(modifier = modifier.fillMaxWidth()) {
val isCompact = maxWidth < 600.dp val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
val sharedTransitionScope = LocalSharedTransitionScope.current val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedVisibilityScope.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 dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -104,8 +105,7 @@ private fun FeaturedCarouselContent(
val centerIndex = items.size / 2 val centerIndex = items.size / 2
var selectedIndex by remember { mutableIntStateOf(centerIndex) } var selectedIndex by remember { mutableIntStateOf(centerIndex) }
val state = rememberCoverflowState(centerIndex) { index -> selectedIndex = index } val state = rememberCoverflowState(centerIndex) { index -> selectedIndex = index }
val adaptiveInfo = currentWindowAdaptiveInfo() val isSmallScreen = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
val isSmallScreen = adaptiveInfo.windowSizeClass.minWidthDp <= 600
Box( Box(
modifier = modifier 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 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.AnimatedContent
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut 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.graphics.luminance
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity 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.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -207,11 +209,10 @@ fun TrackList(
} }
} }
val windowInfo = LocalWindowInfo.current val widthBreakpoint = currentWindowWidthBreakpoint()
val maxWidth = with(density) { windowInfo.containerSize.width.toDp() } val isCompact = widthBreakpoint isBelow WindowBreakpoints.Medium
val isCompact = maxWidth < CompactTrackListBreakpoint
val isDesktop = remember { getPlatform().isDesktop() } val isDesktop = remember { getPlatform().isDesktop() }
val showIndex = maxWidth >= LargeTrackListBreakpoint val showIndex = widthBreakpoint isAtLeast WindowBreakpoints.Expanded
val showAlbum = !isCompact val showAlbum = !isCompact
val useDropdownForOptions = isDesktop && !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.Theme
import dev.krtirtho.spotube.modules.settings.UserSettings import dev.krtirtho.spotube.modules.settings.UserSettings
import org.spotube.millennium.composables.MillenniumTooltipHost
import org.spotube.millennium.theme.MillenniumColors import org.spotube.millennium.theme.MillenniumColors
import org.spotube.millennium.theme.MillenniumPalette import org.spotube.millennium.theme.MillenniumPalette
import org.spotube.millennium.theme.MillenniumTheme import org.spotube.millennium.theme.MillenniumTheme
@ -75,7 +76,8 @@ data class AppColors(
val onError: Color, val onError: Color,
val errorContainer: Color, val errorContainer: Color,
val onErrorContainer: Color, val onErrorContainer: Color,
val scrim: Color) val scrim: Color
)
/** Maps Millennium's semantic palette onto the app's role names. */ /** Maps Millennium's semantic palette onto the app's role names. */
fun appColorsFrom(m: MillenniumColors): AppColors = AppColors( fun appColorsFrom(m: MillenniumColors): AppColors = AppColors(
@ -106,7 +108,8 @@ fun appColorsFrom(m: MillenniumColors): AppColors = AppColors(
onError = m.background, onError = m.background,
errorContainer = m.danger.copy(alpha = 0.16f), errorContainer = m.danger.copy(alpha = 0.16f),
onErrorContainer = m.danger, onErrorContainer = m.danger,
scrim = m.scrim) scrim = m.scrim
)
/** Named text styles used by the app, independent of Material typography. */ /** Named text styles used by the app, independent of Material typography. */
@Immutable @Immutable
@ -123,16 +126,19 @@ data class AppTypography(
val bodySmall: TextStyle, val bodySmall: TextStyle,
val labelLarge: TextStyle, val labelLarge: TextStyle,
val labelMedium: TextStyle, val labelMedium: TextStyle,
val labelSmall: TextStyle) val labelSmall: TextStyle
)
private fun style( private fun style(
size: Int, size: Int,
lineHeight: Int, lineHeight: Int,
weight: FontWeight = FontWeight.Normal): TextStyle = TextStyle( weight: FontWeight = FontWeight.Normal
): TextStyle = TextStyle(
fontFamily = FontFamily.SansSerif, fontFamily = FontFamily.SansSerif,
fontSize = size.sp, fontSize = size.sp,
lineHeight = lineHeight.sp, lineHeight = lineHeight.sp,
fontWeight = weight) fontWeight = weight
)
val AppDefaultTypography = AppTypography( val AppDefaultTypography = AppTypography(
displaySmall = style(36, 44), displaySmall = style(36, 44),
@ -147,21 +153,25 @@ val AppDefaultTypography = AppTypography(
bodySmall = style(12, 16), bodySmall = style(12, 16),
labelLarge = style(14, 20, FontWeight.Medium), labelLarge = style(14, 20, FontWeight.Medium),
labelMedium = style(12, 16, 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. */ /** Corner shapes used by the app. */
@Immutable @Immutable
data class AppShapes( data class AppShapes(
val small: Shape, val small: Shape,
val medium: Shape, val medium: Shape,
val large: Shape) val large: Shape
)
val AppDefaultShapes = AppShapes( val AppDefaultShapes = AppShapes(
small = RoundedCornerShape(8.dp), small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.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 LocalAppTypography = staticCompositionLocalOf { AppDefaultTypography }
val LocalAppShapes = staticCompositionLocalOf { AppDefaultShapes } val LocalAppShapes = staticCompositionLocalOf { AppDefaultShapes }
@ -182,7 +192,8 @@ object AppTheme {
@Composable @Composable
fun SpotubeTheme( fun SpotubeTheme(
settings: UserSettings, settings: UserSettings,
content: @Composable () -> Unit) { content: @Composable () -> Unit
) {
val isDarkTheme = when (settings.theme) { val isDarkTheme = when (settings.theme) {
Theme.LIGHT -> false Theme.LIGHT -> false
Theme.DARK -> true Theme.DARK -> true
@ -192,11 +203,14 @@ fun SpotubeTheme(
MillenniumTheme(dark = isDarkTheme, config = MillenniumThemeConfig(palette = palette)) { MillenniumTheme(dark = isDarkTheme, config = MillenniumThemeConfig(palette = palette)) {
val colors = appColorsFrom(MillenniumTheme.colors) val colors = appColorsFrom(MillenniumTheme.colors)
CompositionLocalProvider( MillenniumTooltipHost {
LocalAppColors provides colors, CompositionLocalProvider(
LocalAppTypography provides AppDefaultTypography, LocalAppColors provides colors,
LocalAppShapes provides AppDefaultShapes, LocalAppTypography provides AppDefaultTypography,
content = content) 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -327,8 +328,8 @@ private fun ArtistHeaderCard(
onFollowClick: () -> Unit, onFollowClick: () -> Unit,
isBlacklisted: Boolean, isBlacklisted: Boolean,
onBlacklistClick: () -> Unit) { onBlacklistClick: () -> Unit) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
val isCompact = maxWidth < 600.dp val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
MillenniumCard( MillenniumCard(
modifier = Modifier modifier = Modifier

View File

@ -42,7 +42,8 @@ import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.SheetDetent import com.composeunstyled.SheetDetent
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.rememberModalBottomSheetState 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.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -74,12 +75,10 @@ fun AddToPlaylistPicker(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onPlaylistSelected: (String) -> Unit, onPlaylistSelected: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
libraryRepository: LibraryRepository = koinInject(), libraryRepository: LibraryRepository = koinInject()) {
breakpointDp: Float = 600f) {
if (!visible) return if (!visible) return
val adaptiveInfo = currentWindowAdaptiveInfo() val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
var playlists by remember { mutableStateOf<List<MetadataPlaylist>>(emptyList()) } var playlists by remember { mutableStateOf<List<MetadataPlaylist>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) } 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.SheetDetent
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.rememberModalBottomSheetState 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.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -82,12 +83,10 @@ fun PlaylistFormSheet(
onSubmit: (PlaylistFormData) -> Unit, onSubmit: (PlaylistFormData) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
initialData: PlaylistFormData? = null, initialData: PlaylistFormData? = null,
isEditing: Boolean = false, isEditing: Boolean = false) {
breakpointDp: Float = 600f) {
if (!visible) return if (!visible) return
val adaptiveInfo = currentWindowAdaptiveInfo() val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
var name by remember(initialData) { mutableStateOf(initialData?.name ?: "") } var name by remember(initialData) { mutableStateOf(initialData?.name ?: "") }
var description by remember(initialData) { mutableStateOf(initialData?.description ?: "") } 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.SheetDetent
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.rememberModalBottomSheetState 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.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -79,12 +80,10 @@ fun AddTracksToPlaylistDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
searchRepository: SearchRepository = koinInject(), searchRepository: SearchRepository = koinInject(),
libraryRepository: LibraryRepository = koinInject(), libraryRepository: LibraryRepository = koinInject()) {
breakpointDp: Float = 600f) {
if (!visible) return if (!visible) return
val adaptiveInfo = currentWindowAdaptiveInfo() val isLargeScreen = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
var query by remember { mutableStateOf("") } var query by remember { mutableStateOf("") }
var searchResults by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) } var searchResults by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }

View File

@ -17,8 +17,10 @@
package dev.krtirtho.spotube.modules.settings 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.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -67,8 +69,8 @@ private val SettingsDialogMinWidth = 720.dp
fun SettingsScreen(settingsViewModel: SettingsViewModel) { fun SettingsScreen(settingsViewModel: SettingsViewModel) {
val navigatorCommands: NavigationCommands = koinInject() val navigatorCommands: NavigationCommands = koinInject()
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (maxWidth >= SettingsDialogMinWidth) { if (currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded) {
SettingsDialog( SettingsDialog(
settingsViewModel = settingsViewModel, settingsViewModel = settingsViewModel,
onDismiss = { navigatorCommands.pop(Routes.Settings) }) 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.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumSeparator import org.spotube.millennium.composables.MillenniumSeparator
import org.spotube.millennium.composables.MillenniumSidebarItem
import org.spotube.millennium.theme.MillenniumTheme import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.millenniumPanelBrush import org.spotube.millennium.theme.millenniumPanelBrush
import org.spotube.millennium.theme.millenniumSurface import org.spotube.millennium.theme.millenniumSurface
@ -62,7 +63,6 @@ import dev.krtirtho.spotube.PlatformType
import dev.krtirtho.spotube.getPlatform import dev.krtirtho.spotube.getPlatform
import dev.krtirtho.spotube.core.discovery.rememberLocalNetworkPermissionRequester import dev.krtirtho.spotube.core.discovery.rememberLocalNetworkPermissionRequester
import dev.krtirtho.spotube.core.navigation.NavigationCommands 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.blacklist.BlacklistContent
import dev.krtirtho.spotube.modules.plugin.PluginScreenContent import dev.krtirtho.spotube.modules.plugin.PluginScreenContent
import dev.krtirtho.spotube.modules.settings.sections.appearanceSection import dev.krtirtho.spotube.modules.settings.sections.appearanceSection
@ -247,11 +247,12 @@ private fun SettingsSidebar(
) )
} }
items(tabs, key = { it.name }) { tab -> items(tabs, key = { it.name }) { tab ->
SidebarItem( MillenniumSidebarItem(
label = stringResource(tab.titleRes), label = stringResource(tab.titleRes),
icon = tab.icon, onClick = { onSelect(tab) },
selected = tab == selectedTab, 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 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 androidx.compose.ui.Alignment
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton 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.MillenniumSwitch
import org.spotube.millennium.composables.MillenniumTextField import org.spotube.millennium.composables.MillenniumTextField
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -92,8 +96,8 @@ internal fun <T> SelectionSettingCard(
filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) { filter: ((AdaptiveMenuItem, String) -> Boolean)? = null) {
var isDialogOpen by remember { mutableStateOf(false) } var isDialogOpen by remember { mutableStateOf(false) }
BoxWithConstraints { Box {
val isWideLayout = maxWidth >= inlineSelectorMinWidth val isWideLayout = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
SettingCardItem( SettingCardItem(
title = title, title = title,
@ -173,8 +177,8 @@ internal fun TextInputSettingCard(
enabled: Boolean = true) { enabled: Boolean = true) {
var isDialogOpen by remember { mutableStateOf(false) } var isDialogOpen by remember { mutableStateOf(false) }
BoxWithConstraints { Box {
val isWideLayout = maxWidth >= inlineTextFieldMinWidth val isWideLayout = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Medium
val inlineState = rememberTextFieldState(value) val inlineState = rememberTextFieldState(value)
LaunchedEffect(inlineState, isWideLayout) { LaunchedEffect(inlineState, isWideLayout) {

View File

@ -23,8 +23,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.ui.graphics.Color import androidx.compose.foundation.layout.padding
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalDensity
import androidx.compose.ui.unit.dp 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.Dialog
import androidx.compose.ui.window.DialogProperties 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.NavigationCommands
import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator 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 kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel 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 } val LocalAppShellBottomInset = staticCompositionLocalOf { 0.dp }
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun AppShell( fun AppShell(
navigator: Navigator, navigator: Navigator,
@ -158,7 +145,6 @@ fun AppShell(
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.weight(1f)) { Row(modifier = Modifier.weight(1f)) {
AppSidebar( AppSidebar(
hazeState = bgHazeState,
navigator = navigator, navigator = navigator,
navigationState = navigationState navigationState = navigationState
) )
@ -214,7 +200,7 @@ fun AppShell(
} }
} }
} else { } else {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -278,7 +264,7 @@ fun AppShell(
@OptIn( @OptIn(
ExperimentalSharedTransitionApi::class, ExperimentalFoundationApi::class) ExperimentalSharedTransitionApi::class, ExperimentalFoundationApi::class)
@Composable @Composable
private fun BoxWithConstraintsScope.CompactPlayerOverlay( private fun CompactPlayerOverlay(
navigator: Navigator, navigator: Navigator,
navigationState: NavigationState, navigationState: NavigationState,
viewModel: AppShellViewModel, viewModel: AppShellViewModel,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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