mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
Compare commits
4 Commits
76c472cdbf
...
d31ca87e8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d31ca87e8d | ||
|
|
8efcbd4af4 | ||
|
|
76518a01a1 | ||
|
|
b0327fd224 |
22
composeApp/src/androidMain/res/values-sv/strings.xml
Normal file
22
composeApp/src/androidMain/res/values-sv/strings.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<!--
|
||||
~ Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU Affero General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU Affero General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="app_name">Spotube</string>
|
||||
<string name="now_playing">Spelas nu</string>
|
||||
<string name="car_connect_hint">Spela musik från telefonen för att börja lyssna i bilen</string>
|
||||
</resources>
|
||||
169
composeApp/src/commonMain/composeResources/values-sv/strings.xml
Normal file
169
composeApp/src/commonMain/composeResources/values-sv/strings.xml
Normal file
@ -0,0 +1,169 @@
|
||||
<!--
|
||||
~ Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU Affero General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU Affero General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="settings_screen_title">Inställningar</string>
|
||||
|
||||
<string name="settings_section_plugins">Tillägg</string>
|
||||
<string name="settings_section_language_region">Språk och region</string>
|
||||
<string name="settings_section_appearance">Utseende</string>
|
||||
<string name="settings_section_playback">Uppspelning</string>
|
||||
<string name="settings_section_downloads">Nedladdningar</string>
|
||||
<string name="settings_section_caching">Cachelagring</string>
|
||||
<string name="settings_section_desktop">Dator</string>
|
||||
<string name="settings_section_updates">Uppdateringar</string>
|
||||
|
||||
<string name="settings_action_close">Stäng</string>
|
||||
<string name="settings_action_save">Spara</string>
|
||||
<string name="settings_action_cancel">Avbryt</string>
|
||||
|
||||
<string name="settings_language_title">Språk</string>
|
||||
<string name="settings_language_subtitle_current">Språk för appens gränssnitt. Nuvarande: %1$s</string>
|
||||
<string name="settings_country_title">Land</string>
|
||||
<string name="settings_country_subtitle_current">Land som styr innehåll och tillgänglighet. Nuvarande: %1$s</string>
|
||||
<string name="settings_option_name_and_code">%1$s (%2$s)</string>
|
||||
|
||||
<string name="settings_theme_title">Tema</string>
|
||||
<string name="settings_theme_subtitle_current">Välj hur Spotube anpassar sig till systemets utseende. Nuvarande: %1$s</string>
|
||||
<string name="settings_theme_light">Ljust</string>
|
||||
<string name="settings_theme_dark">Mörkt</string>
|
||||
<string name="settings_theme_system">System</string>
|
||||
|
||||
<string name="settings_accent_title">Accentfärg</string>
|
||||
<string name="settings_accent_subtitle_current">Nuvarande färgpalett: %1$s</string>
|
||||
<string name="settings_accent_dialog_title">Accentfärg</string>
|
||||
<string name="settings_accent_dialog_description">Välj en accentpalett och förhandsgranska den i både ljust och mörkt tema.</string>
|
||||
<string name="settings_preview_light">Ljust</string>
|
||||
<string name="settings_preview_dark">Mörkt</string>
|
||||
<string name="settings_preview_label">Förhandsgranskning</string>
|
||||
<string name="settings_preview_button">Knapp</string>
|
||||
<string name="settings_accent_green_goblin">Gröna trollet</string>
|
||||
<string name="settings_accent_electric_violet">Elektriskt violett</string>
|
||||
<string name="settings_accent_oceanic_cyan">Havscyan</string>
|
||||
<string name="settings_accent_sunset_orange">Solnedgångsorange</string>
|
||||
<string name="settings_accent_rose_garden">Rosenträdgård</string>
|
||||
<string name="settings_accent_midnight_blue">Midnattsblått</string>
|
||||
<string name="settings_accent_metallic_slate">Metalliskt skiffergrått</string>
|
||||
|
||||
<string name="settings_download_folder_title">Egen nedladdningsmapp</string>
|
||||
<string name="settings_download_folder_current">Nuvarande mapp: %1$s</string>
|
||||
<string name="settings_download_folder_default">Nuvarande mapp: standardmappen för musik</string>
|
||||
<string name="settings_download_folder_description">Lämna fältet tomt för att använda standardmappen för musik.</string>
|
||||
<string name="settings_download_folder_placeholder">C:\Music\Spotube</string>
|
||||
<string name="settings_local_media_folders_title">Lokala mediemappar</string>
|
||||
<string name="settings_local_media_folders_subtitle_current">Egna mappar att skanna på datorn. Tillagda: %1$d</string>
|
||||
<string name="settings_local_media_folders_subtitle_empty">Ingen egen mapp har lagts till ännu</string>
|
||||
<string name="settings_local_media_folders_description">Lägg till mappar med mappväljaren. Den egna nedladdningsmappen tas med automatiskt.</string>
|
||||
<string name="settings_local_media_folders_manage">Hantera</string>
|
||||
<string name="settings_local_media_folders_add_action">Lägg till mapp</string>
|
||||
<string name="settings_local_media_folders_remove_action">Ta bort mapp</string>
|
||||
<string name="settings_local_media_folders_none_added">Inga egna mappar har lagts till.</string>
|
||||
<string name="settings_download_format_title">Musikformat för nedladdning</string>
|
||||
<string name="settings_download_format_subtitle_current">Önskad kodek och önskat behållarformat för nedladdningar. Nuvarande: %1$s</string>
|
||||
<string name="settings_download_quality_title">Musikkvalitet för nedladdning</string>
|
||||
|
||||
<string name="settings_streaming_format_title">Musikformat för strömning</string>
|
||||
<string name="settings_streaming_format_subtitle_current">Önskad kodek och önskat behållarformat för musikströmning. Nuvarande: %1$s</string>
|
||||
<string name="settings_streaming_quality_title">Musikkvalitet för strömning</string>
|
||||
<string name="settings_subtitle_current">Nuvarande: %1$s</string>
|
||||
<string name="settings_enable_music_caching_title">Aktivera cachelagring av musik</string>
|
||||
<string name="settings_enable_music_caching_subtitle">Lagra strömmat ljud lokalt så att det går snabbare att spela upp igen.</string>
|
||||
<string name="settings_cache_folder_title">Cachemapp</string>
|
||||
<string name="settings_cache_folder_current">Nuvarande: %1$s</string>
|
||||
<string name="settings_cache_folder_default">Nuvarande: appens standardcache</string>
|
||||
<string name="settings_cache_size_limit_title">Maximal cachestorlek</string>
|
||||
<string name="settings_cache_size_limit_unlimited">Ingen gräns</string>
|
||||
<string name="settings_cache_size_limit_current">Nuvarande gräns: %1$d MB</string>
|
||||
<string name="settings_cache_size_limit_description">Ange den maximala cachestorleken i MB. Ange 0 eller lämna tomt för obegränsad storlek.</string>
|
||||
<string name="settings_cache_size_limit_placeholder">500</string>
|
||||
<string name="settings_error_cache_size_range">Storleken måste vara 0 eller ett positivt tal.</string>
|
||||
<string name="settings_enable_endless_playback_title">Fortsätt spela automatiskt</string>
|
||||
<string name="settings_enable_endless_playback_subtitle">Fortsätt spela rekommenderade låtar när kön tar slut.</string>
|
||||
<string name="settings_enable_connect_title">Aktivera Connect</string>
|
||||
<string name="settings_enable_connect_subtitle">Gör det möjligt att fjärrstyra uppspelningen via Spotube Connect.</string>
|
||||
<string name="settings_playback_port_title">Port för uppspelningens proxyserver</string>
|
||||
<string name="settings_playback_port_subtitle_current">Port som uppspelningens proxyserver använder. Nuvarande: %1$d</string>
|
||||
<string name="settings_playback_port_description">Välj en port mellan 1 och 65535.</string>
|
||||
<string name="settings_playback_port_placeholder">14769</string>
|
||||
<string name="settings_error_whole_number">Ange ett giltigt heltal.</string>
|
||||
<string name="settings_error_port_range">Porten måste vara mellan 1 och 65535.</string>
|
||||
|
||||
<string name="settings_blacklist_title">Spärrlista</string>
|
||||
<string name="settings_blacklist_subtitle">Hantera spärrade låtar och artister</string>
|
||||
|
||||
<string name="settings_desktop_minimize_title">Minimera till meddelandefältet</string>
|
||||
<string name="settings_desktop_minimize_subtitle">Låt Spotube fortsätta köras i meddelandefältet när fönstret minimeras.</string>
|
||||
<string name="settings_desktop_discord_title">Discord Rich Presence</string>
|
||||
<string name="settings_desktop_discord_subtitle">Visa din aktuella uppspelningsstatus i Discord på datorn.</string>
|
||||
|
||||
<string name="settings_updates_auto_check_title">Sök automatiskt efter uppdateringar</string>
|
||||
<string name="settings_updates_auto_check_subtitle">Sök efter nya versioner av Spotube i bakgrunden.</string>
|
||||
|
||||
<string name="settings_plugins_manage_title">Hantera tillägg</string>
|
||||
<string name="settings_plugins_manage_subtitle">Hantera dina tillägg</string>
|
||||
<string name="settings_plugins_default_ability_title">Standardtillägg: %1$s</string>
|
||||
<string name="settings_plugins_ability_metadata">Metadata</string>
|
||||
<string name="settings_plugins_ability_audio">Ljud</string>
|
||||
<string name="settings_plugins_ability_lyrics">Låttexter</string>
|
||||
<string name="settings_plugins_ability_scrobble">Skrobbling</string>
|
||||
<string name="settings_plugins_no_selection">Inget tillägg valt</string>
|
||||
<string name="settings_plugins_action_change">Ändra</string>
|
||||
<string name="settings_plugins_action_select">Välj</string>
|
||||
<string name="settings_plugins_selected">Valt</string>
|
||||
<string name="settings_plugins_clear">Rensa val</string>
|
||||
<string name="settings_plugins_no_plugins">Inga tillägg är tillgängliga</string>
|
||||
<string name="settings_plugins_plugin_content_description">Tillägget %1$s</string>
|
||||
<string name="settings_plugins_clear_content_description">Rensa</string>
|
||||
|
||||
<string name="plugin_configure_title">Konfigurera</string>
|
||||
<string name="plugin_screen_title">Tilläggshanterare</string>
|
||||
<string name="plugin_error_enter_url">Ange en URL</string>
|
||||
<string name="plugin_error_url_scheme">URL:en måste börja med http:// eller https://</string>
|
||||
<string name="plugin_error_download_failed">Det gick inte att ladda ner tillägget</string>
|
||||
<string name="plugin_empty_title">Inga tillägg har installerats ännu</string>
|
||||
<string name="plugin_empty_subtitle">Klistra in en nedladdningsadress eller välj en fil ovan.</string>
|
||||
<string name="plugin_installed_count">Installerade %2$s: %1$d</string>
|
||||
<string name="plugin_installed_singular">tillägg</string>
|
||||
<string name="plugin_installed_plural">tillägg</string>
|
||||
<string name="plugin_install_section_title">Installera ett tillägg</string>
|
||||
<string name="plugin_url_placeholder">https://example.com/plugin.smplug</string>
|
||||
<string name="plugin_action_download">Ladda ner</string>
|
||||
<string name="plugin_action_install_from_file">Installera från fil (.smplug)</string>
|
||||
<string name="plugin_state_active">Aktivt</string>
|
||||
<string name="plugin_version_label">v%1$s</string>
|
||||
<string name="plugin_state_builtin">Inbyggt</string>
|
||||
<string name="plugin_action_remove">Ta bort tillägg</string>
|
||||
<string name="plugin_action_login">Logga in</string>
|
||||
<string name="plugin_action_logout">Logga ut</string>
|
||||
<string name="plugin_section_url_title">Ladda ner från URL</string>
|
||||
<string name="plugin_section_file_title">Installera från fil</string>
|
||||
<string name="plugin_section_install">Installera</string>
|
||||
|
||||
<string name="plugin_permissions_author_version">• %1$s</string>
|
||||
<string name="plugin_permissions_requested_title">Begärda behörigheter</string>
|
||||
<string name="plugin_permissions_none">Det här tillägget kräver inga särskilda behörigheter.</string>
|
||||
<string name="plugin_permissions_compare_title">Installerat och tillhandahållet</string>
|
||||
<string name="plugin_permissions_installed_label">Installerat</string>
|
||||
<string name="plugin_permissions_supplied_label">Tillhandahållet</string>
|
||||
<string name="plugin_permissions_api_diff">API %1$s → %2$s</string>
|
||||
<string name="plugin_permissions_capability_storage_title">Beständig lagring</string>
|
||||
<string name="plugin_permissions_capability_storage_desc">Läsa och skriva data på disken</string>
|
||||
<string name="plugin_permissions_capability_network_title">Nätverksförfrågningar</string>
|
||||
<string name="plugin_permissions_capability_network_desc">Skicka och ta emot data via internet</string>
|
||||
<string name="plugin_permissions_capability_webview_title">WebView</string>
|
||||
<string name="plugin_permissions_capability_webview_desc">Visa webbinnehåll i appen</string>
|
||||
</resources>
|
||||
@ -230,6 +230,9 @@
|
||||
<string name="player_go_to_album">Go to album</string>
|
||||
<string name="player_sleep_timer">Sleep timer</string>
|
||||
<string name="player_track_details">Track details</string>
|
||||
<string name="player_play_content_description">Play</string>
|
||||
<string name="player_pause_content_description">Pause</string>
|
||||
<string name="player_add_to_queue_content_description">Add to queue</string>
|
||||
|
||||
<string name="update_action_ignore">Ignore</string>
|
||||
<string name="update_action_update">Update</string>
|
||||
@ -246,7 +249,7 @@
|
||||
<string name="welcome_app_name">Spotube</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_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_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>
|
||||
@ -255,12 +258,21 @@
|
||||
<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_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_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_demo_streaming">DEMONSTRATION</string>
|
||||
<string name="welcome_demo_paused">PAUSED</string>
|
||||
<string name="welcome_demo_pause_action">Pause demonstration</string>
|
||||
<string name="welcome_demo_play_action">Play demonstration</string>
|
||||
<string name="welcome_step_welcome">Welcome</string>
|
||||
<string name="welcome_step_support">Support</string>
|
||||
<string name="welcome_step_plugins">Plugins</string>
|
||||
<string name="welcome_step_guide">Guide</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 -->
|
||||
<string name="welcome_brand_tagline">Your music. Your sources. Your rules.</string>
|
||||
|
||||
@ -62,6 +62,11 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.player_add_to_queue_content_description
|
||||
import spotube.composeapp.generated.resources.player_pause_content_description
|
||||
import spotube.composeapp.generated.resources.player_play_content_description
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
@ -107,14 +112,16 @@ fun PlayableCard(
|
||||
) {
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
this@Column.AnimatedVisibility(
|
||||
visible = isHovered,
|
||||
visible = isHovered || (isPlaying && onPlay != null),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
onAddToQueue?.let {
|
||||
// While this collection is playing the play/pause toggle stays
|
||||
// visible and the add-to-queue action is hidden entirely.
|
||||
onAddToQueue?.takeIf { !isPlaying }?.let {
|
||||
MillenniumIconButton(
|
||||
onClick = onAddToQueue,
|
||||
modifier = Modifier.size(34.dp),
|
||||
@ -122,10 +129,11 @@ fun PlayableCard(
|
||||
) {
|
||||
MillenniumIcon(
|
||||
imageVector = Iconsax.IconsaxAddSquare,
|
||||
contentDescription = "Add to queue")
|
||||
}
|
||||
contentDescription = stringResource(
|
||||
Res.string.player_add_to_queue_content_description))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
onPlay?.let {
|
||||
MillenniumIconButton(
|
||||
onClick = onPlay,
|
||||
@ -137,7 +145,12 @@ fun PlayableCard(
|
||||
imageVector = if (isPlaying) {
|
||||
Iconsax.IconsaxPause
|
||||
} else Iconsax.IconsaxPlay,
|
||||
contentDescription = "Play")
|
||||
contentDescription = stringResource(
|
||||
if (isPlaying) {
|
||||
Res.string.player_pause_content_description
|
||||
} else {
|
||||
Res.string.player_play_content_description
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,9 +107,12 @@ fun TextWithShimmer(
|
||||
minLines: Int = 1,
|
||||
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||
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,
|
||||
color = color,
|
||||
color = if (isLoadingSkeleton) Color.Transparent else color,
|
||||
modifier = Modifier.shimmerApply().then(modifier),
|
||||
autoSize = autoSize,
|
||||
fontSize = fontSize,
|
||||
|
||||
@ -26,6 +26,9 @@ import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
|
||||
import org.spotube.millennium.composables.MillenniumSeparator
|
||||
import org.spotube.millennium.composables.MillenniumSegmentedButton
|
||||
import org.spotube.millennium.composables.MillenniumSegmentedButtonDefaults
|
||||
import org.spotube.millennium.composables.MillenniumSegmentedButtonRow
|
||||
import org.spotube.millennium.composables.MillenniumTextField
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
import org.spotube.millennium.theme.millenniumPanelBrush
|
||||
@ -285,29 +288,19 @@ internal fun PluginScreenContent(
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
) {
|
||||
MillenniumButton(
|
||||
MillenniumSegmentedButtonRow(
|
||||
modifier = Modifier.padding(bottom = 12.dp)) {
|
||||
MillenniumSegmentedButton(
|
||||
selected = data.onboardingDiscover,
|
||||
onClick = { viewModel.showOnboardingDiscovery(true) },
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
Text(
|
||||
stringResource(Res.string.welcome_plugins_discover),
|
||||
fontWeight = if (data.onboardingDiscover) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
shape = MillenniumSegmentedButtonDefaults.itemShape(0, 2)) {
|
||||
Text(stringResource(Res.string.welcome_plugins_discover))
|
||||
}
|
||||
MillenniumButton(
|
||||
onClick = {
|
||||
viewModel.showOnboardingDiscovery(
|
||||
false
|
||||
)
|
||||
},
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
Text(
|
||||
stringResource(Res.string.welcome_plugins_sources),
|
||||
fontWeight = if (!data.onboardingDiscover) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
MillenniumSegmentedButton(
|
||||
selected = !data.onboardingDiscover,
|
||||
onClick = { viewModel.showOnboardingDiscovery(false) },
|
||||
shape = MillenniumSegmentedButtonDefaults.itemShape(1, 2)) {
|
||||
Text(stringResource(Res.string.welcome_plugins_sources))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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_lets_go
|
||||
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. */
|
||||
internal val welcomePrimaryLabels: List<StringResource> = listOf(
|
||||
@ -40,6 +44,13 @@ internal val welcomePrimaryLabels: List<StringResource> = listOf(
|
||||
Res.string.welcome_next,
|
||||
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(
|
||||
DonationOption(
|
||||
id = "patreon",
|
||||
|
||||
@ -33,5 +33,4 @@ data class WelcomeUiState(
|
||||
val pageCount: Int,
|
||||
val isLastPage: Boolean,
|
||||
val primaryLabel: StringResource,
|
||||
val donations: List<DonationOption>,
|
||||
val demoPlaying: Boolean = true)
|
||||
val donations: List<DonationOption>)
|
||||
|
||||
@ -20,28 +20,22 @@ package dev.krtirtho.spotube.modules.welcome
|
||||
|
||||
import org.spotube.millennium.composables.MillenniumButton
|
||||
import org.spotube.millennium.composables.MillenniumButtonStyle
|
||||
import org.spotube.millennium.composables.MillenniumCard
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
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 org.spotube.millennium.composables.MillenniumSeparator
|
||||
import org.spotube.millennium.composables.MillenniumTabGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.fillMaxHeight
|
||||
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.size
|
||||
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 com.composeunstyled.Text
|
||||
@ -49,7 +43,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -63,13 +56,15 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.welcome_back
|
||||
import spotube.composeapp.generated.resources.welcome_skip
|
||||
|
||||
/**
|
||||
* First-run Welcome/Onboarding screen. It is a route ([dev.krtirtho.spotube.core.navigation.Routes.Welcome])
|
||||
* rendered inside the app shell. Uses the application's base controls and keeps plugin setup
|
||||
* in-place, with a neutral palette independent of the user's chosen accent.
|
||||
* First-run Welcome/Onboarding screen. It is a route
|
||||
* ([dev.krtirtho.spotube.core.navigation.Routes.Welcome]) rendered inside the app shell.
|
||||
*
|
||||
* 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
|
||||
fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
||||
@ -96,33 +91,51 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
||||
}
|
||||
}
|
||||
})
|
||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
// Central stage (animated page content)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.widthIn(max = 1280.dp)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center) {
|
||||
AnimatedContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
targetState = state.currentPageIndex,
|
||||
transitionSpec = {
|
||||
if (targetState > initialState) {
|
||||
(slideInHorizontally { it / 4 } + fadeIn()) togetherWith
|
||||
(slideOutHorizontally { -it / 4 } + fadeOut())
|
||||
} else {
|
||||
(slideInHorizontally { -it / 4 } + fadeIn()) togetherWith
|
||||
(slideOutHorizontally { it / 4 } + fadeOut())
|
||||
MillenniumCard(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 1040.dp)
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
contentPadding = PaddingValues(0.dp)) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
WelcomeStepSwitcher(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
state = state,
|
||||
viewModel = viewModel)
|
||||
MillenniumSeparator()
|
||||
WelcomeActionBar(
|
||||
state = state,
|
||||
viewModel = viewModel)
|
||||
}
|
||||
},
|
||||
label = "welcome_page") { index ->
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WelcomeStepSwitcher(
|
||||
state: WelcomeUiState,
|
||||
viewModel: WelcomeViewModel,
|
||||
modifier: Modifier = Modifier) {
|
||||
val labels = welcomeStepLabels.map { stringResource(it) }
|
||||
MillenniumTabGroup(
|
||||
selectedTab = state.currentPageIndex,
|
||||
onSelectedTabChange = viewModel::goToPage,
|
||||
tabs = labels.indices.toList(),
|
||||
modifier = modifier,
|
||||
tabLabel = { labels[it] }) { index ->
|
||||
when (index) {
|
||||
0 -> BrandingPage()
|
||||
|
||||
@ -132,44 +145,21 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
||||
|
||||
2 -> PluginsPage()
|
||||
|
||||
else -> HowItWorksPage(
|
||||
isPlaying = state.demoPlaying,
|
||||
onTogglePlayback = viewModel::toggleDemo
|
||||
)
|
||||
}
|
||||
else -> HowItWorksPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
// Bottom precision navigation bar: Back | Page Dots | Action Button
|
||||
@Composable
|
||||
private fun WelcomeActionBar(
|
||||
state: WelcomeUiState,
|
||||
viewModel: WelcomeViewModel) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 680.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 12.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
|
||||
Spacer(Modifier.weight(1f))
|
||||
MillenniumButton(
|
||||
onClick = viewModel::onPrimary,
|
||||
style = MillenniumButtonStyle.Secondary) {
|
||||
@ -190,39 +180,4 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageIndicator(
|
||||
pageCount: Int,
|
||||
currentPageIndex: Int,
|
||||
modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
repeat(pageCount) { index ->
|
||||
val selected = index == currentPageIndex
|
||||
val width by animateDpAsState(
|
||||
targetValue = if (selected) 20.dp else 6.dp,
|
||||
label = "welcome_page_indicator")
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = width, height = 6.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (selected) {
|
||||
AppTheme.colors.onSurface
|
||||
} else {
|
||||
AppTheme.colors.outlineVariant
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@ -30,18 +30,16 @@ import kotlinx.coroutines.launch
|
||||
class WelcomeViewModel(
|
||||
private val repository: WelcomeRepository) : ViewModel() {
|
||||
private val currentPageIndex = MutableStateFlow(0)
|
||||
private val demoPlaying = MutableStateFlow(true)
|
||||
|
||||
val uiState: StateFlow<WelcomeUiState> =
|
||||
combine(currentPageIndex, demoPlaying) { index, playing ->
|
||||
currentPageIndex.map { index ->
|
||||
val pageCount = welcomePrimaryLabels.size
|
||||
WelcomeUiState(
|
||||
currentPageIndex = index,
|
||||
pageCount = pageCount,
|
||||
isLastPage = index == pageCount - 1,
|
||||
primaryLabel = welcomePrimaryLabels[index.coerceIn(0, pageCount - 1)],
|
||||
donations = welcomeDonations,
|
||||
demoPlaying = playing)
|
||||
donations = welcomeDonations)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
@ -53,16 +51,16 @@ class WelcomeViewModel(
|
||||
primaryLabel = welcomePrimaryLabels.first(),
|
||||
donations = welcomeDonations))
|
||||
|
||||
fun nextPage() {
|
||||
currentPageIndex.update { (it + 1).coerceAtMost(welcomePrimaryLabels.lastIndex) }
|
||||
}
|
||||
|
||||
fun toggleDemo() {
|
||||
demoPlaying.update { !it }
|
||||
fun goToPage(index: Int) {
|
||||
currentPageIndex.update { index.coerceIn(0, welcomePrimaryLabels.lastIndex) }
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -71,10 +69,6 @@ class WelcomeViewModel(
|
||||
runCatching { dev.krtirtho.spotube.openUrlInBrowser(url) }
|
||||
}
|
||||
|
||||
fun previousPage() {
|
||||
currentPageIndex.update { (it - 1).coerceAtLeast(0) }
|
||||
}
|
||||
|
||||
fun completeOnboarding() {
|
||||
viewModelScope.launch {
|
||||
repository.completeOnboarding()
|
||||
|
||||
@ -33,13 +33,17 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import org.spotube.millennium.theme.MillenniumTheme
|
||||
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
|
||||
fun WelcomeTheme(content: @Composable () -> Unit) {
|
||||
val dark = AppTheme.colors.surface.luminance() < 0.5f
|
||||
MillenniumTheme(dark = dark) {
|
||||
content()
|
||||
}
|
||||
MillenniumTheme(
|
||||
dark = dark,
|
||||
config = MillenniumTheme.config,
|
||||
content = content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@ -1,187 +1,98 @@
|
||||
package dev.krtirtho.spotube.modules.welcome.pages
|
||||
|
||||
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import com.composeunstyled.currentWindowWidthBreakpoint
|
||||
import org.spotube.millennium.adaptive.WindowBreakpoints
|
||||
import org.spotube.millennium.composables.MillenniumCard
|
||||
import org.spotube.millennium.composables.MillenniumIcon
|
||||
import org.spotube.millennium.composables.MillenniumIconButton
|
||||
import org.spotube.millennium.composables.MillenniumIconButtonStyle
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import org.spotube.millennium.composables.MillenniumListTile
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
||||
import dev.krtirtho.spotube.core.ui.theming.AppTheme
|
||||
import com.composeunstyled.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
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 spotube.composeapp.generated.resources.*
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.welcome_guide_lyrics_desc
|
||||
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
|
||||
fun HowItWorksPage(
|
||||
isPlaying: Boolean,
|
||||
onTogglePlayback: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(modifier.fillMaxSize()) {
|
||||
val wide = currentWindowWidthBreakpoint() isAtLeast WindowBreakpoints.Expanded
|
||||
if (wide) {
|
||||
Row(
|
||||
Modifier.fillMaxSize().padding(40.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
PlayerDemonstration(isPlaying, onTogglePlayback, Modifier.weight(1f))
|
||||
ListeningIntroduction(Modifier.weight(1f))
|
||||
}
|
||||
} else {
|
||||
fun HowItWorksPage(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically)) {
|
||||
ListeningIntroduction()
|
||||
PlayerDemonstration(isPlaying, onTogglePlayback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListeningIntroduction(modifier: Modifier = Modifier) {
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(28.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
stringResource(Res.string.welcome_how_quiet_title),
|
||||
style = AppTheme.typography.headlineLarge.copy(fontFamily = FontFamily.Serif))
|
||||
stringResource(Res.string.welcome_guide_title),
|
||||
style = AppTheme.typography.headlineMedium.copy(fontFamily = FontFamily.Serif))
|
||||
Text(
|
||||
stringResource(Res.string.welcome_how_quiet_description),
|
||||
stringResource(Res.string.welcome_guide_subtitle),
|
||||
style = AppTheme.typography.bodyLarge,
|
||||
color = AppTheme.colors.onSurfaceVariant)
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
WelcomeGuideStep(
|
||||
icon = Iconsax.User,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Silent, illustrative player. The ViewModel owns the play/pause interaction. */
|
||||
@Composable
|
||||
private fun PlayerDemonstration(
|
||||
isPlaying: Boolean,
|
||||
onTogglePlayback: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
private fun WelcomeGuideStep(
|
||||
icon: ImageVector,
|
||||
title: StringResource,
|
||||
description: StringResource,
|
||||
) {
|
||||
val motion = rememberInfiniteTransition(label = "welcome_record")
|
||||
val rotation by motion.animateFloat(
|
||||
0f, 360f,
|
||||
infiniteRepeatable(tween(12000, easing = LinearEasing)), label = "record_rotation"
|
||||
)
|
||||
val phase by motion.animateFloat(
|
||||
0f, (2 * PI).toFloat(),
|
||||
infiniteRepeatable(tween(2400, easing = LinearEasing)), label = "audio_meter"
|
||||
)
|
||||
val scheme = AppTheme.colors
|
||||
|
||||
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
|
||||
) {
|
||||
MillenniumListTile(
|
||||
leading = {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = AppTheme.colors.primary)
|
||||
},
|
||||
title = { Text(stringResource(title)) },
|
||||
subtitle = { Text(stringResource(description)) })
|
||||
}
|
||||
|
||||
@ -25,10 +25,13 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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 org.spotube.millennium.composables.MillenniumTopAppBar
|
||||
@ -53,6 +56,8 @@ import dev.nucleusframework.window.WindowDoubleClickAction
|
||||
import dev.nucleusframework.window.windowDragArea
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
private val WindowControlsReservedWidth = 132.dp
|
||||
|
||||
@Composable
|
||||
actual fun ApplicationMainBar(
|
||||
title: @Composable (() -> Unit),
|
||||
@ -68,6 +73,9 @@ actual fun ApplicationMainBar(
|
||||
title = title,
|
||||
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 = {
|
||||
if (backButton) {
|
||||
@ -75,7 +83,9 @@ actual fun ApplicationMainBar(
|
||||
}
|
||||
},
|
||||
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(
|
||||
modifier = Modifier.align(Alignment.TopEnd).heightIn(max = 48.dp),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user