diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/settings/model/SettingsItem.kt b/shared/src/commonMain/kotlin/com/toasterofbread/settings/model/SettingsItem.kt index 1e3308585..55b567188 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/settings/model/SettingsItem.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/settings/model/SettingsItem.kt @@ -66,22 +66,26 @@ abstract class SettingsItem { ) @Composable - protected fun ItemTitleText(text: String?, theme: Theme) { + protected fun ItemTitleText(text: String?, theme: Theme, modifier: Modifier = Modifier) { if (text?.isNotBlank() == true) { - WidthShrinkText(text, style = LocalTextStyle.current.copy(color = theme.on_background)) + WidthShrinkText( + text, + modifier, + style = MaterialTheme.typography.titleMedium.copy(color = theme.on_background) + ) } } @Composable - protected fun ItemText(text: String?, colour: Color, font_size: TextUnit = TextUnit.Unspecified) { + protected fun ItemText(text: String?, colour: Color) { if (text?.isNotBlank() == true) { - Text(text, color = colour, fontSize = font_size) + Text(text, color = colour, style = MaterialTheme.typography.bodySmall) } } @Composable - protected fun ItemText(text: String?, theme: Theme, font_size: TextUnit = TextUnit.Unspecified) { - ItemText(text, theme.on_background.setAlpha(0.75f), font_size) + protected fun ItemText(text: String?, theme: Theme) { + ItemText(text, theme.on_background.setAlpha(0.75f)) } } @@ -443,7 +447,7 @@ class SettingsItemSlider( } Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { if (min_label != null) { - ItemText(min_label, theme, 12.sp) + ItemText(min_label, theme) } SliderValueHorizontal( value = getValue(), @@ -513,7 +517,7 @@ class SettingsItemSlider( valueRange = range ) if (max_label != null) { - ItemText(max_label, theme, 12.sp) + ItemText(max_label, theme) } } } @@ -545,8 +549,8 @@ class SettingsItemMultipleChoice( ) { Column { Column(Modifier.fillMaxWidth()) { - ItemTitleText(title, theme) - ItemText(subtitle, theme, 15.sp) + ItemTitleText(title, theme, Modifier.padding(bottom = 7.dp)) + ItemText(subtitle, theme) Spacer(Modifier.height(10.dp)) @@ -742,7 +746,7 @@ class SettingsItemLargeToggle( openPage: (Int) -> Unit, openCustomPage: (SettingsPage) -> Unit ) { - val shape = RoundedCornerShape(20.dp) + val shape = RoundedCornerShape(25.dp) var loading: Boolean by remember { mutableStateOf(false) } var showing_dialog: (@Composable (dismiss: () -> Unit, openPage: (Int) -> Unit) -> Unit)? by remember { mutableStateOf(null) } @@ -761,7 +765,7 @@ class SettingsItemLargeToggle( shape ) .border(2.dp, theme.vibrant_accent, shape) - .padding(horizontal = 10.dp) + .padding(horizontal = 10.dp, vertical = 5.dp) .fillMaxWidth() .height(IntrinsicSize.Max), horizontalArrangement = Arrangement.spacedBy(3.dp), verticalAlignment = Alignment.CenterVertically diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsInterface.kt b/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsInterface.kt index 5301b03a9..8a25080dd 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsInterface.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsInterface.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.toasterofbread.spmp.platform.PlatformContext import com.toasterofbread.spmp.platform.ProjectPreferences import com.toasterofbread.spmp.ui.component.PillMenu @@ -85,7 +86,12 @@ class SettingsInterface( } } - page.TitleBar(page.id == root_page, Modifier.requiredHeight(30.dp)) { go_back = true } + page.TitleBar( + page.id == root_page, + Modifier.zIndex(10f) + ) { + go_back = true + } Box( contentAlignment = Alignment.TopCenter diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsPage.kt b/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsPage.kt index 7ade7faf1..70df42837 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsPage.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/SettingsPage.kt @@ -1,25 +1,45 @@ package com.toasterofbread.composesettings.ui import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.toasterofbread.settings.model.SettingsGroup import com.toasterofbread.settings.model.SettingsItem import com.toasterofbread.spmp.platform.composable.BackHandler +import com.toasterofbread.spmp.ui.component.WaveBorder +import com.toasterofbread.spmp.ui.theme.Theme import com.toasterofbread.utils.composable.WidthShrinkText +import kotlin.math.ceil -abstract class SettingsPage( - private val getTitle: (() -> String?)? = null, - private val getIcon: (@Composable () -> ImageVector?)? = null -) { +abstract class SettingsPage { var id: Int? = null internal set internal lateinit var settings_interface: SettingsInterface @@ -27,6 +47,11 @@ abstract class SettingsPage( open val disable_padding: Boolean = false open val scrolling: Boolean = true + open val title: String? = null + open val icon: ImageVector? + @Composable + get() = null + @Composable fun Page(content_padding: PaddingValues, openPage: (Int) -> Unit, openCustomPage: (SettingsPage) -> Unit, goBack: () -> Unit) { PageView(content_padding, openPage, openCustomPage, goBack) @@ -37,27 +62,33 @@ abstract class SettingsPage( @Composable fun TitleBar(is_root: Boolean, modifier: Modifier = Modifier, goBack: () -> Unit) { - Crossfade(getTitle?.invoke()) { title -> - Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - val icon = getIcon?.invoke() - if (icon != null) { - Icon(icon, null) - } + Crossfade(title, modifier) { title -> + Column(Modifier.fillMaxWidth()) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + icon?.also { + Icon(it, null) + } - if (title != null) { - WidthShrinkText( - title, - Modifier.padding(horizontal = 30.dp), - style = MaterialTheme.typography.headlineMedium.copy( - color = settings_interface.theme.on_background, - fontWeight = FontWeight.Light + if (title != null) { + WidthShrinkText( + title, + Modifier.padding(horizontal = 30.dp), + style = MaterialTheme.typography.headlineMedium.copy( + color = settings_interface.theme.on_background, + fontWeight = FontWeight.Light, + textAlign = TextAlign.Center + ) ) - ) - } + } - if (icon != null) { - Spacer(Modifier.width(24.dp)) + if (icon != null) { + Spacer(Modifier.width(24.dp)) + } } + + WaveBorder( + Modifier.requiredWidth(SpMp.context.getScreenWidth()) + ) } } } @@ -72,11 +103,17 @@ abstract class SettingsPage( private const val SETTINGS_PAGE_WITH_ITEMS_SPACING = 20f class SettingsPageWithItems( - getTitle: () -> String?, + val getTitle: () -> String?, val getItems: () -> List, val modifier: Modifier = Modifier, - getIcon: (@Composable () -> ImageVector?)? = null -): SettingsPage(getTitle, getIcon) { + val getIcon: (@Composable () -> ImageVector?)? = null +): SettingsPage() { + + override val title: String? + get() = getTitle() + override val icon: ImageVector? + @Composable + get() = getIcon?.invoke() @Composable override fun PageView( diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/ThemeEditor.kt b/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/ThemeEditor.kt index 45a625dea..5b5326663 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/ThemeEditor.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/settings/ui/ThemeEditor.kt @@ -75,10 +75,13 @@ class SettingsItemThemeSelector( ) { Column { Row(verticalAlignment = Alignment.CenterVertically) { - ItemTitleText(title, theme) - Spacer(Modifier - .fillMaxWidth() - .weight(1f)) + ItemTitleText( + title, + theme, + Modifier + .fillMaxWidth() + .weight(1f) + ) Text("${state.value + 1} / ${getThemeCount()}") @@ -116,15 +119,15 @@ class SettingsItemThemeSelector( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - Box(Modifier - .border(2.dp, theme_data.accent, CircleShape) - .fillMaxHeight() - .weight(1f) - .padding(start = 15.dp), - contentAlignment = Alignment.CenterStart - ) { - WidthShrinkText(theme_data.name) - } + WidthShrinkText( + theme_data.name, + Modifier + .border(2.dp, theme_data.accent, CircleShape) + .fillMaxHeight() + .weight(1f) + .padding(start = 15.dp), + alignment = Alignment.CenterStart + ) IconButton( { @@ -160,7 +163,9 @@ private fun getEditPage( theme: ThemeData, onEditCompleted: (theme_data: ThemeData) -> Unit ): SettingsPage { - return object : SettingsPage({ editor_title }) { + return object : SettingsPage() { + override val title: String? = editor_title + private var reset by mutableStateOf(false) private var close by mutableStateOf(false) diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/PlayerService.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/PlayerService.kt index ac92affbb..456c37bfc 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/PlayerService.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/PlayerService.kt @@ -502,7 +502,9 @@ class PlayerService : MediaPlayerService() { writer.newLine() } - SpMp.Log.info("savePersistentQueue saved $song_count songs") + if (song_count > 0) { + SpMp.Log.info("savePersistentQueue saved $song_count songs") + } writer.close() diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/Settings.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/Settings.kt index 74150aef8..c1a3a5724 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/Settings.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/model/Settings.kt @@ -45,6 +45,9 @@ enum class MusicTopBarMode { val default: MusicTopBarMode get() = LYRICS } } +enum class NowPlayingQueueWaveBorderMode { + TIME, TIME_SYNC, SCROLL, NONE, LINE +} enum class Settings { // Language @@ -118,6 +121,7 @@ enum class Settings { // Now playing queue KEY_NP_QUEUE_RADIO_INFO_POSITION, // TODO prefs item + KEY_NP_QUEUE_WAVE_BORDER_MODE, // Server KEY_SPMS_PORT, @@ -280,6 +284,7 @@ enum class Settings { KEY_FEED_SHOW_CHARTS_ROW -> true KEY_NP_QUEUE_RADIO_INFO_POSITION -> NowPlayingQueueRadioInfoPosition.TOP_BAR.ordinal + KEY_NP_QUEUE_WAVE_BORDER_MODE -> NowPlayingQueueWaveBorderMode.TIME.ordinal KEY_YTM_AUTH -> { with(ProjectBuildConfig) { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MediaItemLayout.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MediaItemLayout.kt index 2c283bba8..33e5ccd32 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MediaItemLayout.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MediaItemLayout.kt @@ -332,7 +332,7 @@ private fun TitleBar( @Composable fun LazyMediaItemLayoutColumn( - layoutsProvider: () -> List, + getLayouts: () -> List, modifier: Modifier = Modifier, layout_modifier: Modifier = Modifier, padding: PaddingValues = PaddingValues(0.dp), @@ -349,7 +349,7 @@ fun LazyMediaItemLayoutColumn( multiselect_context: MediaItemMultiSelectContext? = null, getType: ((MediaItemLayout) -> MediaItemLayout.Type)? = null ) { - val layouts = layoutsProvider() + val layouts = getLayouts() require(getType != null || layouts.all { it.type != null }) LazyColumn( diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MusicTopBar.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MusicTopBar.kt index 2e5735ecf..16b418564 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MusicTopBar.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/MusicTopBar.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import com.toasterofbread.spmp.model.MusicTopBarMode @@ -20,7 +21,6 @@ import com.toasterofbread.spmp.model.mediaitem.Song import com.toasterofbread.spmp.model.SongLyrics import com.toasterofbread.spmp.platform.composable.platformClickable import com.toasterofbread.spmp.resources.getString -import com.toasterofbread.spmp.ui.layout.nowplaying.LocalNowPlayingExpansion import com.toasterofbread.spmp.ui.layout.nowplaying.overlay.OverlayMenu import com.toasterofbread.utils.composable.rememberSongUpdateLyrics import com.toasterofbread.utils.getContrasted @@ -117,6 +117,7 @@ fun MusicTopBar( can_show_key: Settings, modifier: Modifier = Modifier, padding: PaddingValues = PaddingValues(), + bottom_border_colour: Color? = null, onShowingChanged: ((Boolean) -> Unit)? = null ) { val can_show: Boolean by can_show_key.rememberMutableState() @@ -127,6 +128,7 @@ fun MusicTopBar( hide_while_inactive = true, modifier = modifier, padding = padding, + bottom_border_colour = bottom_border_colour, onShowingChanged = onShowingChanged ) } @@ -141,6 +143,7 @@ private fun MusicTopBar( song: Song? = LocalPlayerState.current.status.m_song, padding: PaddingValues = PaddingValues(), innerContent: (@Composable (MusicTopBarMode) -> Unit)? = null, + bottom_border_colour: Color? = null, onClick: (() -> Unit)? = null, onShowingChanged: ((Boolean) -> Unit)? = null ) { @@ -187,7 +190,7 @@ private fun MusicTopBar( enter = expandVertically(), exit = shrinkVertically() ) { - Box(Modifier.padding(padding).height(30.dp), contentAlignment = Alignment.Center) { + Column(Modifier.padding(padding).height(30.dp)) { innerContent?.invoke(mode_state) Crossfade(current_state, Modifier.fillMaxSize()) { s -> @@ -220,6 +223,12 @@ private fun MusicTopBar( } } } + + Crossfade(bottom_border_colour) { bottom_border_colour -> + if (bottom_border_colour != null) { + WaveBorder(Modifier.fillMaxWidth(), colour = bottom_border_colour) + } + } } } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/WaveBorder.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/WaveBorder.kt new file mode 100644 index 000000000..94b39cbb0 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/component/WaveBorder.kt @@ -0,0 +1,102 @@ +package com.toasterofbread.spmp.ui.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.DrawStyle +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.toasterofbread.spmp.ui.theme.Theme +import kotlin.math.ceil + +@Composable +fun WaveBorder( + modifier: Modifier = Modifier, + colour: Color = Theme.current.background, + height: Dp = 20.dp, + waves: Int = 3, + getOffset: (DrawScope.() -> Float)? = null, + border_thickness: Dp = 0.dp, + border_colour: Color = LocalContentColor.current +) { + Canvas(modifier.fillMaxWidth().offset(y = height * 0)) { + val path = Path() + + // Above equilibrium (cut out from rect) + wavePath(path, -1, getOffset, height, waves) + clipPath( + path, + ClipOp.Difference + ) { + drawRect( + colour, + topLeft = Offset(0f, height.toPx() / 2) + ) + } + + val border_stroke = if (border_thickness > 0.dp) Stroke(border_thickness.toPx()) else null + + // Upper border + if (border_stroke != null) { + drawPath(path, border_colour, style = border_stroke) + } + + // Below equilibrium + wavePath(path, 1, getOffset, height, waves) + drawPath(path, colour) + + // Lower border + if (border_stroke != null) { + drawPath(path, border_colour, style = border_stroke) + } + } +} + +private fun DrawScope.wavePath( + path: Path, + direction: Int, + getOffset: (DrawScope.() -> Float)?, + height: Dp, + waves: Int, +): Path { + path.reset() + + val y_offset = height.toPx() / 2 + val half_period = (size.width / (waves - 1)) / 2 + val offset_px = getOffset?.invoke(this)?.let { offset -> + offset % size.width - (if (offset > 0f) size.width else 0f) + } ?: 0f + + path.moveTo(x = -half_period / 2 + offset_px, y = y_offset) + + for (i in 0 until ceil((size.width * 2) / half_period + 1).toInt()) { + if ((i % 2 == 0) != (direction == 1)) { + path.relativeMoveTo(half_period, 0f) + continue + } + + path.relativeQuadraticBezierTo( + dx1 = half_period / 2, + dy1 = height.toPx() / 2 * direction, + dx2 = half_period, + dy2 = 0f, + ) + } + +// path.moveTo(size.width, y_offset) +// path.moveTo(0f, y_offset) + + return path +} diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/DiscordManualLogin.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/DiscordManualLogin.kt index aad2518e8..d9ec8c9ae 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/DiscordManualLogin.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/DiscordManualLogin.kt @@ -13,7 +13,6 @@ fun DiscordManualLogin(modifier: Modifier = Modifier, onFinished: (Result, suffix: String, entry_label: String, @@ -57,19 +56,15 @@ fun ManualLoginPage( Column( modifier .padding( - horizontal = SpMp.context.getDefaultHorizontalPadding(), vertical = SpMp.context.getDefaultVerticalPadding() ) - .padding(bottom = player.nowPlayingBottomPadding(true)) + .padding( + top = 40.dp, + bottom = player.nowPlayingBottomPadding(true) + ) .fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(30.dp) ) { - Text( - title, - Modifier.align(Alignment.CenterHorizontally).padding(bottom = 25.dp), - style = MaterialTheme.typography.headlineLarge - ) - if (desktop_browser_needed) { Text( getString("manual_login_desktop_browser_may_be_needed"), @@ -82,7 +77,7 @@ fun ManualLoginPage( fun step(text: String, index: Int, modifier: Modifier = Modifier, shrink: Boolean = false) { Row(modifier.alpha(0.85f), horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.Bottom) { Text( - index.toString(), + (index + 1).toString(), style = MaterialTheme.typography.bodySmall ) diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicLogin.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicLogin.kt index d171d1154..e33dc64a3 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicLogin.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicLogin.kt @@ -55,44 +55,55 @@ fun YoutubeMusicLogin(modifier: Modifier = Modifier, manual: Boolean = false, on YoutubeMusicManualLogin(modifier, onFinished) } else if (isWebViewLoginSupported()) { + var finished: Boolean by remember { mutableStateOf(false) } + val lock = remember { Object() } + WebViewLogin(MUSIC_URL, modifier, shouldShowPage = { !it.startsWith(MUSIC_URL) }) { request, openUrl, getCookie -> - val url = URI(request.url) - if (url.host == "music.youtube.com" && url.path?.startsWith("/youtubei/v1/") == true) { - if (!request.requestHeaders.containsKey("Authorization")) { - openUrl(MUSIC_LOGIN_URL) + synchronized(lock) { + if (finished) { return@WebViewLogin } - val cookie = getCookie(MUSIC_URL) - val account_request = Request.Builder() - .url("https://music.youtube.com/youtubei/v1/account/account_menu") - .addHeader("cookie", cookie) - .apply { - for (header in request.requestHeaders) { - addHeader(header.key, header.value) - } + val url = URI(request.url) + if (url.host == "music.youtube.com" && url.path?.startsWith("/youtubei/v1/") == true) { + if (!request.requestHeaders.containsKey("Authorization")) { + openUrl(MUSIC_LOGIN_URL) + return@WebViewLogin } - .post(Api.getYoutubeiRequestBody(null)) - .build() - val result = Api.request(account_request) - result.fold( - { response -> - val parsed: YTAccountMenuResponse = Api.klaxon.parse(response.body!!.charStream())!! - response.close() + finished = true - onFinished(Result.success( - YoutubeMusicAuthInfo( - parsed.getAritst()!!, - cookie, - request.requestHeaders - ) - )) - }, - { - onFinished(result.cast()) - } - ) + val cookie = getCookie(MUSIC_URL) + val account_request = Request.Builder() + .url("https://music.youtube.com/youtubei/v1/account/account_menu") + .addHeader("cookie", cookie) + .apply { + for (header in request.requestHeaders) { + addHeader(header.key, header.value) + } + } + .post(Api.getYoutubeiRequestBody(null)) + .build() + + val result = Api.request(account_request) + result.fold( + { response -> + val parsed: YTAccountMenuResponse = Api.klaxon.parse(response.body!!.charStream())!! + response.close() + + onFinished(Result.success( + YoutubeMusicAuthInfo( + parsed.getAritst()!!, + cookie, + request.requestHeaders + ) + )) + }, + { + onFinished(result.cast()) + } + ) + } } } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicManualLogin.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicManualLogin.kt index 7b661fc04..491bbecca 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicManualLogin.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/YoutubeMusicManualLogin.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.launch fun YoutubeMusicManualLogin(modifier: Modifier = Modifier, onFinished: (Result?) -> Unit) { val coroutine_scope = rememberCoroutineScope() ManualLoginPage( - title = getString("youtube_manual_login_title"), steps = getStringArray("youtube_manual_login_steps"), suffix = getString("youtube_manual_login_suffix"), entry_label = getString("youtube_manual_login_field"), diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPage.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPage.kt index 4d89199c5..9ad92929d 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPage.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPage.kt @@ -2,20 +2,34 @@ package com.toasterofbread.spmp.ui.layout.mainpage import LocalPlayerState import SpMp +import androidx.compose.animation.Crossfade import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.toasterofbread.spmp.api.Api import com.toasterofbread.spmp.model.mediaitem.Artist import com.toasterofbread.spmp.model.mediaitem.MediaItemHolder @@ -25,11 +39,12 @@ import com.toasterofbread.spmp.ui.component.LazyMediaItemLayoutColumn import com.toasterofbread.spmp.ui.component.MediaItemLayout import com.toasterofbread.spmp.ui.component.PillMenu import com.toasterofbread.spmp.ui.layout.library.LibraryPage +import com.toasterofbread.spmp.ui.theme.Theme @Composable fun MainPage( pinned_items: List, - layoutsProvider: () -> List, + getLayouts: () -> List, scroll_state: LazyListState, feed_load_state: MutableState, can_continue_feed: Boolean, @@ -38,6 +53,7 @@ fun MainPage( pill_menu: PillMenu, loadFeed: (filter_chip: Int?, continuation: Boolean) -> Unit ) { + val player = LocalPlayerState.current val padding by animateDpAsState(SpMp.context.getDefaultHorizontalPadding()) val artists_layout: MediaItemLayout = remember { @@ -53,8 +69,8 @@ fun MainPage( ) } - LaunchedEffect(layoutsProvider()) { - populateArtistsLayout(artists_layout, layoutsProvider, Api.ytm_auth.getOwnChannelOrNull()) + LaunchedEffect(getLayouts()) { + populateArtistsLayout(artists_layout, getLayouts, Api.ytm_auth.getOwnChannelOrNull()) } Column(Modifier.padding(horizontal = padding)) { @@ -64,7 +80,7 @@ fun MainPage( getFilterChips, getSelectedFilterChip, { loadFeed(it, false) }, - Modifier.padding(top = SpMp.context.getStatusBarHeight()) + Modifier.padding(top = SpMp.context.getStatusBarHeight()).zIndex(1f) ) // Main scrolling view @@ -74,7 +90,7 @@ fun MainPage( swipe_enabled = feed_load_state.value == FeedLoadState.NONE, indicator = false ) { - val layouts_empty by remember { derivedStateOf { layoutsProvider().isEmpty() } } + val layouts_empty by remember { derivedStateOf { getLayouts().isEmpty() } } val state = if (feed_load_state.value == FeedLoadState.LOADING || feed_load_state.value == FeedLoadState.PREINIT) null else !layouts_empty var current_state by remember { mutableStateOf(state) } val state_alpha = remember { Animatable(1f) } @@ -98,34 +114,51 @@ fun MainPage( when (current_state) { // Loaded true -> { - LazyMediaItemLayoutColumn( - layoutsProvider, - layout_modifier = Modifier.graphicsLayer { alpha = state_alpha.value }, - padding = PaddingValues( + val onContinuationRequested = if (can_continue_feed) { + { loadFeed(getSelectedFilterChip(), true) } + } else null + val loading_continuation = feed_load_state.value != FeedLoadState.NONE + + LazyColumn( + Modifier.graphicsLayer { alpha = state_alpha.value }, + state = scroll_state, + contentPadding = PaddingValues( bottom = LocalPlayerState.current.bottom_padding ), - onContinuationRequested = if (can_continue_feed) { - { loadFeed(getSelectedFilterChip(), true) } - } else null, - continuation_alignment = Alignment.Start, - loading_continuation = feed_load_state.value != FeedLoadState.NONE, - scroll_state = scroll_state, - scroll_enabled = !state_alpha.isRunning, - spacing = 30.dp, - topContent = { - item { + userScrollEnabled = !state_alpha.isRunning, + verticalArrangement = Arrangement.spacedBy(30.dp) + ) { + item { + Column { TopContent() + artists_layout.Layout(multiselect_context = player.main_multiselect_context) } - }, - layoutItem = { layout, i, showLayout -> - if (i == 0 && artists_layout.items.isNotEmpty()) { - artists_layout.also { showLayout(this, it) } + } + + itemsIndexed(getLayouts()) { index, layout -> + if (layout.items.isEmpty()) { + return@itemsIndexed } - showLayout(this, layout) - }, - multiselect_context = LocalPlayerState.current.main_multiselect_context - ) { it.type ?: MediaItemLayout.Type.GRID } + val type = layout.type ?: MediaItemLayout.Type.GRID + type.Layout(layout, multiselect_context = player.main_multiselect_context) + } + + item { + Crossfade(Pair(onContinuationRequested, loading_continuation)) { data -> + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) { + if (data.second) { + CircularProgressIndicator(color = Theme.current.on_background) + } + else if (data.first != null) { + IconButton({ data.first!!.invoke() }) { + Icon(Icons.Filled.KeyboardDoubleArrowDown, null, tint = Theme.current.on_background) + } + } + } + } + } + } } // Offline false -> { diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPageTopBar.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPageTopBar.kt index f23295f53..195e75960 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPageTopBar.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/MainPageTopBar.kt @@ -4,6 +4,7 @@ import LocalPlayerState import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CircleShape @@ -13,11 +14,14 @@ import androidx.compose.material3.* import androidx.compose.runtime.* 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.zIndex import com.toasterofbread.spmp.model.* import com.toasterofbread.spmp.model.mediaitem.MediaItemThumbnailProvider import com.toasterofbread.spmp.resources.uilocalisation.LocalisedYoutubeString import com.toasterofbread.spmp.ui.component.MusicTopBarWithVisualiser +import com.toasterofbread.spmp.ui.component.WaveBorder import com.toasterofbread.spmp.ui.layout.RadioBuilderIcon import com.toasterofbread.spmp.ui.layout.YoutubeMusicLoginConfirmation import com.toasterofbread.spmp.ui.theme.Theme @@ -32,7 +36,7 @@ fun MainPageTopBar( ) { val player = LocalPlayerState.current - Column(modifier.animateContentSize()) { + Column(modifier) { Row(Modifier.height(IntrinsicSize.Min)) { RadioBuilderButton() @@ -82,6 +86,8 @@ fun MainPageTopBar( FilterChipsRow(getFilterChips, getSelectedFilterChip, onFilterChipSelected) } } + + WaveBorder(Modifier.requiredWidth(SpMp.context.getScreenWidth())) } } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/PlayerStateImpl.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/PlayerStateImpl.kt index 723b8019f..ba5d0435a 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/PlayerStateImpl.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/mainpage/PlayerStateImpl.kt @@ -33,6 +33,7 @@ import com.toasterofbread.spmp.ui.layout.nowplaying.NowPlayingExpansionState import com.toasterofbread.spmp.ui.layout.nowplaying.ThemeMode import com.toasterofbread.spmp.ui.layout.nowplaying.overlay.OverlayMenu import com.toasterofbread.spmp.ui.layout.prefspage.PrefsPage +import com.toasterofbread.spmp.ui.layout.prefspage.PrefsPageCategory import com.toasterofbread.utils.addUnique import com.toasterofbread.utils.composable.OnChangedEffect import com.toasterofbread.utils.init @@ -154,9 +155,11 @@ interface PlayerOverlayPage { val SettingsPage = object : PlayerOverlayPage { override fun getItem(): MediaItem? = null + val current_category: MutableState = mutableStateOf(null) + @Composable override fun getPage(pill_menu: PillMenu, previous_item: MediaItemHolder?, bottom_padding: Dp, close: () -> Unit) { - PrefsPage(pill_menu, bottom_padding, Modifier.fillMaxSize(), close) + PrefsPage(pill_menu, bottom_padding, current_category, Modifier.fillMaxSize(), close) } } val LibraryPage = object : PlayerOverlayPage { @@ -317,7 +320,6 @@ class PlayerStateImpl(private val context: PlatformContext): PlayerState(null, n override fun setOverlayPage(page: PlayerOverlayPage?, from_current: Boolean) { val current = if (from_current) overlay_page?.first?.getItem() else null - val new_page = page?.let { Pair(page, current) } if (new_page != overlay_page) { overlay_page_undo_stack.add(overlay_page) diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/NowPlayingQueueTab.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/NowPlayingQueueTab.kt index e8943cfa0..dd9797d3e 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/NowPlayingQueueTab.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/nowplaying/queue/NowPlayingQueueTab.kt @@ -2,44 +2,71 @@ package com.toasterofbread.spmp.ui.layout.nowplaying.queue import LocalPlayerState import SpMp -import androidx.compose.animation.core.* -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Divider +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.* +import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.* +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.toasterofbread.spmp.model.MusicTopBarMode import com.toasterofbread.spmp.model.NowPlayingQueueRadioInfoPosition +import com.toasterofbread.spmp.model.NowPlayingQueueWaveBorderMode import com.toasterofbread.spmp.model.Settings import com.toasterofbread.spmp.model.mediaitem.Song import com.toasterofbread.spmp.platform.MediaPlayerService +import com.toasterofbread.spmp.ui.component.WaveBorder import com.toasterofbread.spmp.ui.component.multiselect.MediaItemMultiSelectContext import com.toasterofbread.spmp.ui.layout.mainpage.MINIMISED_NOW_PLAYING_HEIGHT import com.toasterofbread.spmp.ui.layout.mainpage.MINIMISED_NOW_PLAYING_V_PADDING -import com.toasterofbread.spmp.ui.layout.nowplaying.* +import com.toasterofbread.spmp.ui.layout.nowplaying.LocalNowPlayingExpansion import com.toasterofbread.spmp.ui.layout.nowplaying.NOW_PLAYING_TOP_BAR_HEIGHT import com.toasterofbread.spmp.ui.layout.nowplaying.getNPAltOnBackground import com.toasterofbread.spmp.ui.layout.nowplaying.getNPBackground -import com.toasterofbread.utils.* -import com.toasterofbread.utils.composable.Divider import com.toasterofbread.utils.composable.SubtleLoadingIndicator -import org.burnoutcrew.reorderable.* +import com.toasterofbread.utils.getContrasted +import kotlinx.coroutines.delay +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable @Composable fun QueueTab(page_height: Dp, modifier: Modifier = Modifier) { + val player = LocalPlayerState.current + val density = LocalDensity.current + var key_inc by remember { mutableStateOf(0) } val radio_info_position: NowPlayingQueueRadioInfoPosition = Settings.getEnum(Settings.KEY_NP_QUEUE_RADIO_INFO_POSITION) val multiselect_context: MediaItemMultiSelectContext = remember { MediaItemMultiSelectContext() } - val player = LocalPlayerState.current val song_items: SnapshotStateList = remember { mutableStateListOf().also { list -> player.player?.iterateSongs { _, song: Song -> @@ -157,18 +184,22 @@ fun QueueTab(page_height: Dp, modifier: Modifier = Modifier) { CurrentRadioIndicator(backgroundColourProvider, multiselect_context) } - Divider(Modifier.padding(horizontal = list_padding), 1.dp, backgroundColourProvider) + val wave_border_mode: NowPlayingQueueWaveBorderMode by Settings.KEY_NP_QUEUE_WAVE_BORDER_MODE.rememberMutableEnumState() + QueueBorder(wave_border_mode, list_padding, queue_list_state) CompositionLocalProvider( LocalPlayerState provides remember { player.copy(onClickedOverride = { _, index: Int? -> player.player?.seekToSong(index!!) }) } ) { - val density = LocalDensity.current var list_position by remember { mutableStateOf(0.dp) } LazyColumn( state = queue_list_state.listState, - contentPadding = PaddingValues(top = list_padding), + contentPadding = PaddingValues( + top = list_padding + + // Extra space to prevent initial wave border overlap + if (wave_border_mode != NowPlayingQueueWaveBorderMode.LINE) 15.dp else 0.dp + ), modifier = Modifier .reorderable(queue_list_state) .padding(horizontal = list_padding) @@ -211,3 +242,54 @@ fun QueueTab(page_height: Dp, modifier: Modifier = Modifier) { } } } + +private const val WAVE_BORDER_TIME_SPEED: Float = 0.15f + +@Composable +private fun QueueBorder( + wave_border_mode: NowPlayingQueueWaveBorderMode, + list_padding: Dp, + queue_list_state: ReorderableLazyListState +) { + if (wave_border_mode == NowPlayingQueueWaveBorderMode.LINE) { + Divider(Modifier.padding(horizontal = list_padding), 1.dp, getNPBackground()) + } + else { + val player = LocalPlayerState.current + var wave_border_offset: Float by remember { mutableStateOf(0f) } + + LaunchedEffect(wave_border_mode) { + val update_interval: Long = 1000 / 30 + when (wave_border_mode) { + NowPlayingQueueWaveBorderMode.TIME -> { + while (true) { + wave_border_offset += update_interval * WAVE_BORDER_TIME_SPEED + delay(update_interval) + } + } + NowPlayingQueueWaveBorderMode.TIME_SYNC -> { + while (true) { + wave_border_offset = player.status.getPositionMillis() * WAVE_BORDER_TIME_SPEED + delay(update_interval) + } + } + else -> wave_border_offset = 0f + } + } + + WaveBorder( + Modifier.fillMaxWidth().zIndex(1f), + colour = getNPAltOnBackground(), + getOffset = { + when (wave_border_mode) { + NowPlayingQueueWaveBorderMode.SCROLL -> { + ((50.dp.toPx() * queue_list_state.listState.firstVisibleItemIndex) + queue_list_state.listState.firstVisibleItemScrollOffset) + } + else -> wave_border_offset + } + }, + border_thickness = 1.5.dp, + border_colour = getNPBackground() + ) + } +} 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 57875dfc3..e107fb65b 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -85,6 +86,7 @@ class QueueTabItem(val song: Song, val key: Int) { val anchors = mapOf(-max_offset to 0, 0f to 1, max_offset to 2) val player = LocalPlayerState.current + val density = LocalDensity.current Box( Modifier .offset { IntOffset(swipe_state.offset.value.roundToInt(), 0) } diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/DiscordLoginPage.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/DiscordLoginPage.kt index 6b4b7ebdb..54ad693a4 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/DiscordLoginPage.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/DiscordLoginPage.kt @@ -2,18 +2,27 @@ package com.toasterofbread.spmp.ui.layout.prefspage import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import com.toasterofbread.composesettings.ui.SettingsPage import com.toasterofbread.settings.model.SettingsValueState import com.toasterofbread.spmp.api.getOrReport +import com.toasterofbread.spmp.resources.getString import com.toasterofbread.spmp.ui.layout.DiscordLogin internal fun getDiscordLoginPage(discord_auth: SettingsValueState, manual: Boolean = false): SettingsPage { return object : SettingsPage() { - override val disable_padding: Boolean = true + override val disable_padding: Boolean = !manual override val scrolling: Boolean = false + override val title: String? = if (manual) getString("discord_manual_login_title") else null + override val icon: ImageVector? + @Composable + get() = if (manual) PrefsPageCategory.DISCORD_STATUS.getIcon() else null + @Composable override fun PageView( content_padding: PaddingValues, diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PlayerCategory.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PlayerCategory.kt new file mode 100644 index 000000000..3f0ed34e9 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PlayerCategory.kt @@ -0,0 +1,27 @@ +package com.toasterofbread.spmp.ui.layout.prefspage + +import com.toasterofbread.settings.model.* +import com.toasterofbread.spmp.model.NowPlayingQueueWaveBorderMode +import com.toasterofbread.spmp.model.Settings +import com.toasterofbread.spmp.resources.getString + +internal fun getPlayerCategory(): List { + return listOf( + SettingsItemMultipleChoice( + SettingsValueState(Settings.KEY_NP_QUEUE_WAVE_BORDER_MODE.name), + getString("s_key_np_queue_wave_border_mode"), + getString("s_sub_np_queue_wave_border_mode"), + NowPlayingQueueWaveBorderMode.values().size, + false, + { index -> + when (NowPlayingQueueWaveBorderMode.values()[index]) { + NowPlayingQueueWaveBorderMode.TIME -> getString("s_option_wave_border_mode_time") + NowPlayingQueueWaveBorderMode.TIME_SYNC -> getString("s_option_wave_border_mode_time_sync") + NowPlayingQueueWaveBorderMode.SCROLL -> getString("s_option_wave_border_mode_scroll") + NowPlayingQueueWaveBorderMode.NONE -> getString("s_option_wave_border_mode_none") + NowPlayingQueueWaveBorderMode.LINE -> getString("s_option_wave_border_mode_line") + } + } + ) + ) +} diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPage.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPage.kt index dd1bddd6e..9e88dd6b4 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPage.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPage.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.toasterofbread.composesettings.ui.SettingsInterface import com.toasterofbread.settings.model.* import com.toasterofbread.spmp.model.* @@ -42,9 +43,10 @@ internal enum class PrefsPageScreen { DISCORD_LOGIN, DISCORD_MANUAL_LOGIN } -internal enum class PrefsPageCategory { +enum class PrefsPageCategory { GENERAL, FEED, + PLAYER, LIBRARY, THEME, LYRICS, @@ -57,6 +59,7 @@ internal enum class PrefsPageCategory { fun getIcon(filled: Boolean = false): ImageVector = when (this) { GENERAL -> if (filled) Icons.Filled.Settings else Icons.Outlined.Settings FEED -> if (filled) Icons.Filled.FormatListBulleted else Icons.Outlined.FormatListBulleted + PLAYER -> if (filled) Icons.Filled.PlayArrow else Icons.Outlined.PlayArrow LIBRARY -> if (filled) Icons.Filled.LibraryMusic else Icons.Outlined.LibraryMusic THEME -> if (filled) Icons.Filled.Palette else Icons.Outlined.Palette LYRICS -> if (filled) Icons.Filled.MusicNote else Icons.Outlined.MusicNote @@ -68,6 +71,7 @@ internal enum class PrefsPageCategory { fun getTitle(): String = when (this) { GENERAL -> getString("s_cat_general") FEED -> getString("s_cat_home_page") + PLAYER -> getString("s_cat_player") LIBRARY -> getString("s_cat_library") THEME -> getString("s_cat_theming") LYRICS -> getString("s_cat_lyrics") @@ -79,6 +83,7 @@ internal enum class PrefsPageCategory { fun getDescription(): String = when (this) { GENERAL -> getString("s_cat_desc_general") FEED -> getString("s_cat_desc_home_page") + PLAYER -> getString("s_cat_desc_player") LIBRARY -> getString("s_cat_desc_library") THEME -> getString("s_cat_desc_theming") LYRICS -> getString("s_cat_desc_lyrics") @@ -90,7 +95,13 @@ internal enum class PrefsPageCategory { @OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class) @Composable -fun PrefsPage(pill_menu: PillMenu, bottom_padding: Dp, modifier: Modifier = Modifier, close: () -> Unit) { +fun PrefsPage( + pill_menu: PillMenu, + bottom_padding: Dp, + category_state: MutableState, + modifier: Modifier = Modifier, + close: () -> Unit, +) { val ytm_auth = remember { SettingsValueState( Settings.KEY_YTM_AUTH.name, @@ -100,7 +111,7 @@ fun PrefsPage(pill_menu: PillMenu, bottom_padding: Dp, modifier: Modifier = Modi ).init(Settings.prefs, Settings.Companion::provideDefault) } - var current_category: PrefsPageCategory? by remember { mutableStateOf(null) } + var current_category by category_state val category_open by remember { derivedStateOf { current_category != null } } val settings_interface: SettingsInterface = rememberPrefsPageSettingsInterfade(pill_menu, ytm_auth, { current_category }, { current_category = null }) @@ -148,7 +159,8 @@ fun PrefsPage(pill_menu: PillMenu, bottom_padding: Dp, modifier: Modifier = Modi Column(modifier) { MusicTopBar( Settings.KEY_LYRICS_SHOW_IN_SETTINGS, - Modifier.fillMaxWidth() + Modifier.fillMaxWidth().zIndex(10f), + bottom_border_colour = if (current_category == null) Theme.current.background else null ) Crossfade(category_open || settings_interface.current_page.id!! != PrefsPageScreen.ROOT.ordinal) { open -> diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPageSettingsInterface.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPageSettingsInterface.kt index 4847b7cf8..68401e264 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPageSettingsInterface.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/PrefsPageSettingsInterface.kt @@ -54,6 +54,7 @@ internal fun rememberPrefsPageSettingsInterfade(pill_menu: PillMenu, ytm_auth: S when (getCategory()) { PrefsPageCategory.GENERAL -> getGeneralCategory() PrefsPageCategory.FEED -> getFeedCategory() + PrefsPageCategory.PLAYER -> getPlayerCategory() PrefsPageCategory.LIBRARY -> getLibraryCategory() PrefsPageCategory.THEME -> getThemeCategory(Theme.manager) PrefsPageCategory.LYRICS -> getLyricsCategory() diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/YoutubeMusicLoginPage.kt b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/YoutubeMusicLoginPage.kt index eb65d8ac9..18007cc4e 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/YoutubeMusicLoginPage.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/spmp/ui/layout/prefspage/YoutubeMusicLoginPage.kt @@ -2,18 +2,27 @@ package com.toasterofbread.spmp.ui.layout.prefspage import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import com.toasterofbread.composesettings.ui.SettingsPage import com.toasterofbread.settings.model.SettingsValueState import com.toasterofbread.spmp.model.YoutubeMusicAuthInfo +import com.toasterofbread.spmp.resources.getString import com.toasterofbread.spmp.ui.layout.YoutubeMusicLogin internal fun getYoutubeMusicLoginPage(ytm_auth: SettingsValueState, manual: Boolean = false): SettingsPage { return object : SettingsPage() { - override val disable_padding: Boolean = true + override val disable_padding: Boolean = !manual override val scrolling: Boolean = false + override val title: String? = if (manual) getString("youtube_manual_login_title") else null + override val icon: ImageVector? + @Composable + get() = if (manual) Icons.Default.PlayCircle else null + @Composable override fun PageView( content_padding: PaddingValues, diff --git a/shared/src/commonMain/kotlin/com/toasterofbread/utils/composable/WidthShrinkText.kt b/shared/src/commonMain/kotlin/com/toasterofbread/utils/composable/WidthShrinkText.kt index 23ff01e21..dbad19ac1 100644 --- a/shared/src/commonMain/kotlin/com/toasterofbread/utils/composable/WidthShrinkText.kt +++ b/shared/src/commonMain/kotlin/com/toasterofbread/utils/composable/WidthShrinkText.kt @@ -24,7 +24,8 @@ fun WidthShrinkText( string: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current, - inline_content: Map = mapOf() + inline_content: Map = mapOf(), + alignment: Alignment = Alignment.TopStart ) { var text_style by remember(style) { mutableStateOf(style) } var text_style_large: TextStyle? by remember(style) { mutableStateOf(null) } @@ -32,7 +33,7 @@ fun WidthShrinkText( val delta = 0.05 - Box(modifier) { + Box(modifier, contentAlignment = alignment) { Text( string, Modifier.fillMaxWidth().drawWithContent { if (ready_to_draw) drawContent() }, @@ -73,16 +74,30 @@ fun WidthShrinkText( fun WidthShrinkText( text: String, modifier: Modifier = Modifier, - style: TextStyle = LocalTextStyle.current + style: TextStyle = LocalTextStyle.current, + alignment: Alignment = Alignment.TopStart ) { - WidthShrinkText(AnnotatedString(text), modifier, style) + WidthShrinkText( + AnnotatedString(text), + modifier, + style, + alignment = alignment + ) } @Composable -fun WidthShrinkText(text: String, fontSize: TextUnit, modifier: Modifier = Modifier, fontWeight: FontWeight? = null, colour: Color = LocalContentColor.current) { +fun WidthShrinkText( + text: String, + fontSize: TextUnit, + modifier: Modifier = Modifier, + fontWeight: FontWeight? = null, + colour: Color = LocalContentColor.current, + alignment: Alignment = Alignment.TopStart +) { WidthShrinkText( text, modifier, - LocalTextStyle.current.copy(fontSize = fontSize, fontWeight = fontWeight, color = colour) + LocalTextStyle.current.copy(fontSize = fontSize, fontWeight = fontWeight, color = colour), + alignment ) } 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 26d705ab2..d91a15e97 100644 --- a/shared/src/commonMain/resources/assets/values-ja-JP/strings.xml +++ b/shared/src/commonMain/resources/assets/values-ja-JP/strings.xml @@ -232,6 +232,16 @@ おすすめフィード フィルターを表示する + プレイヤー + + 曲キューの波ボーダーのアニメーションモード + 曲キューと操作ボタンの間にある波型ボーダーのアニメーション方法 + 時の流れ + 時の流れ(再生時のみ) + 曲キューのスクロールと + アニメーションなし + 線型ボーダー + ラジオ番組を作成 条件に当てはまる曲がありません 次へ @@ -264,7 +274,7 @@ 検索 ロード中 - Discord ステータス + Discordステータス 以下のキーワードをテキスト内で使うことで、流れてる曲の情報を表示することができます: - $song (曲) - $artist (アーティスト) @@ -297,6 +307,7 @@ 言語やアプリ動作などのオプション おすすめの曲やアーティストの調節 + 音楽プレイヤー画面の変更 ライブラリの表示設定 アプリ内の色をカスタマイズする 歌詞の表示や動作の設定 diff --git a/shared/src/commonMain/resources/assets/values/strings.xml b/shared/src/commonMain/resources/assets/values/strings.xml index dd55f2706..5fa2e5946 100644 --- a/shared/src/commonMain/resources/assets/values/strings.xml +++ b/shared/src/commonMain/resources/assets/values/strings.xml @@ -276,6 +276,16 @@ Treat artist singles as a song Playlists contained in the 'Singles' tab on an artist's page will be treated as the contained song, if there is only one + Player + + Queue wave border mode + Animation mode of the wave border between the queued songs and option buttons + Animate over time + Animate over time while playing + Animate on queue scroll + No animation + Line border + Library Show likes playlist @@ -425,6 +435,7 @@ Language, behaviour, and other options Cherry-pick song recommendations + Adjust music player view Control how the library is displayed Customise app colours and change accent source Lyrics appearance and behaviour