mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
feat(onboarding): enhance welcome screen with structured guide steps and improved layout
This commit is contained in:
parent
caad76ba95
commit
76518a01a1
@ -246,7 +246,7 @@
|
|||||||
<string name="welcome_app_name">Spotube</string>
|
<string name="welcome_app_name">Spotube</string>
|
||||||
<string name="welcome_brand_freedom">Music, without the strings.</string>
|
<string name="welcome_brand_freedom">Music, without the strings.</string>
|
||||||
<string name="welcome_brand_quiet_description">An open-source player for a world of music.\nYours to explore. Yours to make your own.</string>
|
<string name="welcome_brand_quiet_description">An open-source player for a world of music.\nYours to explore. Yours to make your own.</string>
|
||||||
<string name="welcome_donate_quiet_title">Keep music free.</string>
|
<string name="welcome_donate_quiet_title">Keep this project alive.</string>
|
||||||
<string name="welcome_donate_letter">Spotube is built by people, not a subscription business. If it becomes part of your everyday life, a little support helps us give it the care it deserves.\n\nThank you for making room for independent software.</string>
|
<string name="welcome_donate_letter">Spotube is built by people, not a subscription business. If it becomes part of your everyday life, a little support helps us give it the care it deserves.\n\nThank you for making room for independent software.</string>
|
||||||
<string name="welcome_plugins_setup_guide">Start with a catalog plugin to find songs and playlists. Choose an audio plugin to play them. Lyrics and listening history are optional. Discover a plugin below, review its permissions, install it, then select it here.</string>
|
<string name="welcome_plugins_setup_guide">Start with a catalog plugin to find songs and playlists. Choose an audio plugin to play them. Lyrics and listening history are optional. Discover a plugin below, review its permissions, install it, then select it here.</string>
|
||||||
<string name="welcome_plugins_discover">Discover plugins</string>
|
<string name="welcome_plugins_discover">Discover plugins</string>
|
||||||
@ -255,12 +255,21 @@
|
|||||||
<string name="welcome_plugins_more">More plugins</string>
|
<string name="welcome_plugins_more">More plugins</string>
|
||||||
<string name="welcome_plugins_discovery_empty">No new plugins found. You can also install one from a link or a .smplug file above.</string>
|
<string name="welcome_plugins_discovery_empty">No new plugins found. You can also install one from a link or a .smplug file above.</string>
|
||||||
<string name="welcome_plugins_discovery_error">Plugin discovery is unavailable right now. Try again, or install from a link or file.</string>
|
<string name="welcome_plugins_discovery_error">Plugin discovery is unavailable right now. Try again, or install from a link or file.</string>
|
||||||
<string name="welcome_how_quiet_title">A familiar player. A different kind of freedom.</string>
|
<string name="welcome_step_welcome">Welcome</string>
|
||||||
<string name="welcome_how_quiet_description">Browse through your chosen catalog. Press play and your audio plugin finds the stream.\n\nYour device handles playback and caching. Add synced lyrics, save tracks for later, or listen together with Group Jam.</string>
|
<string name="welcome_step_support">Support</string>
|
||||||
<string name="welcome_demo_streaming">DEMONSTRATION</string>
|
<string name="welcome_step_plugins">Plugins</string>
|
||||||
<string name="welcome_demo_paused">PAUSED</string>
|
<string name="welcome_step_guide">Guide</string>
|
||||||
<string name="welcome_demo_pause_action">Pause demonstration</string>
|
|
||||||
<string name="welcome_demo_play_action">Play demonstration</string>
|
<string name="welcome_guide_title">How to use Spotube</string>
|
||||||
|
<string name="welcome_guide_subtitle">Four quick steps and you’re listening.</string>
|
||||||
|
<string name="welcome_guide_signin_title">Sign in</string>
|
||||||
|
<string name="welcome_guide_signin_desc">Connect your Spotify account, or pick your own catalog plugin.</string>
|
||||||
|
<string name="welcome_guide_search_title">Search and play</string>
|
||||||
|
<string name="welcome_guide_search_desc">Find any song, album, or artist, then press play.</string>
|
||||||
|
<string name="welcome_guide_offline_title">Listen offline</string>
|
||||||
|
<string name="welcome_guide_offline_desc">Download tracks to keep playing without a connection.</string>
|
||||||
|
<string name="welcome_guide_lyrics_title">Follow the lyrics</string>
|
||||||
|
<string name="welcome_guide_lyrics_desc">Open synced lyrics from the player while a track plays.</string>
|
||||||
|
|
||||||
<!-- Page 1: branding -->
|
<!-- Page 1: branding -->
|
||||||
<string name="welcome_brand_tagline">Your music. Your sources. Your rules.</string>
|
<string name="welcome_brand_tagline">Your music. Your sources. Your rules.</string>
|
||||||
|
|||||||
@ -107,9 +107,12 @@ fun TextWithShimmer(
|
|||||||
minLines: Int = 1,
|
minLines: Int = 1,
|
||||||
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||||
style: TextStyle = LocalTextStyle.current) {
|
style: TextStyle = LocalTextStyle.current) {
|
||||||
|
// While a skeleton tree is loading, the placeholder glyphs must not be readable:
|
||||||
|
// only the shimmer bars (from `shimmerApply`) should show through.
|
||||||
|
val isLoadingSkeleton = LocalSkeletonLoading.current
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
color = color,
|
color = if (isLoadingSkeleton) Color.Transparent else color,
|
||||||
modifier = Modifier.shimmerApply().then(modifier),
|
modifier = Modifier.shimmerApply().then(modifier),
|
||||||
autoSize = autoSize,
|
autoSize = autoSize,
|
||||||
fontSize = fontSize,
|
fontSize = fontSize,
|
||||||
|
|||||||
@ -26,6 +26,9 @@ import org.spotube.millennium.composables.MillenniumIconButton
|
|||||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||||
import org.spotube.millennium.composables.MillenniumSeparator
|
import org.spotube.millennium.composables.MillenniumSeparator
|
||||||
|
import org.spotube.millennium.composables.MillenniumSegmentedButton
|
||||||
|
import org.spotube.millennium.composables.MillenniumSegmentedButtonDefaults
|
||||||
|
import org.spotube.millennium.composables.MillenniumSegmentedButtonRow
|
||||||
import org.spotube.millennium.composables.MillenniumTextField
|
import org.spotube.millennium.composables.MillenniumTextField
|
||||||
import org.spotube.millennium.theme.MillenniumTheme
|
import org.spotube.millennium.theme.MillenniumTheme
|
||||||
import org.spotube.millennium.theme.millenniumPanelBrush
|
import org.spotube.millennium.theme.millenniumPanelBrush
|
||||||
@ -285,29 +288,19 @@ internal fun PluginScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Row(
|
MillenniumSegmentedButtonRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
modifier = Modifier.padding(bottom = 12.dp)) {
|
||||||
modifier = Modifier.padding(bottom = 12.dp)
|
MillenniumSegmentedButton(
|
||||||
) {
|
selected = data.onboardingDiscover,
|
||||||
MillenniumButton(
|
|
||||||
onClick = { viewModel.showOnboardingDiscovery(true) },
|
onClick = { viewModel.showOnboardingDiscovery(true) },
|
||||||
style = MillenniumButtonStyle.Secondary) {
|
shape = MillenniumSegmentedButtonDefaults.itemShape(0, 2)) {
|
||||||
Text(
|
Text(stringResource(Res.string.welcome_plugins_discover))
|
||||||
stringResource(Res.string.welcome_plugins_discover),
|
|
||||||
fontWeight = if (data.onboardingDiscover) FontWeight.Bold else FontWeight.Normal
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
MillenniumButton(
|
MillenniumSegmentedButton(
|
||||||
onClick = {
|
selected = !data.onboardingDiscover,
|
||||||
viewModel.showOnboardingDiscovery(
|
onClick = { viewModel.showOnboardingDiscovery(false) },
|
||||||
false
|
shape = MillenniumSegmentedButtonDefaults.itemShape(1, 2)) {
|
||||||
)
|
Text(stringResource(Res.string.welcome_plugins_sources))
|
||||||
},
|
|
||||||
style = MillenniumButtonStyle.Secondary) {
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.welcome_plugins_sources),
|
|
||||||
fontWeight = if (!data.onboardingDiscover) FontWeight.Bold else FontWeight.Normal
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,10 @@ import spotube.composeapp.generated.resources.welcome_donate_patreon_title
|
|||||||
import spotube.composeapp.generated.resources.welcome_get_started
|
import spotube.composeapp.generated.resources.welcome_get_started
|
||||||
import spotube.composeapp.generated.resources.welcome_lets_go
|
import spotube.composeapp.generated.resources.welcome_lets_go
|
||||||
import spotube.composeapp.generated.resources.welcome_next
|
import spotube.composeapp.generated.resources.welcome_next
|
||||||
|
import spotube.composeapp.generated.resources.welcome_step_guide
|
||||||
|
import spotube.composeapp.generated.resources.welcome_step_plugins
|
||||||
|
import spotube.composeapp.generated.resources.welcome_step_support
|
||||||
|
import spotube.composeapp.generated.resources.welcome_step_welcome
|
||||||
|
|
||||||
/** Order matches the 4 onboarding pages. */
|
/** Order matches the 4 onboarding pages. */
|
||||||
internal val welcomePrimaryLabels: List<StringResource> = listOf(
|
internal val welcomePrimaryLabels: List<StringResource> = listOf(
|
||||||
@ -40,6 +44,13 @@ internal val welcomePrimaryLabels: List<StringResource> = listOf(
|
|||||||
Res.string.welcome_next,
|
Res.string.welcome_next,
|
||||||
Res.string.welcome_get_started)
|
Res.string.welcome_get_started)
|
||||||
|
|
||||||
|
/** Short labels for the step switcher. Order matches the 4 onboarding pages. */
|
||||||
|
internal val welcomeStepLabels: List<StringResource> = listOf(
|
||||||
|
Res.string.welcome_step_welcome,
|
||||||
|
Res.string.welcome_step_support,
|
||||||
|
Res.string.welcome_step_plugins,
|
||||||
|
Res.string.welcome_step_guide)
|
||||||
|
|
||||||
internal val welcomeDonations: List<DonationOption> = listOf(
|
internal val welcomeDonations: List<DonationOption> = listOf(
|
||||||
DonationOption(
|
DonationOption(
|
||||||
id = "patreon",
|
id = "patreon",
|
||||||
|
|||||||
@ -33,5 +33,4 @@ data class WelcomeUiState(
|
|||||||
val pageCount: Int,
|
val pageCount: Int,
|
||||||
val isLastPage: Boolean,
|
val isLastPage: Boolean,
|
||||||
val primaryLabel: StringResource,
|
val primaryLabel: StringResource,
|
||||||
val donations: List<DonationOption>,
|
val donations: List<DonationOption>)
|
||||||
val demoPlaying: Boolean = true)
|
|
||||||
|
|||||||
@ -20,28 +20,22 @@ package dev.krtirtho.spotube.modules.welcome
|
|||||||
|
|
||||||
import org.spotube.millennium.composables.MillenniumButton
|
import org.spotube.millennium.composables.MillenniumButton
|
||||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||||
|
import org.spotube.millennium.composables.MillenniumCard
|
||||||
import org.spotube.millennium.composables.MillenniumIcon
|
import org.spotube.millennium.composables.MillenniumIcon
|
||||||
import androidx.compose.animation.AnimatedContent
|
import org.spotube.millennium.composables.MillenniumSeparator
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import org.spotube.millennium.composables.MillenniumTabGroup
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.slideInHorizontally
|
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
|
|
||||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||||
import com.composeunstyled.Text
|
import com.composeunstyled.Text
|
||||||
@ -49,7 +43,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@ -63,13 +56,15 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import spotube.composeapp.generated.resources.Res
|
import spotube.composeapp.generated.resources.Res
|
||||||
import spotube.composeapp.generated.resources.welcome_back
|
|
||||||
import spotube.composeapp.generated.resources.welcome_skip
|
import spotube.composeapp.generated.resources.welcome_skip
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First-run Welcome/Onboarding screen. It is a route ([dev.krtirtho.spotube.core.navigation.Routes.Welcome])
|
* First-run Welcome/Onboarding screen. It is a route
|
||||||
* rendered inside the app shell. Uses the application's base controls and keeps plugin setup
|
* ([dev.krtirtho.spotube.core.navigation.Routes.Welcome]) rendered inside the app shell.
|
||||||
* in-place, with a neutral palette independent of the user's chosen accent.
|
*
|
||||||
|
* The steps are exposed through a Millennium tab group rather than ad-hoc dots or a footer
|
||||||
|
* walk-through, so the screen reads as one deliberate panel on every window size.
|
||||||
|
* Each step owns its content and the footer only carries the forward action.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
||||||
@ -96,99 +91,31 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
|
||||||
|
|
||||||
Column(
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
contentAlignment = Alignment.Center) {
|
||||||
|
MillenniumCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.widthIn(max = 1040.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.fillMaxWidth()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally) {
|
.fillMaxHeight(),
|
||||||
// Central stage (animated page content)
|
contentPadding = PaddingValues(0.dp)) {
|
||||||
Box(
|
Column(Modifier.fillMaxSize()) {
|
||||||
modifier = Modifier
|
WelcomeStepSwitcher(
|
||||||
.weight(1f)
|
modifier = Modifier
|
||||||
.widthIn(max = 1280.dp)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
contentAlignment = Alignment.Center) {
|
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||||
AnimatedContent(
|
state = state,
|
||||||
modifier = Modifier.fillMaxSize(),
|
viewModel = viewModel)
|
||||||
targetState = state.currentPageIndex,
|
MillenniumSeparator()
|
||||||
transitionSpec = {
|
WelcomeActionBar(
|
||||||
if (targetState > initialState) {
|
state = state,
|
||||||
(slideInHorizontally { it / 4 } + fadeIn()) togetherWith
|
viewModel = viewModel)
|
||||||
(slideOutHorizontally { -it / 4 } + fadeOut())
|
|
||||||
} else {
|
|
||||||
(slideInHorizontally { -it / 4 } + fadeIn()) togetherWith
|
|
||||||
(slideOutHorizontally { it / 4 } + fadeOut())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = "welcome_page") { index ->
|
|
||||||
when (index) {
|
|
||||||
0 -> BrandingPage()
|
|
||||||
|
|
||||||
1 -> DonationPage(
|
|
||||||
donations = state.donations,
|
|
||||||
onDonate = viewModel::openDonation)
|
|
||||||
|
|
||||||
2 -> PluginsPage()
|
|
||||||
|
|
||||||
else -> HowItWorksPage(
|
|
||||||
isPlaying = state.demoPlaying,
|
|
||||||
onTogglePlayback = viewModel::toggleDemo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(10.dp))
|
|
||||||
|
|
||||||
// Bottom precision navigation bar: Back | Page Dots | Action Button
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.widthIn(max = 680.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
// Left: Back button
|
|
||||||
if (state.currentPageIndex > 0) {
|
|
||||||
MillenniumButton(
|
|
||||||
onClick = viewModel::previousPage,
|
|
||||||
style = MillenniumButtonStyle.Secondary) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(Res.string.welcome_back),
|
|
||||||
color = AppTheme.colors.onSurface)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier.width(76.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center: Precision page dots
|
|
||||||
Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
|
|
||||||
PageIndicator(
|
|
||||||
pageCount = state.pageCount,
|
|
||||||
currentPageIndex = state.currentPageIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right: Primary Next / Let's Go action
|
|
||||||
MillenniumButton(
|
|
||||||
onClick = viewModel::onPrimary,
|
|
||||||
style = MillenniumButtonStyle.Secondary) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(state.primaryLabel),
|
|
||||||
fontWeight = FontWeight.SemiBold)
|
|
||||||
MillenniumIcon(
|
|
||||||
imageVector = if (state.isLastPage) {
|
|
||||||
Iconsax.IconsaxCheckCircle
|
|
||||||
} else {
|
|
||||||
Iconsax.IconsaxNext
|
|
||||||
},
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,31 +125,59 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageIndicator(
|
private fun WelcomeStepSwitcher(
|
||||||
pageCount: Int,
|
state: WelcomeUiState,
|
||||||
currentPageIndex: Int,
|
viewModel: WelcomeViewModel,
|
||||||
modifier: Modifier = Modifier) {
|
modifier: Modifier = Modifier) {
|
||||||
Row(
|
val labels = welcomeStepLabels.map { stringResource(it) }
|
||||||
modifier = modifier
|
MillenniumTabGroup(
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
selectedTab = state.currentPageIndex,
|
||||||
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
onSelectedTabChange = viewModel::goToPage,
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
tabs = labels.indices.toList(),
|
||||||
repeat(pageCount) { index ->
|
modifier = modifier,
|
||||||
val selected = index == currentPageIndex
|
tabLabel = { labels[it] }) { index ->
|
||||||
val width by animateDpAsState(
|
when (index) {
|
||||||
targetValue = if (selected) 20.dp else 6.dp,
|
0 -> BrandingPage()
|
||||||
label = "welcome_page_indicator")
|
|
||||||
Box(
|
1 -> DonationPage(
|
||||||
modifier = Modifier
|
donations = state.donations,
|
||||||
.size(width = width, height = 6.dp)
|
onDonate = viewModel::openDonation)
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
2 -> PluginsPage()
|
||||||
if (selected) {
|
|
||||||
AppTheme.colors.onSurface
|
else -> HowItWorksPage()
|
||||||
} else {
|
}
|
||||||
AppTheme.colors.outlineVariant
|
}
|
||||||
}
|
}
|
||||||
))
|
|
||||||
|
@Composable
|
||||||
|
private fun WelcomeActionBar(
|
||||||
|
state: WelcomeUiState,
|
||||||
|
viewModel: WelcomeViewModel) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
MillenniumButton(
|
||||||
|
onClick = viewModel::onPrimary,
|
||||||
|
style = MillenniumButtonStyle.Secondary) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(state.primaryLabel),
|
||||||
|
fontWeight = FontWeight.SemiBold)
|
||||||
|
MillenniumIcon(
|
||||||
|
imageVector = if (state.isLastPage) {
|
||||||
|
Iconsax.IconsaxCheckCircle
|
||||||
|
} else {
|
||||||
|
Iconsax.IconsaxNext
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -30,18 +30,16 @@ import kotlinx.coroutines.launch
|
|||||||
class WelcomeViewModel(
|
class WelcomeViewModel(
|
||||||
private val repository: WelcomeRepository) : ViewModel() {
|
private val repository: WelcomeRepository) : ViewModel() {
|
||||||
private val currentPageIndex = MutableStateFlow(0)
|
private val currentPageIndex = MutableStateFlow(0)
|
||||||
private val demoPlaying = MutableStateFlow(true)
|
|
||||||
|
|
||||||
val uiState: StateFlow<WelcomeUiState> =
|
val uiState: StateFlow<WelcomeUiState> =
|
||||||
combine(currentPageIndex, demoPlaying) { index, playing ->
|
currentPageIndex.map { index ->
|
||||||
val pageCount = welcomePrimaryLabels.size
|
val pageCount = welcomePrimaryLabels.size
|
||||||
WelcomeUiState(
|
WelcomeUiState(
|
||||||
currentPageIndex = index,
|
currentPageIndex = index,
|
||||||
pageCount = pageCount,
|
pageCount = pageCount,
|
||||||
isLastPage = index == pageCount - 1,
|
isLastPage = index == pageCount - 1,
|
||||||
primaryLabel = welcomePrimaryLabels[index.coerceIn(0, pageCount - 1)],
|
primaryLabel = welcomePrimaryLabels[index.coerceIn(0, pageCount - 1)],
|
||||||
donations = welcomeDonations,
|
donations = welcomeDonations)
|
||||||
demoPlaying = playing)
|
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
@ -53,16 +51,16 @@ class WelcomeViewModel(
|
|||||||
primaryLabel = welcomePrimaryLabels.first(),
|
primaryLabel = welcomePrimaryLabels.first(),
|
||||||
donations = welcomeDonations))
|
donations = welcomeDonations))
|
||||||
|
|
||||||
fun nextPage() {
|
fun goToPage(index: Int) {
|
||||||
currentPageIndex.update { (it + 1).coerceAtMost(welcomePrimaryLabels.lastIndex) }
|
currentPageIndex.update { index.coerceIn(0, welcomePrimaryLabels.lastIndex) }
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleDemo() {
|
|
||||||
demoPlaying.update { !it }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrimary() {
|
fun onPrimary() {
|
||||||
if (currentPageIndex.value == welcomePrimaryLabels.lastIndex) completeOnboarding() else nextPage()
|
if (currentPageIndex.value == welcomePrimaryLabels.lastIndex) {
|
||||||
|
completeOnboarding()
|
||||||
|
} else {
|
||||||
|
goToPage(currentPageIndex.value + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openDonation(url: String) {
|
fun openDonation(url: String) {
|
||||||
@ -71,10 +69,6 @@ class WelcomeViewModel(
|
|||||||
runCatching { dev.krtirtho.spotube.openUrlInBrowser(url) }
|
runCatching { dev.krtirtho.spotube.openUrlInBrowser(url) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun previousPage() {
|
|
||||||
currentPageIndex.update { (it - 1).coerceAtLeast(0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun completeOnboarding() {
|
fun completeOnboarding() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.completeOnboarding()
|
repository.completeOnboarding()
|
||||||
|
|||||||
@ -33,13 +33,17 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
import org.spotube.millennium.theme.MillenniumTheme
|
import org.spotube.millennium.theme.MillenniumTheme
|
||||||
import spotube.composeapp.generated.resources.*
|
import spotube.composeapp.generated.resources.*
|
||||||
|
|
||||||
/** Installs the Millennium theme for the onboarding pages. */
|
/**
|
||||||
|
* Reapplies the Millennium theme for the onboarding pages using the app's active configuration,
|
||||||
|
* so onboarding matches the palette and appearance used by the rest of the app.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun WelcomeTheme(content: @Composable () -> Unit) {
|
fun WelcomeTheme(content: @Composable () -> Unit) {
|
||||||
val dark = AppTheme.colors.surface.luminance() < 0.5f
|
val dark = AppTheme.colors.surface.luminance() < 0.5f
|
||||||
MillenniumTheme(dark = dark) {
|
MillenniumTheme(
|
||||||
content()
|
dark = dark,
|
||||||
}
|
config = MillenniumTheme.config,
|
||||||
|
content = content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@ -1,187 +1,98 @@
|
|||||||
package dev.krtirtho.spotube.modules.welcome.pages
|
package dev.krtirtho.spotube.modules.welcome.pages
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import com.composeunstyled.currentWindowWidthBreakpoint
|
|
||||||
import org.spotube.millennium.adaptive.WindowBreakpoints
|
|
||||||
import org.spotube.millennium.composables.MillenniumCard
|
|
||||||
import org.spotube.millennium.composables.MillenniumIcon
|
import org.spotube.millennium.composables.MillenniumIcon
|
||||||
import org.spotube.millennium.composables.MillenniumIconButton
|
import org.spotube.millennium.composables.MillenniumListTile
|
||||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
||||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||||
import com.composeunstyled.Text
|
import com.composeunstyled.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.krtirtho.spotube.resources.iconsax.*
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentDownload
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.User
|
||||||
|
import org.jetbrains.compose.resources.StringResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import spotube.composeapp.generated.resources.*
|
import spotube.composeapp.generated.resources.Res
|
||||||
import kotlin.math.PI
|
import spotube.composeapp.generated.resources.welcome_guide_lyrics_desc
|
||||||
import kotlin.math.sin
|
import spotube.composeapp.generated.resources.welcome_guide_lyrics_title
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_offline_desc
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_offline_title
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_search_desc
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_search_title
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_signin_desc
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_signin_title
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_subtitle
|
||||||
|
import spotube.composeapp.generated.resources.welcome_guide_title
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The onboarding guide: a short, concrete answer to "how do I actually use this app?".
|
||||||
|
* No marketing prose — just the four things a new user needs to do.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun HowItWorksPage(
|
fun HowItWorksPage(modifier: Modifier = Modifier) {
|
||||||
isPlaying: Boolean,
|
Column(
|
||||||
onTogglePlayback: () -> Unit,
|
modifier
|
||||||
modifier: Modifier = Modifier
|
.fillMaxSize()
|
||||||
) {
|
.verticalScroll(rememberScrollState())
|
||||||
Box(modifier.fillMaxSize()) {
|
.padding(28.dp),
|
||||||
val wide = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded
|
verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||||
if (wide) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Row(
|
Text(
|
||||||
Modifier.fillMaxSize().padding(40.dp),
|
stringResource(Res.string.welcome_guide_title),
|
||||||
horizontalArrangement = Arrangement.spacedBy(48.dp),
|
style = AppTheme.typography.headlineMedium.copy(fontFamily = FontFamily.Serif))
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
Text(
|
||||||
PlayerDemonstration(isPlaying, onTogglePlayback, Modifier.weight(1f))
|
stringResource(Res.string.welcome_guide_subtitle),
|
||||||
ListeningIntroduction(Modifier.weight(1f))
|
style = AppTheme.typography.bodyLarge,
|
||||||
}
|
color = AppTheme.colors.onSurfaceVariant)
|
||||||
} else {
|
}
|
||||||
Column(
|
|
||||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically)) {
|
WelcomeGuideStep(
|
||||||
ListeningIntroduction()
|
icon = Iconsax.User,
|
||||||
PlayerDemonstration(isPlaying, onTogglePlayback)
|
title = Res.string.welcome_guide_signin_title,
|
||||||
}
|
description = Res.string.welcome_guide_signin_desc)
|
||||||
|
WelcomeGuideStep(
|
||||||
|
icon = Iconsax.IconsaxSearch,
|
||||||
|
title = Res.string.welcome_guide_search_title,
|
||||||
|
description = Res.string.welcome_guide_search_desc)
|
||||||
|
WelcomeGuideStep(
|
||||||
|
icon = Iconsax.IconsaxDocumentDownload,
|
||||||
|
title = Res.string.welcome_guide_offline_title,
|
||||||
|
description = Res.string.welcome_guide_offline_desc)
|
||||||
|
WelcomeGuideStep(
|
||||||
|
icon = Iconsax.IconsaxMusic,
|
||||||
|
title = Res.string.welcome_guide_lyrics_title,
|
||||||
|
description = Res.string.welcome_guide_lyrics_desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListeningIntroduction(modifier: Modifier = Modifier) {
|
private fun WelcomeGuideStep(
|
||||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
icon: ImageVector,
|
||||||
Text(
|
title: StringResource,
|
||||||
stringResource(Res.string.welcome_how_quiet_title),
|
description: StringResource,
|
||||||
style = AppTheme.typography.headlineLarge.copy(fontFamily = FontFamily.Serif))
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.welcome_how_quiet_description),
|
|
||||||
style = AppTheme.typography.bodyLarge,
|
|
||||||
color = AppTheme.colors.onSurfaceVariant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Silent, illustrative player. The ViewModel owns the play/pause interaction. */
|
|
||||||
@Composable
|
|
||||||
private fun PlayerDemonstration(
|
|
||||||
isPlaying: Boolean,
|
|
||||||
onTogglePlayback: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
val motion = rememberInfiniteTransition(label = "welcome_record")
|
MillenniumListTile(
|
||||||
val rotation by motion.animateFloat(
|
leading = {
|
||||||
0f, 360f,
|
MillenniumIcon(
|
||||||
infiniteRepeatable(tween(12000, easing = LinearEasing)), label = "record_rotation"
|
imageVector = icon,
|
||||||
)
|
contentDescription = null,
|
||||||
val phase by motion.animateFloat(
|
modifier = Modifier.size(20.dp),
|
||||||
0f, (2 * PI).toFloat(),
|
tint = AppTheme.colors.primary)
|
||||||
infiniteRepeatable(tween(2400, easing = LinearEasing)), label = "audio_meter"
|
},
|
||||||
)
|
title = { Text(stringResource(title)) },
|
||||||
val scheme = AppTheme.colors
|
subtitle = { Text(stringResource(description)) })
|
||||||
|
|
||||||
MillenniumCard(modifier = modifier.fillMaxWidth()) {
|
|
||||||
Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
|
||||||
Text(
|
|
||||||
stringResource(if (isPlaying) Res.string.welcome_demo_streaming else Res.string.welcome_demo_paused),
|
|
||||||
style = AppTheme.typography.labelSmall,
|
|
||||||
color = scheme.onSurfaceVariant)
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(20.dp)
|
|
||||||
) {
|
|
||||||
Canvas(Modifier.size(104.dp).rotate(if (isPlaying) rotation else 0f)) {
|
|
||||||
val radius = size.minDimension / 2f
|
|
||||||
drawCircle(Color(0xFF171717))
|
|
||||||
for (groove in 1..8) {
|
|
||||||
drawCircle(
|
|
||||||
Color(0xFF343434),
|
|
||||||
radius * (0.4f + groove * 0.065f),
|
|
||||||
style = Stroke(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
drawCircle(Color(0xFF334A6B), radius * 0.32f)
|
|
||||||
drawCircle(Color(0xFFE1DDD5), radius * 0.045f)
|
|
||||||
// An off-centre label mark makes the turntable's motion visible.
|
|
||||||
drawCircle(
|
|
||||||
Color(0xFFD6CEC0),
|
|
||||||
radius * 0.025f,
|
|
||||||
Offset(center.x, center.y - radius * 0.2f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.welcome_how_mockup_title),
|
|
||||||
style = AppTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.welcome_how_mockup_artist),
|
|
||||||
style = AppTheme.typography.bodySmall, color = scheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Canvas(Modifier.fillMaxWidth().height(32.dp)) {
|
|
||||||
val count = 18
|
|
||||||
val barWidth = size.width / (count * 1.7f)
|
|
||||||
repeat(count) { index ->
|
|
||||||
val wave = if (isPlaying) (sin(phase + index * 0.6f) + 1f) / 2f else 0f
|
|
||||||
val height = size.height * (0.12f + wave * 0.88f)
|
|
||||||
drawRect(
|
|
||||||
scheme.primary,
|
|
||||||
Offset(index * size.width / count, size.height - height),
|
|
||||||
androidx.compose.ui.geometry.Size(barWidth, height)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.welcome_how_mockup_lyric),
|
|
||||||
style = AppTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Serif),
|
|
||||||
color = scheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
MillenniumIcon(
|
|
||||||
Iconsax.IconsaxPrevious,
|
|
||||||
null,
|
|
||||||
Modifier.size(18.dp),
|
|
||||||
tint = scheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(24.dp))
|
|
||||||
MillenniumIconButton(
|
|
||||||
onClick = onTogglePlayback,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
style = MillenniumIconButtonStyle.Quiet) {
|
|
||||||
MillenniumIcon(
|
|
||||||
if (isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
|
||||||
stringResource(if (isPlaying) Res.string.welcome_demo_pause_action else Res.string.welcome_demo_play_action),
|
|
||||||
Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(24.dp))
|
|
||||||
MillenniumIcon(
|
|
||||||
Iconsax.IconsaxNext,
|
|
||||||
null,
|
|
||||||
Modifier.size(18.dp),
|
|
||||||
tint = scheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.foundation.window.WindowDraggableArea
|
import androidx.compose.foundation.window.WindowDraggableArea
|
||||||
|
|
||||||
import org.spotube.millennium.composables.MillenniumTopAppBar
|
import org.spotube.millennium.composables.MillenniumTopAppBar
|
||||||
@ -53,6 +56,8 @@ import dev.nucleusframework.window.WindowDoubleClickAction
|
|||||||
import dev.nucleusframework.window.windowDragArea
|
import dev.nucleusframework.window.windowDragArea
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
|
private val WindowControlsReservedWidth = 132.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ApplicationMainBar(
|
actual fun ApplicationMainBar(
|
||||||
title: @Composable (() -> Unit),
|
title: @Composable (() -> Unit),
|
||||||
@ -68,6 +73,9 @@ actual fun ApplicationMainBar(
|
|||||||
title = title,
|
title = title,
|
||||||
actions = {
|
actions = {
|
||||||
actions()
|
actions()
|
||||||
|
// Reserve room for the platform window controls instead of shrinking the
|
||||||
|
// whole bar, so it spans the full width with no seam under the controls.
|
||||||
|
Spacer(Modifier.width(WindowControlsReservedWidth))
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (backButton) {
|
if (backButton) {
|
||||||
@ -75,7 +83,9 @@ actual fun ApplicationMainBar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
modifier = Modifier.padding(end = 125.dp) // To avoid overlap with window buttons
|
background = if (transparent) SolidColor(Color.Transparent) else null,
|
||||||
|
separatorColor = if (transparent) Color.Transparent else null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
LocalWindowScope.current.WindowControls(
|
LocalWindowScope.current.WindowControls(
|
||||||
modifier = Modifier.align(Alignment.TopEnd).heightIn(max = 48.dp),
|
modifier = Modifier.align(Alignment.TopEnd).heightIn(max = 48.dp),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user