From 9571658fb00b9f0fc3a7f77dd07433716cf1d937 Mon Sep 17 00:00:00 2001 From: Talo Halton Date: Mon, 27 Nov 2023 23:52:35 +0000 Subject: [PATCH] Continue desktop player, add narrow desktop player --- ComposeKit | 2 +- .../settings/category/DesktopSettings.kt | 2 + .../playerservice/PlayerServicePlayer.kt | 3 +- .../spmp/ui/component/LikeDislikeButton.kt | 5 +- .../spmp/ui/component/PillMenu.kt | 24 +- .../longpressmenu/LongPressMenu.desktop.kt | 2 +- .../LongPressMenuActionProvider.kt | 10 +- .../longpressmenu/LongPressMenuData.kt | 22 +- .../mediaitempreview/MediaItemPreview.kt | 5 +- .../apppage/mainpage/LoadingSplashView.kt | 15 +- .../apppage/mainpage/PlayerStateImpl.kt | 5 +- .../settingspage/category/DesktopCategory.kt | 33 ++- .../spmp/ui/layout/nowplaying/NowPlaying.kt | 27 +- .../nowplaying/NowPlayingExpansionState.kt | 2 +- .../ui/layout/nowplaying/maintab/Controls.kt | 149 +++++----- .../nowplaying/maintab/LargeBottomBar.kt | 21 +- .../layout/nowplaying/maintab/LargeTopBar.kt | 22 ++ ...scape.kt => NowPlayingMainTabLandscape.kt} | 0 ...nTabLarge.kt => NowPlayingMainTabLarge.kt} | 260 +++++++++++++----- .../maintab/NowPlayingMainTabNarrow.kt | 159 +++++++++++ .../maintab/NowPlayingMainTabPage.kt | 30 +- ...rtrait.kt => NowPlayingMainTabPortrait.kt} | 0 .../ui/layout/nowplaying/maintab/SeekBar.kt | 4 +- .../maintab/thumbnailrow/LargeThumbnailRow.kt | 4 +- .../overlay/PaletteSelectorOverlayMenu.kt | 2 +- .../nowplaying/queue/QueueButtonsRow.kt | 97 +++---- .../ui/layout/nowplaying/queue/QueueTab.kt | 5 +- .../layout/nowplaying/queue/QueueTabItem.kt | 23 +- .../spmp/ui/theme/ApplicationTheme.kt | 27 ++ .../resources/assets/values-ja-JP/strings.xml | 7 +- .../resources/assets/values/strings.xml | 7 +- .../splash/ExtraLoadingContent.desktop.kt | 179 ++++++------ 32 files changed, 812 insertions(+), 341 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeTopBar.kt rename shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/{MainTabLandscape.kt => NowPlayingMainTabLandscape.kt} (100%) rename shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/{MainTabLarge.kt => NowPlayingMainTabLarge.kt} (53%) create mode 100644 shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabNarrow.kt rename shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/{MainTabPortrait.kt => NowPlayingMainTabPortrait.kt} (100%) diff --git a/ComposeKit b/ComposeKit index de124bfbd..d49cff570 160000 --- a/ComposeKit +++ b/ComposeKit @@ -1 +1 @@ -Subproject commit de124bfbd8c6ed1f7ed0ae3365c1d4af9ccde0e8 +Subproject commit d49cff570d635967bd5b2f1e7e57d1421cd3042f diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/DesktopSettings.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/DesktopSettings.kt index 1e0196e78..31dab8c85 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/DesktopSettings.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/settings/category/DesktopSettings.kt @@ -24,6 +24,7 @@ data object DesktopSettings: SettingsCategory("desktop") { SERVER_IP_ADDRESS, SERVER_PORT, SERVER_LOCAL_COMMAND, + SERVER_LOCAL_START_AUTOMATICALLY, SERVER_KILL_CHILD_ON_EXIT; override val category: SettingsCategory get() = DesktopSettings @@ -35,6 +36,7 @@ data object DesktopSettings: SettingsCategory("desktop") { SERVER_IP_ADDRESS -> "127.0.0.1" SERVER_PORT -> 3973 SERVER_LOCAL_COMMAND -> "spms" + SERVER_LOCAL_START_AUTOMATICALLY -> false SERVER_KILL_CHILD_ON_EXIT -> true } as T } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt index 8fe1a176c..95644ed6e 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/platform/playerservice/PlayerServicePlayer.kt @@ -8,6 +8,7 @@ import app.cash.sqldelight.Query import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import com.toasterofbread.composekit.platform.Platform import com.toasterofbread.composekit.platform.PlatformPreferences import com.toasterofbread.composekit.platform.PlatformPreferencesListener import com.toasterofbread.spmp.ProjectBuildConfig @@ -204,7 +205,7 @@ abstract class PlayerServicePlayer(private val service: PlatformPlayerService) { } init { - if (ProjectBuildConfig.MUTE_PLAYER == true) { + if (ProjectBuildConfig.MUTE_PLAYER == true && !Platform.DESKTOP.isCurrent()) { service.volume = 0f } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/LikeDislikeButton.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/LikeDislikeButton.kt index 59f8f5e91..82ccf5efd 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/LikeDislikeButton.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/LikeDislikeButton.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.outlined.ThumbUp +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -25,6 +26,7 @@ import com.toasterofbread.spmp.model.mediaitem.loader.SongLikedLoader import com.toasterofbread.spmp.model.mediaitem.song.Song import com.toasterofbread.spmp.model.mediaitem.song.SongLikedStatus import com.toasterofbread.spmp.model.mediaitem.song.updateLiked +import com.toasterofbread.spmp.ui.theme.appHover import com.toasterofbread.spmp.youtubeapi.YoutubeApi import com.toasterofbread.spmp.youtubeapi.endpoint.SetSongLikedEndpoint import com.toasterofbread.spmp.youtubeapi.endpoint.SongLikedEndpoint @@ -79,7 +81,8 @@ fun LikeDislikeButton( ) } }, - modifier.bounceOnClick(), + modifier.bounceOnClick().appHover(true), + enabled = getEnabled?.invoke() != false, apply_minimum_size = false ) { Crossfade(liked_status) { status -> diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/PillMenu.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/PillMenu.kt index 85ffb7fad..e10b3857c 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/PillMenu.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/PillMenu.kt @@ -51,6 +51,7 @@ import com.toasterofbread.composekit.utils.common.getContrasted import com.toasterofbread.composekit.utils.common.thenIf import com.toasterofbread.composekit.utils.composable.NoRipple import com.toasterofbread.spmp.platform.AppContext +import kotlin.math.sign class PillMenu( private val action_count: Int = 0, @@ -406,12 +407,31 @@ fun RowOrColumn( row: Boolean, modifier: Modifier = Modifier, arrangement: Arrangement.HorizontalOrVertical = Arrangement.SpaceEvenly, + alignment: Int = 0, content: @Composable (getWeightModifier: (Float) -> Modifier) -> Unit, ) { if (row) { - Row(modifier, horizontalArrangement = arrangement, verticalAlignment = Alignment.CenterVertically) { content { Modifier.weight(it) } } + Row( + modifier, + horizontalArrangement = arrangement, + verticalAlignment = + when (alignment.sign) { + -1 -> Alignment.Top + 0 -> Alignment.CenterVertically + else -> Alignment.Bottom + } + ) { content { Modifier.weight(it) } } } else { - Column(modifier, verticalArrangement = arrangement, horizontalAlignment = Alignment.CenterHorizontally) { content { Modifier.weight(it) } } + Column( + modifier, + verticalArrangement = arrangement, + horizontalAlignment = + when (alignment.sign) { + -1 -> Alignment.Start + 0 -> Alignment.CenterHorizontally + else -> Alignment.End + } + ) { content { Modifier.weight(it) } } } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenu.desktop.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenu.desktop.kt index dfd9fd991..997772bbf 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenu.desktop.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenu.desktop.kt @@ -250,7 +250,7 @@ internal fun DesktopLongPressMenu( .width(MENU_WIDTH_DP.dp) .focusRequester(focus_requester) .onFocusChanged { - if (focused && !it.hasFocus) { + if (focused && !it.hasFocus && show_background) { close() } focused = it.hasFocus diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuActionProvider.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuActionProvider.kt index 3f62a1f6b..bcfb55fb5 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuActionProvider.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuActionProvider.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Radio import androidx.compose.material.icons.filled.Remove import androidx.compose.material.icons.filled.SubdirectoryArrowRight import androidx.compose.material.ripple.rememberRipple @@ -27,10 +28,12 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.toasterofbread.composekit.platform.vibrateShort +import com.toasterofbread.composekit.utils.common.thenIf import com.toasterofbread.spmp.model.mediaitem.song.Song import com.toasterofbread.spmp.platform.playerservice.PlatformPlayerService import com.toasterofbread.spmp.ui.component.mediaitempreview.MediaItemPreviewLong import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState +import com.toasterofbread.spmp.ui.theme.appHover class LongPressMenuActionProvider( val getContentColour: () -> Color, @@ -148,7 +151,7 @@ class LongPressMenuActionProvider( onAction: () -> Unit, fill_width: Boolean = true ) { - val player = LocalPlayerState.current + val player: PlayerState = LocalPlayerState.current Row( modifier @@ -167,7 +170,10 @@ class LongPressMenuActionProvider( } } ) - .let { if (fill_width) it.fillMaxWidth() else it }, + .appHover(true) + .thenIf(fill_width) { + fillMaxWidth() + }, horizontalArrangement = Arrangement.spacedBy(20.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuData.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuData.kt index 95ea35caf..f4e6b8781 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuData.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/longpressmenu/LongPressMenuData.kt @@ -1,9 +1,15 @@ package com.toasterofbread.spmp.ui.component.longpressmenu import LocalPlayerState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -13,6 +19,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp import com.toasterofbread.composekit.utils.common.getContrasted import com.toasterofbread.spmp.model.mediaitem.MediaItem import com.toasterofbread.spmp.model.mediaitem.MediaItemPreviewInteractionPressStage @@ -25,6 +32,7 @@ import com.toasterofbread.spmp.ui.component.longpressmenu.playlist.PlaylistLongP import com.toasterofbread.spmp.ui.component.longpressmenu.song.SongLongPressMenuActions import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext import com.toasterofbread.spmp.ui.layout.artistpage.ArtistSubscribeButton +import com.toasterofbread.spmp.ui.theme.appHover import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -107,8 +115,12 @@ data class LongPressMenuData( } } -fun Modifier.longPressItem(long_press_menu_data: LongPressMenuData): Modifier = - onGloballyPositioned { - long_press_menu_data.layout_size = it.size - long_press_menu_data.layout_offset = it.positionInRoot() - } +@Composable +fun Modifier.longPressItem(long_press_menu_data: LongPressMenuData): Modifier { + return this + .onGloballyPositioned { + long_press_menu_data.layout_size = it.size + long_press_menu_data.layout_offset = it.positionInRoot() + } + .appHover() +} diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/mediaitempreview/MediaItemPreview.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/mediaitempreview/MediaItemPreview.kt index 81fba2ba0..47dd5b04a 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/mediaitempreview/MediaItemPreview.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/mediaitempreview/MediaItemPreview.kt @@ -254,15 +254,14 @@ fun MediaItemPreviewLong( Column( Modifier .padding(horizontal = 10.dp) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(3.dp) + .fillMaxWidth() ) { val item_title: String? by loaded_item.observeActiveTitle() Text( item_title ?: "", color = contentColour?.invoke() ?: Color.Unspecified, fontSize = font_size, - lineHeight = font_size, +// lineHeight = font_size, maxLines = title_lines, overflow = TextOverflow.Clip ) diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/LoadingSplashView.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/LoadingSplashView.kt index ce08d594a..bc74617f6 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/LoadingSplashView.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/LoadingSplashView.kt @@ -43,10 +43,14 @@ import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.toasterofbread.composekit.utils.common.bitmapResource +import com.toasterofbread.composekit.utils.common.blockGestures +import com.toasterofbread.composekit.utils.common.thenIf +import com.toasterofbread.composekit.utils.common.toFloat import com.toasterofbread.spmp.platform.splash.SplashExtraLoadingContent import com.toasterofbread.spmp.resources.getString import kotlinx.coroutines.delay @@ -125,10 +129,17 @@ fun LoadingSplashView(splash_mode: SplashMode?, loading_message: String?, modifi Text(loading_message, Modifier.padding(horizontal = 20.dp), color = player.theme.on_background) } LinearProgressIndicator(Modifier.fillMaxWidth(), color = player.theme.accent) - - SplashExtraLoadingContent(Modifier) } } + + val extra_content_alpha: Float by animateFloatAsState(show_message.toFloat()) + SplashExtraLoadingContent( + Modifier + .thenIf(!show_message) { + blockGestures() + } + .graphicsLayer { alpha = extra_content_alpha } + ) } } SplashMode.WARNING -> { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/PlayerStateImpl.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/PlayerStateImpl.kt index 9939616b9..3d501048e 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/PlayerStateImpl.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/mainpage/PlayerStateImpl.kt @@ -77,7 +77,7 @@ class PlayerStateImpl(override val context: AppContext, private val coroutine_sc np_swipe_state.value.animateTo( page, when (NowPlayingMainTabPage.Mode.getCurrent(this@PlayerStateImpl)) { - NowPlayingMainTabPage.Mode.LARGE -> spring(Spring.DampingRatioNoBouncy, Spring.StiffnessMediumLow) +// NowPlayingMainTabPage.Mode.LARGE -> spring(Spring.DampingRatioNoBouncy, Spring.StiffnessMediumLow) else -> spring() } ) @@ -321,6 +321,9 @@ class PlayerStateImpl(override val context: AppContext, private val coroutine_sc } override fun showLongPressMenu(data: LongPressMenuData) { + // Check lateinit + data.layout_size + long_press_menu_data = data if (long_press_menu_showing) { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/DesktopCategory.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/DesktopCategory.kt index 845888e42..8eb9df050 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/DesktopCategory.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/apppage/settingspage/category/DesktopCategory.kt @@ -10,15 +10,6 @@ import com.toasterofbread.spmp.model.settings.category.DesktopSettings import com.toasterofbread.spmp.resources.getString internal fun getDesktopCategoryItems(): List { - // (I will never learn regex) - // https://stackoverflow.com/a/36760050 - val ip_regex: Regex = "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}\$".toRegex() - // https://stackoverflow.com/a/12968117 - val port_regex: Regex = "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])\$".toRegex() - - check(ip_regex.matches("127.0.0.1")) - check(port_regex.matches("1111")) - return listOf( GroupSettingsItem( getString("s_group_desktop_system") @@ -29,6 +20,23 @@ internal fun getDesktopCategoryItems(): List { getString("s_key_startup_command"), getString("s_sub_startup_command") ), + GroupSettingsItem( + getString("s_group_server") + ) + ) + getServerGroupItems() +} + +fun getServerGroupItems(): List { + // (I will never learn regex) + // https://stackoverflow.com/a/36760050 + val ip_regex: Regex = "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}\$".toRegex() + // https://stackoverflow.com/a/12968117 + val port_regex: Regex = "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])\$".toRegex() + + check(ip_regex.matches("127.0.0.1")) + check(port_regex.matches("1111")) + + return listOf( InfoTextSettingsItem( getString("s_info_server") ), @@ -65,7 +73,12 @@ internal fun getDesktopCategoryItems(): List { TextFieldSettingsItem( SettingsValueState(DesktopSettings.Key.SERVER_LOCAL_COMMAND.getName()), - getString("s_key_server_command"), getString("s_sub_server_command") + getString("s_key_local_server_command"), getString("s_sub_local_server_command") + ), + + ToggleSettingsItem( + SettingsValueState(DesktopSettings.Key.SERVER_LOCAL_START_AUTOMATICALLY.getName()), + getString("s_key_server_local_start_automatically"), getString("s_sub_server_local_start_automatically") ), ToggleSettingsItem( diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlaying.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlaying.kt index cf7f0240a..babde6b00 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlaying.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlaying.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterialApi::class) + package com.toasterofbread.spmp.ui.layout.nowplaying import LocalNowPlayingExpansion @@ -27,6 +29,7 @@ import com.toasterofbread.composekit.platform.composable.composeScope import com.toasterofbread.composekit.platform.vibrateShort import com.toasterofbread.composekit.utils.* import com.toasterofbread.composekit.utils.common.amplifyPercent +import com.toasterofbread.composekit.utils.common.getContrasted import com.toasterofbread.composekit.utils.composable.RecomposeOnInterval import com.toasterofbread.composekit.utils.composable.getTop import com.toasterofbread.composekit.utils.modifier.brushBackground @@ -55,14 +58,28 @@ private const val GRADIENT_TOP_START_RATIO = 0.7f private const val OVERSCROLL_CLEAR_DISTANCE_THRESHOLD_DP = 5f internal fun PlayerState.getNPBackground(theme_mode: ThemeMode = np_theme_mode): Color { + val pages: List = NowPlayingPage.ALL.filter { it.shouldShow(this) } + + val override: Color? = pages[expansion.swipe_state.currentValue.coerceAtMost(pages.size - 1)].getPlayerBackgroundColourOverride(this) + if (override != null) { + return override + } + return when (theme_mode) { ThemeMode.BACKGROUND -> theme.accent - ThemeMode.ELEMENTS -> theme.background - ThemeMode.NONE -> theme.background + ThemeMode.ELEMENTS -> theme.card + ThemeMode.NONE -> theme.card } } internal fun PlayerState.getNPOnBackground(): Color { + val pages: List = NowPlayingPage.ALL.filter { it.shouldShow(this) } + + val override: Color? = pages[expansion.swipe_state.currentValue.coerceAtMost(pages.size - 1)].getPlayerBackgroundColourOverride(this) + if (override != null) { + return override.getContrasted() + } + return when (np_theme_mode) { ThemeMode.BACKGROUND -> theme.on_accent ThemeMode.ELEMENTS -> theme.accent @@ -189,7 +206,6 @@ fun NowPlaying(swipe_state: SwipeableState, swipe_anchors: Map, val song_gradient_depth: Float? = player.status.m_song?.PlayerGradientDepth?.observe(player.database)?.value - val pages: List = NowPlayingPage.ALL.filter { it.shouldShow(player) } val large_page_showing: Boolean = !player.isPortrait() && player.isLargeFormFactor() val swipe_modifier: Modifier = remember(swipe_anchors, large_page_showing) { @@ -232,11 +248,6 @@ fun NowPlaying(swipe_state: SwipeableState, swipe_anchors: Map, } .brushBackground { with(density) { - val override_colour: Color? = pages[swipe_state.currentValue.coerceAtMost(pages.size - 1)].getPlayerBackgroundColourOverride(player) - if (override_colour != null) { - return@brushBackground Brush.verticalGradient(listOf(override_colour, override_colour)) - } - val screen_height_px = page_height.toPx() val v_offset = (expansion.get() - 1f).coerceAtLeast(0f) * screen_height_px diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlayingExpansionState.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlayingExpansionState.kt index aadd2d2fe..138b7bc05 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlayingExpansionState.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/NowPlayingExpansionState.kt @@ -63,7 +63,7 @@ class NowPlayingExpansionState( swipe_state: State>, private val coroutine_scope: CoroutineScope ): ExpansionState { - private val swipe_state by swipe_state + val swipe_state by swipe_state override val top_bar_mode: MutableState = mutableStateOf(MusicTopBarMode.default) diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/Controls.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/Controls.kt index 2184341c2..1d442e82b 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/Controls.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/Controls.kt @@ -2,6 +2,7 @@ package com.toasterofbread.spmp.ui.layout.nowplaying.maintab import LocalPlayerState import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -34,6 +35,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -58,11 +60,73 @@ import com.toasterofbread.spmp.ui.layout.nowplaying.getNPOnBackground private const val TITLE_FONT_SIZE_SP: Float = 21f private const val ARTIST_FONT_SIZE_SP: Float = 12f + +@Composable +fun PlayerButton( + image: ImageVector, + size: Dp = 60.dp, + enabled: Boolean = true, + getBackgroundColour: PlayerState.() -> Color = { getNPBackground() }, + getOnBackgroundColour: PlayerState.() -> Color = { getNPOnBackground() }, + getAccentColour: (PlayerState.() -> Color)? = null, + onClick: () -> Unit +) { + val player: PlayerState = LocalPlayerState.current + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .bounceOnClick() + .clickable( + onClick = onClick, + indication = null, + interactionSource = remember { MutableInteractionSource() }, + enabled = enabled + ) + .alpha(if (enabled) 1.0f else 0.5f) + ) { + val painter: VectorPainter = rememberVectorPainter(image) + + Canvas( + Modifier + .requiredSize(size) + // https://stackoverflow.com/a/67820996 + .graphicsLayer { alpha = 0.99f } + ) { + with(painter) { + draw(this@Canvas.size) + } + + val gradient_end: Float + val gradient_colours: List + + val accent: Color? = if (player.np_theme_mode != ThemeMode.NONE) getAccentColour?.invoke(player) else null + if (accent != null) { + gradient_end = this@Canvas.size.width * 0.95f + gradient_colours = listOf(getOnBackgroundColour(player), getOnBackgroundColour(player), accent) + } + else { + gradient_end = this@Canvas.size.width * 1.9f + gradient_colours = listOf(getOnBackgroundColour(player), getBackgroundColour(player)) + } + + drawRect( + Brush.linearGradient( + gradient_colours, + end = Offset(gradient_end, gradient_end) + ), + blendMode = BlendMode.SrcAtop + ) + } + } +} + @Composable internal fun Controls( song: Song?, seek: (Float) -> Unit, modifier: Modifier = Modifier, + enabled: Boolean = true, button_row_arrangement: Arrangement.Horizontal = Arrangement.Center, seek_bar_next_to_buttons: Boolean = false, disable_text_marquees: Boolean = false, @@ -79,7 +143,7 @@ internal fun Controls( artistRowStartContent: @Composable RowScope.() -> Unit = {}, artistRowEndContent: @Composable RowScope.() -> Unit = {} ) { - val player = LocalPlayerState.current + val player: PlayerState = LocalPlayerState.current val song_title: String? by song?.observeActiveTitle() val song_artist_title: String? by song?.Artist?.observePropertyActiveTitle() @@ -94,61 +158,6 @@ internal fun Controls( MediaItemTitleEditDialog(song) { show_title_edit_dialog = false } } - @Composable - fun PlayerButton( - image: ImageVector, - size: Dp = 60.dp, - enabled: Boolean = true, - onClick: () -> Unit - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .bounceOnClick() - .clickable( - onClick = onClick, - indication = null, - interactionSource = remember { MutableInteractionSource() }, - enabled = enabled - ) - .alpha(if (enabled) 1.0f else 0.5f) - ) { - val painter = rememberVectorPainter(image) - - Canvas( - Modifier - .requiredSize(size) - // https://stackoverflow.com/a/67820996 - .graphicsLayer { alpha = 0.99f } - ) { - with(painter) { - draw(this@Canvas.size) - } - - val gradient_end: Float - val gradient_colours: List - - val accent: Color? = if (player.np_theme_mode != ThemeMode.NONE) getAccentColour?.invoke(player) else null - if (accent != null) { - gradient_end = this@Canvas.size.width * 0.95f - gradient_colours = listOf(getOnBackgroundColour(player), getOnBackgroundColour(player), accent) - } - else { - gradient_end = this@Canvas.size.width * 1.9f - gradient_colours = listOf(getOnBackgroundColour(player), getBackgroundColour(player)) - } - - drawRect( - Brush.linearGradient( - gradient_colours, - end = Offset(gradient_end, gradient_end) - ), - blendMode = BlendMode.SrcAtop - ) - } - } - } - Column(modifier, verticalArrangement = vertical_arrangement) { Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { Marquee(Modifier.fillMaxWidth(), disable = disable_text_marquees) { @@ -163,6 +172,7 @@ internal fun Controls( modifier = Modifier .fillMaxWidth() .platformClickable( + enabled = enabled, onAltClick = { show_title_edit_dialog = !show_title_edit_dialog player.context.vibrateShort() @@ -182,7 +192,9 @@ internal fun Controls( maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier + .fillMaxWidth() .platformClickable( + enabled = enabled, onClick = { val artist: Artist? = song?.Artist?.get(player.database) if (artist?.isForItem() == false) { @@ -208,7 +220,7 @@ internal fun Controls( } if (!seek_bar_next_to_buttons) { - SeekBar(seek, getColour = getOnBackgroundColour, getTrackColour = getSeekBarTrackColour) + SeekBar(seek, getColour = getOnBackgroundColour, getTrackColour = getSeekBarTrackColour, enabled = enabled) } Row( @@ -221,8 +233,11 @@ internal fun Controls( // Previous PlayerButton( Icons.Rounded.SkipPrevious, - enabled = player.status.m_has_previous, - size = 60.dp + enabled = enabled && player.status.m_has_previous, + size = 60.dp, + getBackgroundColour = getBackgroundColour, + getOnBackgroundColour = getOnBackgroundColour, + getAccentColour = getAccentColour ) { player.controller?.seekToPrevious() } @@ -230,8 +245,11 @@ internal fun Controls( // Play / pause PlayerButton( if (player.status.m_playing) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, - enabled = song != null, - size = 75.dp + enabled = enabled && song != null, + size = 75.dp, + getBackgroundColour = getBackgroundColour, + getOnBackgroundColour = getOnBackgroundColour, + getAccentColour = getAccentColour ) { player.controller?.playPause() } @@ -239,14 +257,17 @@ internal fun Controls( // Next PlayerButton( Icons.Rounded.SkipNext, - enabled = player.status.m_has_next, - size = 60.dp + enabled = enabled && player.status.m_has_next, + size = 60.dp, + getBackgroundColour = getBackgroundColour, + getOnBackgroundColour = getOnBackgroundColour, + getAccentColour = getAccentColour ) { player.controller?.seekToNext() } if (seek_bar_next_to_buttons) { - SeekBar(seek, Modifier.fillMaxWidth().weight(1f), getColour = getOnBackgroundColour, getTrackColour = getSeekBarTrackColour) + SeekBar(seek, Modifier.fillMaxWidth().weight(1f), getColour = getOnBackgroundColour, getTrackColour = getSeekBarTrackColour, enabled = enabled) } buttonRowEndContent() diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeBottomBar.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeBottomBar.kt index 7a4dafec3..19cf087f5 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeBottomBar.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeBottomBar.kt @@ -3,6 +3,7 @@ package com.toasterofbread.spmp.ui.layout.nowplaying.maintab import LocalPlayerState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -14,13 +15,15 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import com.toasterofbread.composekit.utils.modifier.bounceOnClick import com.toasterofbread.spmp.model.mediaitem.song.Song import com.toasterofbread.spmp.ui.component.LikeDislikeButton import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState +import com.toasterofbread.spmp.ui.theme.appHover import com.toasterofbread.spmp.youtubeapi.YoutubeApi @Composable -internal fun LargeBottomBar(modifier: Modifier = Modifier, start_modifier: Modifier = Modifier) { +internal fun LargeBottomBar(modifier: Modifier = Modifier) { val player: PlayerState = LocalPlayerState.current val auth_state: YoutubeApi.UserAuthState? = player.context.ytapi.user_auth_state val current_song: Song? by player.status.song_state @@ -31,24 +34,20 @@ internal fun LargeBottomBar(modifier: Modifier = Modifier, start_modifier: Modif modifier, verticalAlignment = Alignment.CenterVertically ) { - Row( - start_modifier.fillMaxWidth(0.5f), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - if (auth_state != null) { - current_song?.also { song -> - LikeDislikeButton(song, auth_state) { button_colour } - } + if (auth_state != null) { + current_song?.also { song -> + LikeDislikeButton(song, auth_state) { button_colour } } } + Spacer(Modifier.fillMaxWidth().weight(1f)) + Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { - IconButton({ player.expansion.close() }) { + IconButton({ player.expansion.close() }, Modifier.bounceOnClick().appHover(true)) { Icon(Icons.Default.KeyboardArrowDown, null, tint = button_colour) } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeTopBar.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeTopBar.kt new file mode 100644 index 000000000..463537e2d --- /dev/null +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/LargeTopBar.kt @@ -0,0 +1,22 @@ +package com.toasterofbread.spmp.ui.layout.nowplaying.maintab + +import LocalPlayerState +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.toasterofbread.spmp.model.mediaitem.song.Song +import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState + +@Composable +internal fun LargeTopBar(modifier: Modifier = Modifier) { + val player: PlayerState = LocalPlayerState.current + val current_song: Song? by player.status.song_state + + Row( + modifier, + verticalAlignment = Alignment.CenterVertically + ) { + } +} diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabLandscape.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabLandscape.kt similarity index 100% rename from shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabLandscape.kt rename to shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabLandscape.kt diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabLarge.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabLarge.kt similarity index 53% rename from shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabLarge.kt rename to shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabLarge.kt index bb4c7a44b..3d43a896c 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabLarge.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabLarge.kt @@ -2,10 +2,12 @@ package com.toasterofbread.spmp.ui.layout.nowplaying.maintab import LocalNowPlayingExpansion import LocalPlayerState +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -31,15 +33,27 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection @@ -49,9 +63,13 @@ import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.times import androidx.compose.ui.zIndex +import com.github.krottv.compose.sliders.lerp import com.toasterofbread.composekit.platform.composable.composeScope +import com.toasterofbread.composekit.utils.common.amplify +import com.toasterofbread.composekit.utils.common.blendWith import com.toasterofbread.composekit.utils.common.getContrasted import com.toasterofbread.composekit.utils.common.thenIf +import com.toasterofbread.composekit.utils.common.toFloat import com.toasterofbread.composekit.utils.composable.getTop import com.toasterofbread.spmp.model.settings.category.NowPlayingQueueWaveBorderMode import com.toasterofbread.spmp.ui.layout.apppage.mainpage.MINIMISED_NOW_PLAYING_HEIGHT_DP @@ -65,18 +83,53 @@ import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingPage.Companion.top import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingTopBar import com.toasterofbread.spmp.ui.layout.nowplaying.ThemeMode import com.toasterofbread.spmp.ui.layout.nowplaying.getNPAltBackground -import com.toasterofbread.spmp.ui.layout.nowplaying.getNPAltOnBackground -import com.toasterofbread.spmp.ui.layout.nowplaying.getNPBackground import com.toasterofbread.spmp.ui.layout.nowplaying.maintab.thumbnailrow.LargeThumbnailRow -import com.toasterofbread.spmp.ui.layout.nowplaying.queue.QUEUE_CORNER_RADIUS_DP import com.toasterofbread.spmp.ui.layout.nowplaying.queue.QueueTab import kotlin.math.absoluteValue +import kotlin.math.roundToInt + +private enum class ControlsPosition { + ABOVE_IMAGE, ABOVE_QUEUE +} + +@Composable +private fun MainTabControls( + onSeek: (Float) -> Unit, + enabled: Boolean, + modifier: Modifier = Modifier +) { + val player: PlayerState = LocalPlayerState.current + val expansion: NowPlayingExpansionState = LocalNowPlayingExpansion.current + val absolute_expansion: Float = expansion.getAbsolute() + + Controls( + player.status.m_song, + { seek_progress -> + player.withPlayer { + seekTo((duration_ms * seek_progress).toLong()) + } + onSeek(seek_progress) + }, + modifier.padding(top = 30.dp), + enabled = enabled, + vertical_arrangement = Arrangement.spacedBy(10.dp), + title_text_max_lines = 2, + title_font_size = 25.sp, + artist_font_size = 18.sp, + seek_bar_next_to_buttons = true, + text_align = TextAlign.Start, + getBackgroundColour = { theme.background }, + getOnBackgroundColour = { theme.on_background }, + getAccentColour = { theme.accent } + ) +} @Composable internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_bar: NowPlayingTopBar, content_padding: PaddingValues, modifier: Modifier = Modifier) { val player: PlayerState = LocalPlayerState.current val expansion: NowPlayingExpansionState = LocalNowPlayingExpansion.current val layout_direction: LayoutDirection = LocalLayoutDirection.current + val density: Density = LocalDensity.current val proportion: Float = WindowInsets.getTop() / player.screen_size.height val proportion_exp: Float by remember { derivedStateOf { @@ -86,23 +139,49 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b val absolute_expansion: Float by remember { derivedStateOf { expansion.getAbsolute() } } + val expanded: Boolean by remember { derivedStateOf { absolute_expansion >= 1f } } val min_height: Dp = MINIMISED_NOW_PLAYING_HEIGHT_DP.dp - (MINIMISED_NOW_PLAYING_V_PADDING_DP.dp * 2) val height: Dp = ((absolute_expansion * (page_height - min_height)) + min_height).coerceAtLeast(min_height) val current_horizontal_padding: Dp = lerp(horizontal_padding_minimised, horizontal_padding, absolute_expansion) + val inner_padding: Dp = lerp(0.dp, 25.dp, absolute_expansion) + val start_padding: Dp = maxOf(inner_padding, content_padding.calculateStartPadding(layout_direction) + current_horizontal_padding) + val end_padding: Dp = maxOf(inner_padding, content_padding.calculateEndPadding(layout_direction) + current_horizontal_padding) + val top_padding: Dp = top_padding val bottom_padding: Dp = bottom_padding - val start_padding: Dp = content_padding.calculateStartPadding(layout_direction) + current_horizontal_padding - val end_padding: Dp = content_padding.calculateEndPadding(layout_direction) + current_horizontal_padding val bottom_bar_height: Dp = (horizontal_padding * 2) + bottom_padding val inner_bottom_padding: Dp = horizontal_padding + var controls_y_position: Float by remember { mutableStateOf(0f) } + var thumbnail_y_position: Float by remember { mutableStateOf(0f) } + var display_mode: ControlsPosition by remember { mutableStateOf(ControlsPosition.ABOVE_IMAGE) } + val display_mode_transition: Float by animateFloatAsState((display_mode == ControlsPosition.ABOVE_IMAGE).toFloat()) + + val bar_background_colour: Color = player.theme.card + val stroke_width: Dp = 1.dp + val stroke_colour: Color = bar_background_colour.amplify(255f) + BoxWithConstraints( modifier = modifier.height(height) ) { + LargeTopBar( + Modifier + .fillMaxWidth() + .height(bottom_bar_height) + .graphicsLayer { alpha = absolute_expansion } + .align(Alignment.TopStart) + .background(bar_background_colour) + .drawWithContent { + drawContent() + drawLine(stroke_colour, Offset(0f, size.height), Offset(size.width, size.height), stroke_width.toPx()) + } + .zIndex(1f) + ) + Row( Modifier .fillMaxSize() @@ -118,7 +197,7 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b ) { val extra_width: Dp = 0.dp//(page_width / 2) - page_height val parent_max_width: Dp = this@BoxWithConstraints.maxWidth - val thumb_size: Dp = minOf(height, parent_max_width * 0.5f) + val thumb_size: Dp = minOf(height, parent_max_width * 0.5f) - (inner_padding) Box(Modifier.requiredSize(0.dp).zIndex(1f)) { Box( @@ -130,42 +209,29 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b (page_height - (bottom_bar_height * absolute_expansion / 2) - top_padding - 2.dp).roundToPx() ) } - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - player.expansion.close() + .pointerInput(Unit) { + detectTapGestures { + player.expansion.close() + } } ) { - val background_colour: Color = - when (player.np_theme_mode) { - ThemeMode.BACKGROUND -> player.theme.accent - else -> player.theme.background - } - Canvas(Modifier.fillMaxSize()) { drawLine( - when (player.np_theme_mode) { - ThemeMode.BACKGROUND -> player.getNPAltBackground() - ThemeMode.ELEMENTS -> player.theme.accent - ThemeMode.NONE -> player.theme.on_background - }, + stroke_colour, start = Offset.Zero, end = Offset(size.width, 0f), - strokeWidth = 5.dp.toPx() + strokeWidth = (stroke_width + 2.dp).toPx() ) - drawRect(background_colour) + drawRect(bar_background_colour) } - CompositionLocalProvider(LocalContentColor provides background_colour.getContrasted()) { + CompositionLocalProvider(LocalContentColor provides bar_background_colour.getContrasted()) { LargeBottomBar( Modifier .align(Alignment.CenterEnd) .fillMaxWidth() - .padding(horizontal = 15.dp), - Modifier - .padding(top = bottom_bar_height - inner_bottom_padding - bottom_padding) + .padding(horizontal = 15.dp) ) } } @@ -182,11 +248,51 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b ) { Spacer(Modifier.requiredHeight(inner_bottom_padding).weight(1f, false)) + var controls_height: Int by remember { mutableStateOf(0) } + + MainTabControls( + { seek_state = it }, + expanded && display_mode == ControlsPosition.ABOVE_IMAGE, + Modifier + .thenIf(!expanded) { + requiredHeight(with (density) { + controls_height.toDp() * ((absolute_expansion - 0.5f) * 2f) + }) + } + .onGloballyPositioned { + controls_y_position = it.positionInParent().y + } + .onSizeChanged { + if (expanded) { + controls_height = it.height + } + } + .graphicsLayer { + alpha = display_mode_transition + } + .scale(1f, absolute_expansion) + .padding(bottom = 20.dp) + .width(lerp(parent_max_width, thumb_size, absolute_expansion)) + ) + LargeThumbnailRow( Modifier .height(thumb_size) .padding(start = (extra_width * absolute_expansion / 2).coerceAtLeast(0.dp)) - .width(lerp(parent_max_width, thumb_size, absolute_expansion)), + .width(lerp(parent_max_width, thumb_size, absolute_expansion)) + .onGloballyPositioned { + thumbnail_y_position = with (density) {( + it.positionInParent().y + + lerp(-controls_height / 2f, 0f, maxOf(display_mode_transition, 1f - absolute_expansion)) + - 50.dp.toPx() + )} + } + .offset { + IntOffset( + 0, + lerp(-controls_height / 2f, 0f, maxOf(display_mode_transition, 1f - absolute_expansion)).roundToInt() + ) + }, onThumbnailLoaded = { song, image -> onThumbnailLoaded(song, image) }, @@ -196,6 +302,18 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b getSeekState = { seek_state }, disable_parent_scroll_while_menu_open = false ) + + Spacer( + Modifier.onGloballyPositioned { + with (density) { + if (expanded) { + display_mode = + if (it.positionInWindow().y.toDp() > (page_height - bottom_bar_height - inner_bottom_padding)) ControlsPosition.ABOVE_QUEUE + else ControlsPosition.ABOVE_IMAGE + } + } + } + ) } Spacer(Modifier.requiredHeight(lerp(0.dp, inner_bottom_padding, proportion_exp))) @@ -208,13 +326,12 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b Modifier .weight(1f) .fillMaxWidth() - .padding(start = 10.dp) + .padding(start = inner_padding) ) { composeScope( - remember { { it: Float -> seek_state = it } }, top_bar, page_height - ) { setSeekState, top_bar, page_height -> + ) { top_bar, page_height -> val player: PlayerState = LocalPlayerState.current Column( @@ -231,66 +348,71 @@ internal fun NowPlayingMainTabPage.NowPlayingMainTabLarge(page_height: Dp, top_b }, verticalArrangement = Arrangement.SpaceEvenly ) { - val controls_visible by remember { derivedStateOf { player.expansion.getAbsolute() > 0.0f } } Column( Modifier .weight(1f) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(Modifier.fillMaxHeight(0.075f)) - if (controls_visible) { - Controls( - player.status.m_song, - { seek_progress -> - player.withPlayer { - seekTo((duration_ms * seek_progress).toLong()) - } - setSeekState(seek_progress) - }, - Modifier.fillMaxWidth(), - vertical_arrangement = Arrangement.spacedBy(10.dp), - title_text_max_lines = 2, - title_font_size = 35.sp, - artist_font_size = 18.sp, - seek_bar_next_to_buttons = true, - text_align = TextAlign.Start, - getBackgroundColour = { theme.background }, - getOnBackgroundColour = { theme.on_background }, - getAccentColour = { theme.accent } + AnimatedVisibility( + display_mode == ControlsPosition.ABOVE_QUEUE, + Modifier.offset { + IntOffset( + 0, + thumbnail_y_position.roundToInt() + ) + } + ) { + MainTabControls( + { seek_state = it }, + expanded ) } + val queue_shape: Shape = RoundedCornerShape(10.dp) + val getVerticalOffset: () -> Int = remember {{ lerp(thumbnail_y_position, controls_y_position, display_mode_transition).roundToInt() }} + QueueTab( null, Modifier .fillMaxHeight() .weight(1f) + .offset { + IntOffset( + 0, + getVerticalOffset() + ) + } .thenIf(player.np_theme_mode != ThemeMode.BACKGROUND) { border( - 2.dp, - if (player.np_theme_mode == ThemeMode.ELEMENTS) player.theme.accent - else player.theme.on_background, - RoundedCornerShape(QUEUE_CORNER_RADIUS_DP.dp) + stroke_width, + stroke_colour, + queue_shape ) }, inline = true, - border_thickness = 2.dp, + border_thickness = stroke_width, wave_border_mode_override = NowPlayingQueueWaveBorderMode.SCROLL, + shape = queue_shape, button_row_arrangement = Arrangement.spacedBy(5.dp), - content_padding = PaddingValues(bottom = inner_bottom_padding), + content_padding = PaddingValues( + bottom = inner_bottom_padding + with (density) { getVerticalOffset().toDp() } + ), getBackgroundColour = { - if (player.np_theme_mode == ThemeMode.BACKGROUND) getNPAltOnBackground() - else theme.background + getNPAltBackground() +// if (player.np_theme_mode == ThemeMode.BACKGROUND) getNPAltOnBackground() +// else theme.background }, getOnBackgroundColour = { - when (player.np_theme_mode) { - ThemeMode.BACKGROUND -> getNPBackground() - ThemeMode.ELEMENTS -> theme.accent - ThemeMode.NONE -> theme.on_background - } - } + theme.accent.blendWith(theme.background, 0.01f) +// when (player.np_theme_mode) { +// ThemeMode.BACKGROUND -> getNPBackground() +// ThemeMode.ELEMENTS -> theme.accent +// ThemeMode.NONE -> theme.on_background +// } + }, + getWaveBorderColour = { stroke_colour } ) } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabNarrow.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabNarrow.kt new file mode 100644 index 000000000..5533ba01c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabNarrow.kt @@ -0,0 +1,159 @@ +package com.toasterofbread.spmp.ui.layout.nowplaying.maintab + +import LocalPlayerState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Pause +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.SkipNext +import androidx.compose.material.icons.rounded.SkipPrevious +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layout +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.toasterofbread.composekit.utils.common.getValue +import com.toasterofbread.composekit.utils.common.isJP +import com.toasterofbread.composekit.utils.common.thenIf +import com.toasterofbread.spmp.model.mediaitem.MediaItemThumbnailProvider +import com.toasterofbread.spmp.model.mediaitem.song.Song +import com.toasterofbread.spmp.ui.component.RowOrColumn +import com.toasterofbread.spmp.ui.component.Thumbnail +import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState +import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingPage.Companion.bottom_padding +import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingPage.Companion.horizontal_padding +import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingPage.Companion.top_padding +import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingTopBar + +@Composable +internal fun NowPlayingMainTabPage.NowPlayingMainTabNarrow(page_height: Dp, top_bar: NowPlayingTopBar, content_padding: PaddingValues, vertical: Boolean, modifier: Modifier = Modifier) { + val player: PlayerState = LocalPlayerState.current + val song: Song? by player.status.song_state + + RowOrColumn( + row = !vertical, + alignment = 0, + modifier = modifier + .requiredHeight(page_height) + .padding(content_padding) + .padding(horizontal = horizontal_padding) + .padding(top = top_padding, bottom = bottom_padding) + ) { weight -> + val spacing: Dp = 10.dp + + if (vertical) { + song?.Thumbnail(MediaItemThumbnailProvider.Quality.LOW, Modifier.aspectRatio(1f)) { + onThumbnailLoaded(song, it) + } + Spacer(Modifier.height(spacing)) + + player.PlayButton() + player.NextButton() + player.PreviousButton() + Spacer(Modifier.height(spacing)) + } + else { + player.PreviousButton() + player.PlayButton() + player.NextButton() + Spacer(Modifier.width(spacing)) + + song?.Thumbnail(MediaItemThumbnailProvider.Quality.LOW, Modifier.aspectRatio(1f)) { + onThumbnailLoaded(song, it) + } + Spacer(Modifier.width(spacing)) + } + + val active_title: String? by song?.observeActiveTitle() + active_title?.also { title -> + for (segment in title.split(' ')) { + if (vertical && segment.all { it.isJP() }) { + var offset: Dp = 0.dp + for (c in segment) { + Text( + c.toString(), + Modifier + .offset(0.dp, offset) + .thenIf(c == 'ー') { + rotate(-90f) + }, + style = MaterialTheme.typography.titleLarge + ) + offset -= 5.dp + } + } + else { + Text( + segment, + Modifier.run { + if (vertical) rotate(90f).vertical() + else this + }, + softWrap = false, + overflow = TextOverflow.Visible, + style = MaterialTheme.typography.titleLarge + ) + } + } + } + + Spacer(Modifier.fillMaxSize().then(weight(1f))) + } +} + +fun Modifier.vertical() = layout { measurable, constraints -> + val placeable: Placeable = measurable.measure(constraints) + layout(placeable.height, placeable.width) { + placeable.place( + x = -(placeable.width / 2 - placeable.height / 2), + y = -(placeable.height / 2 - placeable.width / 2) + ) + } +} + +@Composable +private fun PlayerState.PlayButton() { + PlayerButton( + if (status.m_playing) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + enabled = status.m_song != null, + size = 75.dp + ) { + controller?.playPause() + } +} + +@Composable +private fun PlayerState.NextButton() { + PlayerButton( + Icons.Rounded.SkipNext, + enabled = status.m_has_next, + size = 60.dp + ) { + controller?.seekToNext() + } +} + +@Composable +private fun PlayerState.PreviousButton() { + PlayerButton( + Icons.Rounded.SkipPrevious, + enabled = status.m_has_previous, + size = 60.dp + ) { + controller?.seekToPrevious() + } +} diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabPage.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabPage.kt index 36b4f6e0f..8b4ef5c5a 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabPage.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabPage.kt @@ -1,6 +1,7 @@ package com.toasterofbread.spmp.ui.layout.nowplaying.maintab import LocalPlayerState +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -10,6 +11,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.toasterofbread.composekit.utils.common.blendWith @@ -24,6 +26,7 @@ import com.toasterofbread.spmp.ui.layout.nowplaying.getNPBackground import kotlinx.coroutines.delay private const val ACCENT_CLEAR_WAIT_TIME_MS: Long = 1000 +private const val NARROW_PLAYER_MAX_SIZE_DP: Float = 120f class NowPlayingMainTabPage: NowPlayingPage() { enum class Mode { @@ -93,7 +96,8 @@ class NowPlayingMainTabPage: NowPlayingPage() { override fun getPlayerBackgroundColourOverride(player: PlayerState): Color? { if (!player.isPortrait() && player.isLargeFormFactor()) { - return player.theme.background.blendWith(player.getNPBackground(), player.expansion.getBounded()) + return player.theme.accent.blendWith(player.theme.background, 0.05f) +// return player.theme.background.blendWith(accented_background, player.expansion.getBounded()) } return null } @@ -125,14 +129,22 @@ class NowPlayingMainTabPage: NowPlayingPage() { } } - if (player.isPortrait()) { - NowPlayingMainTabPortrait(page_height, top_bar, content_padding, modifier) - } - else if (player.isLargeFormFactor()) { - NowPlayingMainTabLarge(page_height, top_bar, content_padding, modifier) - } - else { - NowPlayingMainTabLandscape(page_height, top_bar, content_padding, modifier) + BoxWithConstraints(modifier) { + if (maxWidth <= NARROW_PLAYER_MAX_SIZE_DP.dp) { + NowPlayingMainTabNarrow(page_height, top_bar, content_padding, true) + } + else if (maxHeight <= NARROW_PLAYER_MAX_SIZE_DP.dp) { + NowPlayingMainTabNarrow(page_height, top_bar, content_padding, false) + } + else if (player.isPortrait()) { + NowPlayingMainTabPortrait(page_height, top_bar, content_padding) + } + else if (player.isLargeFormFactor()) { + NowPlayingMainTabLarge(page_height, top_bar, content_padding) + } + else { + NowPlayingMainTabLandscape(page_height, top_bar, content_padding) + } } } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabPortrait.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabPortrait.kt similarity index 100% rename from shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/MainTabPortrait.kt rename to shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/NowPlayingMainTabPortrait.kt diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/SeekBar.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/SeekBar.kt index be3a15e35..68acad624 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/SeekBar.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/SeekBar.kt @@ -45,10 +45,11 @@ import com.toasterofbread.spmp.ui.layout.nowplaying.getNPOnBackground fun SeekBar( seek: (Float) -> Unit, modifier: Modifier = Modifier, + enabled: Boolean = true, getColour: PlayerState.() -> Color = { getNPOnBackground() }, getTrackColour: PlayerState.() -> Color = { getNPAltOnBackground() } ) { - val player = LocalPlayerState.current + val player: PlayerState = LocalPlayerState.current var position_override by remember { mutableStateOf(null) } var old_position by remember { mutableStateOf(null) } @@ -84,6 +85,7 @@ fun SeekBar( SliderValueHorizontal( value = getSliderValue(), + enabled = enabled, onValueChange = { position_override = it }, diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/thumbnailrow/LargeThumbnailRow.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/thumbnailrow/LargeThumbnailRow.kt index e6e004398..09779670f 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/thumbnailrow/LargeThumbnailRow.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/maintab/thumbnailrow/LargeThumbnailRow.kt @@ -157,8 +157,8 @@ fun LargeThumbnailRow( var button_row_width: Dp by remember { mutableStateOf(0.dp) } - MeasureUnconstrainedView({ ButtonRow() }) { width, _ -> - button_row_width = with (density) { width.toDp() } + MeasureUnconstrainedView({ ButtonRow() }) { size -> + button_row_width = with (density) { size.width.toDp() } ButtonRow(Modifier.width(button_row_width * (1f - expansion.getBounded()))) } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/PaletteSelectorOverlayMenu.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/PaletteSelectorOverlayMenu.kt index 143ae16d6..2c1567f67 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/PaletteSelectorOverlayMenu.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/overlay/PaletteSelectorOverlayMenu.kt @@ -50,7 +50,7 @@ import kotlin.math.roundToInt val DEFAULT_THUMBNAIL_ROUNDING: Int @Composable get() = - if (LocalPlayerState.current.isLargeFormFactor()) 1 + if (LocalPlayerState.current.isLargeFormFactor()) 0 else 5 const val MIN_THUMBNAIL_ROUNDING: Int = 0 diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueButtonsRow.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueButtonsRow.kt index a86349255..4a7ca2421 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueButtonsRow.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueButtonsRow.kt @@ -17,12 +17,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.toasterofbread.composekit.platform.composable.platformClickable import com.toasterofbread.composekit.platform.vibrateShort import com.toasterofbread.composekit.utils.common.getContrasted +import com.toasterofbread.composekit.utils.composable.PlatformClickableButton import com.toasterofbread.composekit.utils.modifier.bounceOnClick import com.toasterofbread.spmp.resources.getString import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext +import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState +import com.toasterofbread.spmp.ui.theme.appHover @OptIn(ExperimentalFoundationApi::class) @Composable @@ -32,9 +37,9 @@ fun QueueButtonsRow( arrangement: Arrangement.Horizontal = Arrangement.SpaceEvenly, scrollToitem: (Int) -> Unit ) { - val padding = 10.dp - val player = LocalPlayerState.current - val button_colour = getButtonColour() + val padding: Dp = 10.dp + val player: PlayerState = LocalPlayerState.current + val button_colour: Color = getButtonColour() Row( Modifier @@ -43,11 +48,13 @@ fun QueueButtonsRow( .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { + val button_modifier: Modifier = Modifier.appHover(true).bounceOnClick() + Row(Modifier.fillMaxWidth().weight(1f), horizontalArrangement = arrangement) { - RepeatButton(getButtonColour, Modifier.fillMaxHeight().bounceOnClick()) - StopAfterSongButton(getButtonColour, Modifier.fillMaxHeight().bounceOnClick()) + RepeatButton(getButtonColour, button_modifier.fillMaxHeight()) + StopAfterSongButton(getButtonColour, button_modifier.fillMaxHeight()) - Button( + PlatformClickableButton( onClick = { player.controller?.service_player?.undoableAction { if (multiselect_context.is_active) { @@ -61,7 +68,7 @@ fun QueueButtonsRow( } } }, - modifier = Modifier.bounceOnClick(), + modifier = button_modifier, colors = ButtonDefaults.buttonColors( containerColor = button_colour, contentColor = button_colour.getContrasted() @@ -71,63 +78,47 @@ fun QueueButtonsRow( Text(getString("queue_clear")) } - Surface( - Modifier - .bounceOnClick() - .clip(FilledButtonTokens.ContainerShape.toShape()) - .combinedClickable( - onClick = { - player.controller?.service_player?.undoableAction { - if (multiselect_context.is_active) { - shuffleQueueIndices(multiselect_context.getSelectedItems().map { it.second!! }) - } - else { - shuffleQueue(start = current_song_index + 1) - } - } - }, - onLongClick = if (multiselect_context.is_active) null else ({ - if (!multiselect_context.is_active) { - player.controller?.service_player?.undoableAction { - if (current_song_index > 0) { - moveSong(current_song_index, 0) - scrollToitem(0) - } - shuffleQueue(start = 1) - } - player.context.vibrateShort() + PlatformClickableButton( + onClick = { + player.controller?.service_player?.undoableAction { + if (multiselect_context.is_active) { + shuffleQueueIndices(multiselect_context.getSelectedItems().map { it.second!! }) + } + else { + shuffleQueue(start = current_song_index + 1) + } + } + }, + onAltClick = if (multiselect_context.is_active) null else ({ + if (!multiselect_context.is_active) { + player.controller?.service_player?.undoableAction { + if (current_song_index > 0) { + moveSong(current_song_index, 0) + scrollToitem(0) } - }) - ), - color = button_colour, - shape = FilledButtonTokens.ContainerShape.toShape(), + shuffleQueue(start = 1) + } + player.context.vibrateShort() + } + }), + modifier = button_modifier, + colors = ButtonDefaults.buttonColors( + containerColor = button_colour, + contentColor = button_colour.getContrasted() + ), border = multiselect_context.getActiveHintBorder() ) { - Box( - Modifier - .defaultMinSize( - minWidth = ButtonDefaults.MinWidth, - minHeight = ButtonDefaults.MinHeight - ) - .padding(ButtonDefaults.ContentPadding), - contentAlignment = Alignment.Center - ) { - Text( - text = getString("queue_shuffle"), - style = MaterialTheme.typography.labelLarge - ) - } + Text(text = getString("queue_shuffle")) } } - val undo_background = animateColorAsState( + val undo_background: Color = animateColorAsState( if (player.status.m_undo_count != 0) LocalContentColor.current else LocalContentColor.current.copy(alpha = 0.3f) ).value Box( - modifier = Modifier - .bounceOnClick() + modifier = button_modifier .background( undo_background, CircleShape diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTab.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTab.kt index 4317ea147..873817331 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTab.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTab.kt @@ -85,7 +85,8 @@ internal fun QueueTab( wave_border_mode_override: NowPlayingQueueWaveBorderMode? = null, button_row_arrangement: Arrangement.Horizontal = Arrangement.SpaceEvenly, getBackgroundColour: PlayerState.() -> Color = { getNPAltOnBackground() }, - getOnBackgroundColour: PlayerState.() -> Color = { getNPBackground() } + getOnBackgroundColour: PlayerState.() -> Color = { getNPBackground() }, + getWaveBorderColour: PlayerState.() -> Color = getOnBackgroundColour ) { val player = LocalPlayerState.current val density = LocalDensity.current @@ -242,7 +243,7 @@ internal fun QueueTab( queue_list_state, border_thickness, getBackgroundColour = getBackgroundColour, - getBorderColour = getOnBackgroundColour + getBorderColour = getWaveBorderColour ) CompositionLocalProvider( diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTabItem.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTabItem.kt index 2021cfd4f..7e8cd26b0 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTabItem.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/QueueTabItem.kt @@ -16,17 +16,22 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.toasterofbread.composekit.utils.common.getContrasted import com.toasterofbread.composekit.utils.modifier.background import com.toasterofbread.spmp.model.mediaitem.song.Song import com.toasterofbread.spmp.model.settings.category.PlayerSettings import com.toasterofbread.spmp.platform.getUiLanguage +import com.toasterofbread.spmp.platform.isLargeFormFactor import com.toasterofbread.spmp.resources.getString import com.toasterofbread.spmp.resources.uilocalisation.durationToString import com.toasterofbread.spmp.ui.component.mediaitempreview.MediaItemPreviewLong import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext +import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState +import com.toasterofbread.spmp.ui.theme.appHover import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder import kotlin.math.roundToInt @@ -102,10 +107,10 @@ class QueueTabItem(val song: Song, val key: Int) { multiselect_context: MediaItemMultiSelectContext, requestRemove: () -> Unit ) { - val player = LocalPlayerState.current - val swipe_state = queueElementSwipeState(requestRemove) - val max_offset = with(LocalDensity.current) { player.screen_size.width.toPx() } - val anchors = mapOf(-max_offset to 0, 0f to 1, max_offset to 2) + val player: PlayerState = LocalPlayerState.current + val swipe_state: SwipeableState = queueElementSwipeState(requestRemove) + val max_offset: Float = with(LocalDensity.current) { player.screen_size.width.toPx() } + val anchors: Map = mapOf(-max_offset to 0, 0f to 1, max_offset to 2) TouchSlopScope({ touchSlop * 2f * (2.1f - PlayerSettings.Key.QUEUE_ITEM_SWIPE_SENSITIVITY.get()) @@ -113,19 +118,18 @@ class QueueTabItem(val song: Song, val key: Int) { Box( Modifier .offset { IntOffset(swipe_state.offset.value.roundToInt(), 0) } - .background(RoundedCornerShape(45), getBackgroundColour) + .background(MaterialTheme.shapes.extraLarge, getBackgroundColour) ) { - val padding = 7.dp Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = padding, end = 10.dp) + modifier = Modifier.padding(start = 10.dp, end = 10.dp) ) { MediaItemPreviewLong( song, Modifier .weight(1f) - .padding(vertical = padding) + .padding(top = 5.dp, bottom = 5.dp) .swipeable( swipe_state, anchors, @@ -151,7 +155,8 @@ class QueueTabItem(val song: Song, val key: Int) { null, Modifier .detectReorder(list_state) - .requiredSize(25.dp), + .requiredSize(25.dp) + .appHover(true), tint = getBackgroundColour().getContrasted() ) } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/theme/ApplicationTheme.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/theme/ApplicationTheme.kt index 35d6c999e..bb8e5ee85 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/theme/ApplicationTheme.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/theme/ApplicationTheme.kt @@ -1,6 +1,11 @@ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package com.toasterofbread.spmp.ui.theme import PlatformTheme +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ColorScheme @@ -8,8 +13,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Shapes import androidx.compose.material3.Typography import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.toasterofbread.composekit.platform.Platform @@ -18,6 +29,7 @@ import com.toasterofbread.composekit.utils.common.amplifyPercent import com.toasterofbread.composekit.utils.common.blendWith import com.toasterofbread.composekit.utils.common.contrastAgainst import com.toasterofbread.composekit.utils.common.getContrasted +import com.toasterofbread.composekit.utils.common.thenIf import com.toasterofbread.spmp.platform.AppContext @Composable @@ -111,3 +123,18 @@ fun Theme.ApplicationTheme( PlatformTheme(this, content) } } + +fun Modifier.appHover(button: Boolean = false): Modifier = composed { + val interaction_source: MutableInteractionSource = remember { MutableInteractionSource() } + val hovered: Boolean by interaction_source.collectIsHoveredAsState() + + val hover_scale = if (button) 0.95f else 0.97f + val scale by animateFloatAsState(if (hovered) hover_scale else 1f) + + return@composed this + .hoverable(interaction_source) + .scale(scale) + .thenIf(button) { + pointerHoverIcon(PointerIcon.Hand) + } +} diff --git a/shared/src/commonMain/resources/assets/values-ja-JP/strings.xml b/shared/src/commonMain/resources/assets/values-ja-JP/strings.xml index 542429e95..b19620701 100644 --- a/shared/src/commonMain/resources/assets/values-ja-JP/strings.xml +++ b/shared/src/commonMain/resources/assets/values-ja-JP/strings.xml @@ -525,12 +525,15 @@ 開始コマンド プログラム開始時に一回実行されるコマンド + サーバー SpMpのサーバー部分(SpMs)が設定されたIPアドレスとポートで実行されていなければなりません。https://github.com/toasterofbread/spmp-server サーバーのIPアドレス サーバーのポート - サーバー実行のコマンド - SpMp内からサーバーを手動で実行するのに使われるコマンド 「$@」の最初の出現はサーバーへのアーギュメントに置き換えられ、「$@」がなければコマンドに直接追加されます + サーバー実行のコマンド + SpMp内からサーバーを手動で実行するのに使われるコマンド 「$@」の最初の出現はサーバーへのアーギュメントに置き換えられ、「$@」がなければコマンドに直接追加されます + 自動的にサーバーを実行する + アプリ開始時にサーバーが見つからなければ、設定されたコマンドで自動的にサーバーを実行します 終了時に子プロセスのサーバーを停止 開発 diff --git a/shared/src/commonMain/resources/assets/values/strings.xml b/shared/src/commonMain/resources/assets/values/strings.xml index eb5e0e66b..4be29b734 100644 --- a/shared/src/commonMain/resources/assets/values/strings.xml +++ b/shared/src/commonMain/resources/assets/values/strings.xml @@ -539,12 +539,15 @@ Startup command Command to be executed at program start + Server The SpMp server component (SpMs) must be running at the specified port IP address and port. https://github.com/toasterofbread/spmp-server Server IP address Server port - Server command - Command to be run when manually starting the server from within SpMp The first occurence of '$@' will be replaced with the server arguments, otherwise they will be appended to the command + Server command + Command to be run when manually starting the server from within SpMp The first occurence of '$@' will be replaced with the server arguments, otherwise they will be appended to the command + Start local server automatically + On startup, if a server is not found immediately, a server will be started automatically using the specified command Stop child server on exit Development diff --git a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/splash/ExtraLoadingContent.desktop.kt b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/splash/ExtraLoadingContent.desktop.kt index f75ca3b8a..027d6c7e0 100644 --- a/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/splash/ExtraLoadingContent.desktop.kt +++ b/shared/src/desktopMain/kotlin/com/toasterofbread/spmp/platform/splash/ExtraLoadingContent.desktop.kt @@ -3,6 +3,7 @@ package com.toasterofbread.spmp.platform.splash import LocalPlayerState import SpMp import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -33,49 +34,15 @@ import com.toasterofbread.spmp.model.settings.category.DesktopSettings import com.toasterofbread.spmp.resources.getString import com.toasterofbread.spmp.ui.component.ErrorInfoDisplay import com.toasterofbread.spmp.ui.layout.apppage.mainpage.PlayerState +import com.toasterofbread.spmp.ui.layout.apppage.settingspage.category.getServerGroupItems import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@OptIn(DelicateCoroutinesApi::class) -@Suppress("NewApi") -private fun startLocalServer(port: Int, onExit: (Int) -> Unit): Pair? { - var command: String = DesktopSettings.Key.SERVER_LOCAL_COMMAND.get().trim() - if (command.isEmpty()) { - return null - } - - val args: String = "--port $port" - - val args_index: Int = command.indexOf("\$@") - if (args_index != -1) { - command = command.substring(0, args_index) + args + command.substring(args_index + 2) - } - else { - command += ' ' + args - } - - val builder: ProcessBuilder = ProcessBuilder(command.split(' ')) - builder.inheritIO() - - val process: Process = builder.start() - - Runtime.getRuntime().addShutdownHook(Thread { - if (DesktopSettings.Key.SERVER_KILL_CHILD_ON_EXIT.get()) { - process.destroy() - } - }) - - GlobalScope.launch { - withContext(Dispatchers.IO) { - onExit(process.waitFor()) - } - } - - return Pair(command, process) -} +private const val LOCAL_SERVER_AUTOSTART_DELAY_MS: Long = 100 @Composable actual fun SplashExtraLoadingContent(modifier: Modifier) { @@ -85,33 +52,51 @@ actual fun SplashExtraLoadingContent(modifier: Modifier) { contentColor = player.theme.on_accent ) + var show: Boolean by remember { mutableStateOf(false) } var show_config_dialog: Boolean by remember { mutableStateOf(false) } var local_server_error: Throwable? by remember { mutableStateOf(null) } var local_server_process: Pair? by remember { mutableStateOf(null) } - Column(horizontalAlignment = Alignment.CenterHorizontally) { + fun startServer(stop_if_running: Boolean) { + local_server_process?.also { process -> + if (stop_if_running) { + local_server_process = null + process.second.destroy() + } + return + } + + try { + local_server_process = startLocalServer(DesktopSettings.Key.SERVER_PORT.get()) { + if (local_server_process != null) { + local_server_process = null + local_server_error = RuntimeException(it.toString()) + } + } + } + catch (e: Throwable) { + local_server_process = null + local_server_error = e + } + } + + LaunchedEffect(Unit) { + if (DesktopSettings.Key.SERVER_LOCAL_START_AUTOMATICALLY.get()) { + delay(LOCAL_SERVER_AUTOSTART_DELAY_MS) + startServer(stop_if_running = false) + delay(500) + } + show = true + } + + if (!show) { + return + } + + Column(modifier.animateContentSize().fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Button( - { - local_server_process?.also { process -> - local_server_process = null - process.second.destroy() - return@Button - } - - try { - local_server_process = startLocalServer(DesktopSettings.Key.SERVER_PORT.get()) { - if (local_server_process != null) { - local_server_process = null - local_server_error = RuntimeException(it.toString()) - } - } - } - catch (e: Throwable) { - local_server_process = null - local_server_error = e - } - }, + { startServer(stop_if_running = true) }, colors = button_colours ) { Crossfade(local_server_process) { process -> @@ -145,33 +130,33 @@ actual fun SplashExtraLoadingContent(modifier: Modifier) { } } } - } - Crossfade(local_server_error ?: local_server_process as Any?) { state -> - if (state != null) { - Column( - Modifier.padding(top = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - if (state is Throwable) { - Text(getString("error_on_server_command_execution")) - ErrorInfoDisplay( - state, - show_throw_button = false, - onDismiss = { local_server_error = null }, - modifier = Modifier.fillMaxWidth() - ) - } - else if (state is Pair<*, *>) { - Text(getString("desktop_splash_process_running_with_command_\$x").replace("\$x", state.first as String)) + Crossfade(local_server_error ?: local_server_process as Any?) { state -> + if (state != null) { + Column( + Modifier.padding(top = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (state is Throwable) { + Text(getString("error_on_server_command_execution")) + ErrorInfoDisplay( + state, + show_throw_button = false, + onDismiss = { local_server_error = null }, + modifier = Modifier.fillMaxWidth() + ) + } + else if (state is Pair<*, *>) { + Text(getString("desktop_splash_process_running_with_command_\$x").replace("\$x", state.first as String)) + } } } } } if (show_config_dialog) { - val settings_items: List = remember { DesktopSettings.getPage()?.getItems(player.context) ?: emptyList() } + val settings_items: List = remember { getServerGroupItems() } LaunchedEffect(settings_items) { for (item in settings_items) { @@ -220,3 +205,41 @@ actual fun SplashExtraLoadingContent(modifier: Modifier) { ) } } + +@OptIn(DelicateCoroutinesApi::class) +@Suppress("NewApi") +private fun startLocalServer(port: Int, onExit: (Int) -> Unit): Pair? { + var command: String = DesktopSettings.Key.SERVER_LOCAL_COMMAND.get().trim() + if (command.isEmpty()) { + return null + } + + val args: String = "--port $port" + + val args_index: Int = command.indexOf("\$@") + if (args_index != -1) { + command = command.substring(0, args_index) + args + command.substring(args_index + 2) + } + else { + command += ' ' + args + } + + val builder: ProcessBuilder = ProcessBuilder(command.split(' ')) + builder.inheritIO() + + val process: Process = builder.start() + + Runtime.getRuntime().addShutdownHook(Thread { + if (DesktopSettings.Key.SERVER_KILL_CHILD_ON_EXIT.get()) { + process.destroy() + } + }) + + GlobalScope.launch { + withContext(Dispatchers.IO) { + onExit(process.waitFor()) + } + } + + return Pair(command, process) +}