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(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 = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.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
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user