Phase 2: Spotify/SoundCloud-style theme, nav shell, and component library
Redirects Deepwave from a vanilla Subsonic client toward the Spotify/ SoundCloud-style product the user wants, per the Phase 2+ roadmap. - Dark-first Material3 theme with a fixed aqua accent (no Material You dynamic color), full type scale, rounded-corner shape system - Typed (@Serializable) navigation routes; new bottom-tab shell (Home/Search/Library) with a settings entry point and mini-player placeholder bar, replacing the single placeholder Home screen - Logout moved out of Home into a dedicated Settings screen - Reusable component library (TrackRow, AlbumCard, ArtistCard, ArtworkPlaceholder, BottomNavBar, MiniPlayerBar) for Phase 3 to wire real data into - Strings externalized to strings.xml - Security fix: exclude the Tink keyset and encrypted credentials DataStore from Android auto-backup (backup_rules.xml / data_extraction_rules.xml), previously unexcluded despite allowBackup=true - Fix: Home screen's content column was missing verticalScroll, making it unscrollable once content exceeds the viewport; also added breathing room between each row's header and its cards - Split Login/Settings into stateless content composables + thin ViewModel-wired wrappers, and added @Preview coverage (including interactive previews for MainScreen and BottomNavBar) across every screen and component so the UI can be reviewed in Android Studio's preview pane without running on a device Added material-icons-extended as a pragmatic deviation from the Phase 2 plan (no new deps) since later phases all need icons outside Compose's small default set (play/pause/skip/download/playlist). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1971d02e2b
commit
a07868b688
Generated
+3
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+123
@@ -0,0 +1,123 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="25" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="jbr-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -43,6 +43,7 @@ dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.InfernalAquatics.deepwave.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
private val AlbumCardWidth = 140.dp
|
||||
|
||||
@Composable
|
||||
fun AlbumCard(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(AlbumCardWidth)
|
||||
.clickable(onClick = onClick),
|
||||
) {
|
||||
ArtworkPlaceholder(size = AlbumCardWidth)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AlbumCardPreview() {
|
||||
DeepwaveTheme {
|
||||
AlbumCard(title = "Sample Album", subtitle = "Sample Artist")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.InfernalAquatics.deepwave.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
private val ArtistCardWidth = 110.dp
|
||||
|
||||
@Composable
|
||||
fun ArtistCard(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(ArtistCardWidth)
|
||||
.clickable(onClick = onClick),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
ArtworkPlaceholder(size = ArtistCardWidth, circular = true)
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ArtistCardPreview() {
|
||||
DeepwaveTheme {
|
||||
ArtistCard(name = "Sample Artist")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.InfernalAquatics.deepwave.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MusicNote
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
/** Cover-art placeholder shown until Phase 3 wires in real artwork via Coil. */
|
||||
@Composable
|
||||
fun ArtworkPlaceholder(
|
||||
size: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
circular: Boolean = false,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(if (circular) CircleShape else RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (circular) Icons.Filled.Person else Icons.Filled.MusicNote,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ArtworkPlaceholderPreview() {
|
||||
DeepwaveTheme {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ArtworkPlaceholder(size = 96.dp)
|
||||
ArtworkPlaceholder(size = 96.dp, circular = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.InfernalAquatics.deepwave.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.LibraryMusic
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
enum class BottomTab(val labelRes: Int, val icon: ImageVector) {
|
||||
Home(R.string.nav_home, Icons.Filled.Home),
|
||||
Search(R.string.nav_search, Icons.Filled.Search),
|
||||
Library(R.string.nav_library, Icons.Filled.LibraryMusic),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(selectedTab: BottomTab, onTabSelected: (BottomTab) -> Unit) {
|
||||
NavigationBar {
|
||||
BottomTab.entries.forEach { tab ->
|
||||
val label = stringResource(tab.labelRes)
|
||||
NavigationBarItem(
|
||||
selected = tab == selectedTab,
|
||||
onClick = { onTabSelected(tab) },
|
||||
icon = { Icon(tab.icon, contentDescription = label) },
|
||||
label = { Text(label) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Interactive in Android Studio's preview pane — tap a tab to see the selection change. */
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun BottomNavBarPreview() {
|
||||
DeepwaveTheme {
|
||||
var selected by remember { mutableStateOf(BottomTab.Home) }
|
||||
BottomNavBar(selectedTab = selected, onTabSelected = { selected = it })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.InfernalAquatics.deepwave.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
/** Placeholder now-playing bar; wired to real PlaybackController state in Phase 4. */
|
||||
@Composable
|
||||
fun MiniPlayerBar(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ArtworkPlaceholder(size = 40.dp)
|
||||
Text(
|
||||
text = stringResource(R.string.player_nothing_playing),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 12.dp),
|
||||
)
|
||||
IconButton(onClick = {}, enabled = false) {
|
||||
Icon(Icons.Filled.PlayArrow, contentDescription = stringResource(R.string.player_play))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun MiniPlayerBarPreview() {
|
||||
DeepwaveTheme {
|
||||
MiniPlayerBar()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.InfernalAquatics.deepwave.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
@Composable
|
||||
fun TrackRow(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
trailingContent: (@Composable RowScope.() -> Unit)? = null,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ArtworkPlaceholder(size = 48.dp)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
trailingContent?.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TrackRowPreview() {
|
||||
DeepwaveTheme {
|
||||
TrackRow(title = "Sample Track", subtitle = "Sample Artist")
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,67 @@ package com.InfernalAquatics.deepwave.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.components.AlbumCard
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
/** Placeholder post-login landing screen; replaced by the library browse UI in Phase 2. */
|
||||
/** Sample content demonstrating the design system; replaced by real library data in Phase 3. */
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
onLoggedOut: () -> Unit,
|
||||
viewModel: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
fun HomeScreen(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Text(text = "You're connected", style = MaterialTheme.typography.headlineSmall)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = { viewModel.logout(onLoggedOut) }) {
|
||||
Text("Log out")
|
||||
HomeRow(title = stringResource(R.string.home_row_recently_added))
|
||||
HomeRow(title = stringResource(R.string.home_row_recently_played))
|
||||
HomeRow(title = stringResource(R.string.home_row_made_for_you))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HomeRow(title: String) {
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(5) { index ->
|
||||
AlbumCard(title = "Sample Album ${index + 1}", subtitle = "Sample Artist")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun HomeScreenPreview() {
|
||||
DeepwaveTheme {
|
||||
HomeScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.InfernalAquatics.deepwave.ui.library
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
/** Placeholder; real library browsing (artists/albums) arrives in Phase 3. */
|
||||
@Composable
|
||||
fun LibraryScreen(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = stringResource(R.string.library_placeholder), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun LibraryScreenPreview() {
|
||||
DeepwaveTheme {
|
||||
LibraryScreen()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.InfernalAquatics.deepwave.ui.login
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -19,10 +20,14 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
@@ -35,6 +40,23 @@ fun LoginScreen(
|
||||
if (uiState.loginSucceeded) onLoginSuccess()
|
||||
}
|
||||
|
||||
LoginContent(
|
||||
uiState = uiState,
|
||||
onServerUrlChange = viewModel::onServerUrlChange,
|
||||
onUsernameChange = viewModel::onUsernameChange,
|
||||
onPasswordChange = viewModel::onPasswordChange,
|
||||
onLoginClick = viewModel::login,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoginContent(
|
||||
uiState: LoginUiState,
|
||||
onServerUrlChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onLoginClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -42,16 +64,16 @@ fun LoginScreen(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = "Deepwave", style = MaterialTheme.typography.headlineMedium)
|
||||
Text(text = "Connect to your Navidrome server", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = stringResource(R.string.login_title), style = MaterialTheme.typography.headlineMedium)
|
||||
Text(text = stringResource(R.string.login_subtitle), style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = viewModel::onServerUrlChange,
|
||||
label = { Text("Server URL") },
|
||||
placeholder = { Text("https://navidrome.example.com") },
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text(stringResource(R.string.login_server_url_label)) },
|
||||
placeholder = { Text(stringResource(R.string.login_server_url_placeholder)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -61,8 +83,8 @@ fun LoginScreen(
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = viewModel::onUsernameChange,
|
||||
label = { Text("Username") },
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text(stringResource(R.string.login_username_label)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -71,8 +93,8 @@ fun LoginScreen(
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = viewModel::onPasswordChange,
|
||||
label = { Text("Password") },
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text(stringResource(R.string.login_password_label)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
@@ -93,9 +115,59 @@ fun LoginScreen(
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
Button(onClick = viewModel::login, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Log in")
|
||||
Button(onClick = onLoginClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.login_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Empty", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun LoginContentEmptyPreview() {
|
||||
DeepwaveTheme {
|
||||
LoginContent(
|
||||
uiState = LoginUiState(),
|
||||
onServerUrlChange = {},
|
||||
onUsernameChange = {},
|
||||
onPasswordChange = {},
|
||||
onLoginClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Loading", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun LoginContentLoadingPreview() {
|
||||
DeepwaveTheme {
|
||||
LoginContent(
|
||||
uiState = LoginUiState(
|
||||
serverUrl = "https://navidrome.example.com",
|
||||
username = "alex",
|
||||
isLoading = true,
|
||||
),
|
||||
onServerUrlChange = {},
|
||||
onUsernameChange = {},
|
||||
onPasswordChange = {},
|
||||
onLoginClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Error", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun LoginContentErrorPreview() {
|
||||
DeepwaveTheme {
|
||||
LoginContent(
|
||||
uiState = LoginUiState(
|
||||
serverUrl = "https://navidrome.example.com",
|
||||
username = "alex",
|
||||
errorMessage = "Couldn't reach server: timeout",
|
||||
),
|
||||
onServerUrlChange = {},
|
||||
onUsernameChange = {},
|
||||
onPasswordChange = {},
|
||||
onLoginClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,8 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.InfernalAquatics.deepwave.ui.home.HomeScreen
|
||||
import com.InfernalAquatics.deepwave.ui.login.LoginScreen
|
||||
|
||||
private object Routes {
|
||||
const val LOGIN = "login"
|
||||
const val HOME = "home"
|
||||
}
|
||||
import com.InfernalAquatics.deepwave.ui.settings.SettingsScreen
|
||||
|
||||
@Composable
|
||||
fun DeepwaveNavHost(
|
||||
@@ -34,22 +29,26 @@ fun DeepwaveNavHost(
|
||||
}
|
||||
else -> NavHost(
|
||||
navController = navController,
|
||||
startDestination = if (isLoggedIn == true) Routes.HOME else Routes.LOGIN,
|
||||
startDestination = if (isLoggedIn == true) Route.Main else Route.Login,
|
||||
) {
|
||||
composable(Routes.LOGIN) {
|
||||
composable<Route.Login> {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
navController.navigate(Routes.HOME) {
|
||||
popUpTo(Routes.LOGIN) { inclusive = true }
|
||||
navController.navigate(Route.Main) {
|
||||
popUpTo<Route.Login> { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(Routes.HOME) {
|
||||
HomeScreen(
|
||||
composable<Route.Main> {
|
||||
MainScreen(onOpenSettings = { navController.navigate(Route.Settings) })
|
||||
}
|
||||
composable<Route.Settings> {
|
||||
SettingsScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
onLoggedOut = {
|
||||
navController.navigate(Routes.LOGIN) {
|
||||
popUpTo(Routes.HOME) { inclusive = true }
|
||||
navController.navigate(Route.Login) {
|
||||
popUpTo<Route.Main> { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.InfernalAquatics.deepwave.ui.navigation
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.components.BottomNavBar
|
||||
import com.InfernalAquatics.deepwave.ui.components.BottomTab
|
||||
import com.InfernalAquatics.deepwave.ui.components.MiniPlayerBar
|
||||
import com.InfernalAquatics.deepwave.ui.home.HomeScreen
|
||||
import com.InfernalAquatics.deepwave.ui.library.LibraryScreen
|
||||
import com.InfernalAquatics.deepwave.ui.search.SearchScreen
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
/** Hosts the bottom-tab shell (Home / Search / Library) plus the mini-player bar and settings entry point. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(onOpenSettings: () -> Unit) {
|
||||
var selectedTab by remember { mutableStateOf(BottomTab.Home) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
actions = {
|
||||
IconButton(onClick = onOpenSettings) {
|
||||
Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.action_settings))
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Column {
|
||||
MiniPlayerBar()
|
||||
BottomNavBar(selectedTab = selectedTab, onTabSelected = { selectedTab = it })
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
) {
|
||||
when (selectedTab) {
|
||||
BottomTab.Home -> HomeScreen()
|
||||
BottomTab.Search -> SearchScreen()
|
||||
BottomTab.Library -> LibraryScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Interactive in Android Studio's preview pane — try tapping the tabs and the settings icon. */
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun MainScreenPreview() {
|
||||
DeepwaveTheme {
|
||||
MainScreen(onOpenSettings = {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.InfernalAquatics.deepwave.ui.navigation
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface Route {
|
||||
@Serializable data object Login : Route
|
||||
@Serializable data object Main : Route
|
||||
@Serializable data object Settings : Route
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.InfernalAquatics.deepwave.ui.search
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
/** Placeholder; real search UI arrives in Phase 3. */
|
||||
@Composable
|
||||
fun SearchScreen(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = stringResource(R.string.search_placeholder), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun SearchScreenPreview() {
|
||||
DeepwaveTheme {
|
||||
SearchScreen()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.InfernalAquatics.deepwave.ui.settings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.InfernalAquatics.deepwave.R
|
||||
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onBack: () -> Unit,
|
||||
onLoggedOut: () -> Unit,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
) {
|
||||
SettingsContent(
|
||||
onBack = onBack,
|
||||
onLogOutClick = { viewModel.logout(onLoggedOut) },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SettingsContent(onBack: () -> Unit, onLogOutClick: () -> Unit) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Button(onClick = onLogOutClick) {
|
||||
Text(stringResource(R.string.settings_log_out))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 411, heightDp = 891)
|
||||
@Composable
|
||||
private fun SettingsContentPreview() {
|
||||
DeepwaveTheme {
|
||||
SettingsContent(onBack = {}, onLogOutClick = {})
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.InfernalAquatics.deepwave.ui.home
|
||||
package com.InfernalAquatics.deepwave.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val serverRepository: ServerRepository,
|
||||
) : ViewModel() {
|
||||
fun logout(onLoggedOut: () -> Unit) {
|
||||
@@ -2,10 +2,27 @@ package com.InfernalAquatics.deepwave.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
// Dark palette (primary) — near-black "deep water" surfaces with an aqua accent.
|
||||
val DeepwaveBackgroundDark = Color(0xFF0B0F14)
|
||||
val DeepwaveSurfaceDark = Color(0xFF12181F)
|
||||
val DeepwaveSurfaceVariantDark = Color(0xFF1E2830)
|
||||
val DeepwaveOnBackgroundDark = Color(0xFFECEFF2)
|
||||
val DeepwaveOnSurfaceVariantDark = Color(0xFFA9B4BD)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val WaveAqua = Color(0xFF2FE0C7)
|
||||
val WaveAquaDeep = Color(0xFF0F8C7A)
|
||||
val OnWaveAqua = Color(0xFF00332C)
|
||||
|
||||
val DeepwaveSecondaryDark = Color(0xFF4A6572)
|
||||
val OnDeepwaveSecondaryDark = Color(0xFFE3E9EC)
|
||||
|
||||
val DeepwaveError = Color(0xFFFF5449)
|
||||
val OnDeepwaveError = Color(0xFF1A0000)
|
||||
|
||||
// Light palette (secondary, system-light-mode fallback)
|
||||
val DeepwaveBackgroundLight = Color(0xFFF7F9FA)
|
||||
val DeepwaveSurfaceLight = Color(0xFFFFFFFF)
|
||||
val DeepwaveSurfaceVariantLight = Color(0xFFE3E8EA)
|
||||
val DeepwaveOnBackgroundLight = Color(0xFF12181F)
|
||||
val DeepwaveOnSurfaceVariantLight = Color(0xFF44505A)
|
||||
val DeepwaveSecondaryLight = Color(0xFF3E5461)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.InfernalAquatics.deepwave.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes = Shapes(
|
||||
extraSmall = RoundedCornerShape(4.dp),
|
||||
small = RoundedCornerShape(8.dp),
|
||||
medium = RoundedCornerShape(12.dp),
|
||||
large = RoundedCornerShape(16.dp),
|
||||
extraLarge = RoundedCornerShape(28.dp),
|
||||
)
|
||||
@@ -1,58 +1,61 @@
|
||||
package com.InfernalAquatics.deepwave.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
primary = WaveAqua,
|
||||
onPrimary = OnWaveAqua,
|
||||
primaryContainer = WaveAquaDeep,
|
||||
onPrimaryContainer = DeepwaveOnBackgroundDark,
|
||||
secondary = DeepwaveSecondaryDark,
|
||||
onSecondary = OnDeepwaveSecondaryDark,
|
||||
background = DeepwaveBackgroundDark,
|
||||
onBackground = DeepwaveOnBackgroundDark,
|
||||
surface = DeepwaveSurfaceDark,
|
||||
onSurface = DeepwaveOnBackgroundDark,
|
||||
surfaceVariant = DeepwaveSurfaceVariantDark,
|
||||
onSurfaceVariant = DeepwaveOnSurfaceVariantDark,
|
||||
error = DeepwaveError,
|
||||
onError = OnDeepwaveError,
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
primary = WaveAquaDeep,
|
||||
onPrimary = DeepwaveSurfaceLight,
|
||||
primaryContainer = WaveAqua,
|
||||
onPrimaryContainer = OnWaveAqua,
|
||||
secondary = DeepwaveSecondaryLight,
|
||||
onSecondary = DeepwaveSurfaceLight,
|
||||
background = DeepwaveBackgroundLight,
|
||||
onBackground = DeepwaveOnBackgroundLight,
|
||||
surface = DeepwaveSurfaceLight,
|
||||
onSurface = DeepwaveOnBackgroundLight,
|
||||
surfaceVariant = DeepwaveSurfaceVariantLight,
|
||||
onSurfaceVariant = DeepwaveOnSurfaceVariantLight,
|
||||
error = DeepwaveError,
|
||||
onError = OnDeepwaveError,
|
||||
)
|
||||
|
||||
/**
|
||||
* Deepwave's brand theme: dark-first with a fixed aqua accent. Deliberately not
|
||||
* Material You dynamic color, so the app keeps a consistent identity regardless
|
||||
* of the user's wallpaper, matching the Spotify/SoundCloud-style brief.
|
||||
*/
|
||||
@Composable
|
||||
fun DeepwaveTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
shapes = Shapes,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,29 +6,20 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
displayLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp),
|
||||
displayMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp),
|
||||
displaySmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp),
|
||||
headlineLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp),
|
||||
headlineMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp),
|
||||
headlineSmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp),
|
||||
titleLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp),
|
||||
titleMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp),
|
||||
titleSmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp),
|
||||
bodyLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp),
|
||||
bodyMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp),
|
||||
bodySmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp),
|
||||
labelLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp),
|
||||
labelMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp),
|
||||
labelSmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
<resources>
|
||||
<string name="app_name">Deepwave</string>
|
||||
</resources>
|
||||
|
||||
<string name="login_title">Deepwave</string>
|
||||
<string name="login_subtitle">Connect to your Navidrome server</string>
|
||||
<string name="login_server_url_label">Server URL</string>
|
||||
<string name="login_server_url_placeholder">https://navidrome.example.com</string>
|
||||
<string name="login_username_label">Username</string>
|
||||
<string name="login_password_label">Password</string>
|
||||
<string name="login_button">Log in</string>
|
||||
|
||||
<string name="nav_home">Home</string>
|
||||
<string name="nav_search">Search</string>
|
||||
<string name="nav_library">Library</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_back">Back</string>
|
||||
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_log_out">Log out</string>
|
||||
|
||||
<string name="search_placeholder">Search coming soon</string>
|
||||
<string name="library_placeholder">Your library coming soon</string>
|
||||
|
||||
<string name="player_nothing_playing">Nothing playing</string>
|
||||
<string name="player_play">Play</string>
|
||||
|
||||
<string name="home_row_recently_added">Recently Added</string>
|
||||
<string name="home_row_recently_played">Recently Played</string>
|
||||
<string name="home_row_made_for_you">Made For You</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
<exclude domain="sharedpref" path="deepwave_master_keyset_prefs.xml"/>
|
||||
<exclude domain="file" path="datastore/deepwave_credentials.preferences_pb"/>
|
||||
</full-backup-content>
|
||||
@@ -5,15 +5,11 @@
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
<exclude domain="sharedpref" path="deepwave_master_keyset_prefs.xml"/>
|
||||
<exclude domain="file" path="datastore/deepwave_credentials.preferences_pb"/>
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
<exclude domain="sharedpref" path="deepwave_master_keyset_prefs.xml"/>
|
||||
<exclude domain="file" path="datastore/deepwave_credentials.preferences_pb"/>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -34,6 +34,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltAndroid" }
|
||||
|
||||
Reference in New Issue
Block a user