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:
christopher
2026-09-17 03:37:58 -04:00
co-authored by Claude Sonnet 5
parent 1971d02e2b
commit a07868b688
35 changed files with 1025 additions and 122 deletions
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+6
View File
@@ -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>
+123
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="25" />
</component>
</project>
+11
View File
@@ -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>
+17
View File
@@ -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>
+10
View File
@@ -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>
+17
View File
@@ -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
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+1
View File
@@ -43,6 +43,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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 @Composable
fun HomeScreen( fun HomeScreen(modifier: Modifier = Modifier) {
onLoggedOut: () -> Unit,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(24.dp), .verticalScroll(rememberScrollState())
verticalArrangement = Arrangement.Center, .padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
Text(text = "You're connected", style = MaterialTheme.typography.headlineSmall) HomeRow(title = stringResource(R.string.home_row_recently_added))
Spacer(modifier = Modifier.height(24.dp)) HomeRow(title = stringResource(R.string.home_row_recently_played))
Button(onClick = { viewModel.logout(onLoggedOut) }) { HomeRow(title = stringResource(R.string.home_row_made_for_you))
Text("Log out") }
}
@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 package com.InfernalAquatics.deepwave.ui.login
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -19,10 +20,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.InfernalAquatics.deepwave.R
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
@Composable @Composable
fun LoginScreen( fun LoginScreen(
@@ -35,6 +40,23 @@ fun LoginScreen(
if (uiState.loginSucceeded) onLoginSuccess() 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -42,16 +64,16 @@ fun LoginScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text(text = "Deepwave", style = MaterialTheme.typography.headlineMedium) Text(text = stringResource(R.string.login_title), style = MaterialTheme.typography.headlineMedium)
Text(text = "Connect to your Navidrome server", style = MaterialTheme.typography.bodyMedium) Text(text = stringResource(R.string.login_subtitle), style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
OutlinedTextField( OutlinedTextField(
value = uiState.serverUrl, value = uiState.serverUrl,
onValueChange = viewModel::onServerUrlChange, onValueChange = onServerUrlChange,
label = { Text("Server URL") }, label = { Text(stringResource(R.string.login_server_url_label)) },
placeholder = { Text("https://navidrome.example.com") }, placeholder = { Text(stringResource(R.string.login_server_url_placeholder)) },
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -61,8 +83,8 @@ fun LoginScreen(
OutlinedTextField( OutlinedTextField(
value = uiState.username, value = uiState.username,
onValueChange = viewModel::onUsernameChange, onValueChange = onUsernameChange,
label = { Text("Username") }, label = { Text(stringResource(R.string.login_username_label)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@@ -71,8 +93,8 @@ fun LoginScreen(
OutlinedTextField( OutlinedTextField(
value = uiState.password, value = uiState.password,
onValueChange = viewModel::onPasswordChange, onValueChange = onPasswordChange,
label = { Text("Password") }, label = { Text(stringResource(R.string.login_password_label)) },
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
@@ -93,9 +115,59 @@ fun LoginScreen(
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator() CircularProgressIndicator()
} else { } else {
Button(onClick = viewModel::login, modifier = Modifier.fillMaxWidth()) { Button(onClick = onLoginClick, modifier = Modifier.fillMaxWidth()) {
Text("Log in") 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.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.InfernalAquatics.deepwave.ui.home.HomeScreen
import com.InfernalAquatics.deepwave.ui.login.LoginScreen import com.InfernalAquatics.deepwave.ui.login.LoginScreen
import com.InfernalAquatics.deepwave.ui.settings.SettingsScreen
private object Routes {
const val LOGIN = "login"
const val HOME = "home"
}
@Composable @Composable
fun DeepwaveNavHost( fun DeepwaveNavHost(
@@ -34,22 +29,26 @@ fun DeepwaveNavHost(
} }
else -> NavHost( else -> NavHost(
navController = navController, 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( LoginScreen(
onLoginSuccess = { onLoginSuccess = {
navController.navigate(Routes.HOME) { navController.navigate(Route.Main) {
popUpTo(Routes.LOGIN) { inclusive = true } popUpTo<Route.Login> { inclusive = true }
} }
}, },
) )
} }
composable(Routes.HOME) { composable<Route.Main> {
HomeScreen( MainScreen(onOpenSettings = { navController.navigate(Route.Settings) })
}
composable<Route.Settings> {
SettingsScreen(
onBack = { navController.popBackStack() },
onLoggedOut = { onLoggedOut = {
navController.navigate(Routes.LOGIN) { navController.navigate(Route.Login) {
popUpTo(Routes.HOME) { inclusive = true } 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 = {})
}
}
@@ -1,4 +1,4 @@
package com.InfernalAquatics.deepwave.ui.home package com.InfernalAquatics.deepwave.ui.settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -8,7 +8,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
) : ViewModel() { ) : ViewModel() {
fun logout(onLoggedOut: () -> Unit) { fun logout(onLoggedOut: () -> Unit) {
@@ -2,10 +2,27 @@ package com.InfernalAquatics.deepwave.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) // Dark palette (primary) — near-black "deep water" surfaces with an aqua accent.
val PurpleGrey80 = Color(0xFFCCC2DC) val DeepwaveBackgroundDark = Color(0xFF0B0F14)
val Pink80 = Color(0xFFEFB8C8) val DeepwaveSurfaceDark = Color(0xFF12181F)
val DeepwaveSurfaceVariantDark = Color(0xFF1E2830)
val DeepwaveOnBackgroundDark = Color(0xFFECEFF2)
val DeepwaveOnSurfaceVariantDark = Color(0xFFA9B4BD)
val Purple40 = Color(0xFF6650a4) val WaveAqua = Color(0xFF2FE0C7)
val PurpleGrey40 = Color(0xFF625b71) val WaveAquaDeep = Color(0xFF0F8C7A)
val Pink40 = Color(0xFF7D5260) 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 package com.InfernalAquatics.deepwave.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = WaveAqua,
secondary = PurpleGrey80, onPrimary = OnWaveAqua,
tertiary = Pink80 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( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = WaveAquaDeep,
secondary = PurpleGrey40, onPrimary = DeepwaveSurfaceLight,
tertiary = Pink40 primaryContainer = WaveAqua,
onPrimaryContainer = OnWaveAqua,
/* Other default colors to override secondary = DeepwaveSecondaryLight,
background = Color(0xFFFFFBFE), onSecondary = DeepwaveSurfaceLight,
surface = Color(0xFFFFFBFE), background = DeepwaveBackgroundLight,
onPrimary = Color.White, onBackground = DeepwaveOnBackgroundLight,
onSecondary = Color.White, surface = DeepwaveSurfaceLight,
onTertiary = Color.White, onSurface = DeepwaveOnBackgroundLight,
onBackground = Color(0xFF1C1B1F), surfaceVariant = DeepwaveSurfaceVariantLight,
onSurface = Color(0xFF1C1B1F), 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 @Composable
fun DeepwaveTheme( fun DeepwaveTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ content: @Composable () -> Unit,
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, 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.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography( val Typography = Typography(
bodyLarge = TextStyle( displayLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp),
fontFamily = FontFamily.Default, displayMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp),
fontWeight = FontWeight.Normal, displaySmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp),
fontSize = 16.sp, headlineLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp),
lineHeight = 24.sp, headlineMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp),
letterSpacing = 0.5.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),
/* Other default text styles to override titleMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp),
titleLarge = TextStyle( titleSmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp),
fontFamily = FontFamily.Default, bodyLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp),
fontWeight = FontWeight.Normal, bodyMedium = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp),
fontSize = 22.sp, bodySmall = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp),
lineHeight = 28.sp, labelLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp),
letterSpacing = 0.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),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
) )
+27
View File
@@ -1,3 +1,30 @@
<resources> <resources>
<string name="app_name">Deepwave</string> <string name="app_name">Deepwave</string>
<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> </resources>
+2 -4
View File
@@ -6,8 +6,6 @@
See https://developer.android.com/about/versions/12/backup-restore See https://developer.android.com/about/versions/12/backup-restore
--> -->
<full-backup-content> <full-backup-content>
<!-- <exclude domain="sharedpref" path="deepwave_master_keyset_prefs.xml"/>
<include domain="sharedpref" path="."/> <exclude domain="file" path="datastore/deepwave_credentials.preferences_pb"/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content> </full-backup-content>
@@ -5,15 +5,11 @@
--> -->
<data-extraction-rules> <data-extraction-rules>
<cloud-backup> <cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up. <exclude domain="sharedpref" path="deepwave_master_keyset_prefs.xml"/>
<include .../> <exclude domain="file" path="datastore/deepwave_credentials.preferences_pb"/>
<exclude .../>
-->
</cloud-backup> </cloud-backup>
<!--
<device-transfer> <device-transfer>
<include .../> <exclude domain="sharedpref" path="deepwave_master_keyset_prefs.xml"/>
<exclude .../> <exclude domain="file" path="datastore/deepwave_credentials.preferences_pb"/>
</device-transfer> </device-transfer>
-->
</data-extraction-rules> </data-extraction-rules>
+1
View File
@@ -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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 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-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" } 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" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltAndroid" }
Vendored Regular → Executable
View File