feat(ui): Phase 3 Myne-style Material You 3 redesign

- Add Shapes.kt with rounded shape scheme (8-32dp corners) wired into AppTheme.
- BookshelfLibraryScreen: round cards (shapes.large), surfaceContainerLow, CenterAlignedTopAppBar, onSurfaceVariant labels, larger padding.
- ServerSettingsScreen: section cards with round shapes, scrollable, CenterAlignedTopAppBar.
- Dynamic color already enabled via AppTheme (Android 12+ dynamicColorScheme + materialkolor seed).
- ':app:assembleOssDebug' passes.
This commit is contained in:
Atte149 2026-06-24 17:05:33 +03:00
parent d0ceeef24f
commit 1c0eca423d
4 changed files with 130 additions and 61 deletions

View file

@ -0,0 +1,16 @@
package org.dueattendant149.bookreader.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
/**
* Myne-style Material You 3 shape scheme: generous rounded corners.
*/
val AppShapes = Shapes(
extraSmall = RoundedCornerShape(8.dp),
small = RoundedCornerShape(12.dp),
medium = RoundedCornerShape(16.dp),
large = RoundedCornerShape(24.dp),
extraLarge = RoundedCornerShape(32.dp),
)

View file

@ -178,6 +178,7 @@ fun AppTheme(
MaterialTheme( MaterialTheme(
colorScheme = finalColorScheme, colorScheme = finalColorScheme,
typography = appFontFamily?.let { AppTypography.withAppFontFamily(it) } ?: AppTypography, typography = appFontFamily?.let { AppTypography.withAppFontFamily(it) } ?: AppTypography,
shapes = AppShapes,
content = content content = content
) )
} }

View file

@ -1,21 +1,26 @@
package org.dueattendant149.bookreader.bookshelf package org.dueattendant149.bookreader.bookshelf
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
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -23,7 +28,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -34,13 +38,13 @@ fun ServerSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var bookshelfUrl by remember(uiState.bookshelfUrl) { androidx.compose.runtime.mutableStateOf(uiState.bookshelfUrl) } var bookshelfUrl by remember(uiState.bookshelfUrl) { mutableStateOf(uiState.bookshelfUrl) }
var absUrl by remember(uiState.absUrl) { androidx.compose.runtime.mutableStateOf(uiState.absUrl) } var absUrl by remember(uiState.absUrl) { mutableStateOf(uiState.absUrl) }
var absToken by remember(uiState.absToken) { androidx.compose.runtime.mutableStateOf(uiState.absToken) } var absToken by remember(uiState.absToken) { mutableStateOf(uiState.absToken) }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( androidx.compose.material3.CenterAlignedTopAppBar(
title = { Text("Server settings") }, title = { Text("Server settings") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
@ -54,61 +58,87 @@ fun ServerSettingsScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
.padding(16.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text("Bookshelf API") SettingsSectionCard(title = "Bookshelf API") {
Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField(
OutlinedTextField( value = bookshelfUrl,
value = bookshelfUrl, onValueChange = {
onValueChange = { bookshelfUrl = it
bookshelfUrl = it viewModel.updateBookshelfUrl(it)
viewModel.updateBookshelfUrl(it) },
}, label = { Text("https://books.dueattendant149.org/") },
label = { Text("https://books.dueattendant149.org/") }, singleLine = true,
singleLine = true, modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth() )
) }
Spacer(modifier = Modifier.height(24.dp)) SettingsSectionCard(title = "Audiobookshelf") {
OutlinedTextField(
Text("Audiobookshelf") value = absUrl,
Spacer(modifier = Modifier.height(8.dp)) onValueChange = {
OutlinedTextField( absUrl = it
value = absUrl, viewModel.updateAbsUrl(it)
onValueChange = { },
absUrl = it label = { Text("https://abs.dueattendant149.org/") },
viewModel.updateAbsUrl(it) singleLine = true,
}, modifier = Modifier.fillMaxWidth()
label = { Text("https://abs.dueattendant149.org/") }, )
singleLine = true, Spacer(modifier = Modifier.height(12.dp))
modifier = Modifier.fillMaxWidth() OutlinedTextField(
) value = absToken,
Spacer(modifier = Modifier.height(8.dp)) onValueChange = {
OutlinedTextField( absToken = it
value = absToken, viewModel.updateAbsToken(it)
onValueChange = { },
absToken = it label = { Text("Bearer token") },
viewModel.updateAbsToken(it) singleLine = true,
}, visualTransformation = PasswordVisualTransformation(),
label = { Text("Bearer token") }, modifier = Modifier.fillMaxWidth()
singleLine = true, )
visualTransformation = PasswordVisualTransformation(), }
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
Button( Button(
onClick = viewModel::save, onClick = viewModel::save,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large
) { ) {
Text("Save") Text("Save")
} }
if (uiState.saved) { if (uiState.saved) {
Spacer(modifier = Modifier.height(8.dp)) Text(
Text("Saved") "Saved",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
} }
} }
} }
}
@Composable
private fun SettingsSectionCard(
title: String,
content: @Composable () -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 12.dp)
)
content()
}
}
} }

View file

@ -19,7 +19,6 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -56,7 +55,7 @@ fun BookshelfLibraryScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( androidx.compose.material3.CenterAlignedTopAppBar(
title = { Text("Server Library") }, title = { Text("Server Library") },
actions = { actions = {
IconButton(onClick = onOpenSettings) { IconButton(onClick = onOpenSettings) {
@ -145,9 +144,13 @@ private fun LibraryCard(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp) .padding(vertical = 6.dp),
shape = MaterialTheme.shapes.large,
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text( Text(
text = library.name, text = library.name,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@ -156,7 +159,9 @@ private fun LibraryCard(
) )
Text( Text(
text = "${library.itemCount} items · ${library.mediaType}", text = "${library.itemCount} items · ${library.mediaType}",
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
) )
} }
} }
@ -172,9 +177,13 @@ private fun ItemCard(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp) .padding(vertical = 6.dp),
shape = MaterialTheme.shapes.large,
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text( Text(
text = item.title, text = item.title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@ -184,16 +193,29 @@ private fun ItemCard(
if (item.author.isNotBlank()) { if (item.author.isNotBlank()) {
Text( Text(
text = item.author, text = item.author,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
) )
} }
Text( Text(
text = item.type, text = item.type,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
) )
if (isDownloading) { if (isDownloading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) LinearProgressIndicator(
Text("Downloading...", style = MaterialTheme.typography.bodySmall) modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
)
Text(
"Downloading...",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 4.dp)
)
} }
} }
} }