fix(android): include modified files for Phase 20 (previous commit missed them)
Previous commit (45e2248) committed only the two new theme files; the
six modified files (MainActivity, AuthStore, AppDatabase, AuthSessionDao,
AuthSessionEntity, SettingsScreen) silently didn't get staged. This
commit lands the actual integration so the theme picker works end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -18,6 +19,7 @@ import androidx.navigation.compose.rememberNavController
|
||||
import com.fabledsword.minstrel.auth.ui.AuthGateViewModel
|
||||
import com.fabledsword.minstrel.nav.MinstrelNavGraph
|
||||
import com.fabledsword.minstrel.theme.MinstrelTheme
|
||||
import com.fabledsword.minstrel.theme.ThemePreferenceViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -25,36 +27,45 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
MinstrelTheme { App() }
|
||||
setContent { App() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun App(
|
||||
themeVm: ThemePreferenceViewModel = hiltViewModel(),
|
||||
gate: AuthGateViewModel = hiltViewModel(),
|
||||
) {
|
||||
val theme by themeVm.themeMode.collectAsStateWithLifecycle()
|
||||
MinstrelTheme(darkOverride = theme.toDarkOverride()) {
|
||||
val startDestination by gate.startDestination.collectAsStateWithLifecycle()
|
||||
val resolved = startDestination
|
||||
if (resolved == null) {
|
||||
BootSplash()
|
||||
} else {
|
||||
// No bottom nav, no drawer — navigation is per-screen via
|
||||
// the AppBar actions row + kebab (`MainAppBarActions`). The
|
||||
// MiniPlayer is included by each in-shell route's
|
||||
// `ShellScaffold` wrap; full-screen routes (NowPlaying /
|
||||
// Queue / unauthenticated) bypass the shell entirely.
|
||||
val navController = rememberNavController()
|
||||
MinstrelNavGraph(
|
||||
navController = navController,
|
||||
startDestination = resolved,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun App(gate: AuthGateViewModel = hiltViewModel()) {
|
||||
val startDestination by gate.startDestination.collectAsStateWithLifecycle()
|
||||
val resolved = startDestination
|
||||
if (resolved == null) {
|
||||
BootSplash()
|
||||
} else {
|
||||
// No bottom nav, no drawer — navigation is per-screen via the
|
||||
// AppBar actions row + kebab (`MainAppBarActions`). The MiniPlayer
|
||||
// is included by each in-shell route's `ShellScaffold` wrap;
|
||||
// full-screen routes (NowPlaying / Queue / unauthenticated)
|
||||
// bypass the shell entirely.
|
||||
val navController = rememberNavController()
|
||||
MinstrelNavGraph(
|
||||
navController = navController,
|
||||
startDestination = resolved,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BootSplash() {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,16 @@ class AuthStore @Inject constructor(
|
||||
private val userJsonState = MutableStateFlow<String?>(null)
|
||||
val userJson: StateFlow<String?> = userJsonState.asStateFlow()
|
||||
|
||||
private val themeModeState = MutableStateFlow<String?>(null)
|
||||
val themeMode: StateFlow<String?> = themeModeState.asStateFlow()
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
dao.observe().collect { row ->
|
||||
sessionCookieState.value = row?.sessionCookie
|
||||
baseUrlState.value = row?.baseUrl ?: DEFAULT_BASE_URL
|
||||
userJsonState.value = row?.userJson
|
||||
themeModeState.value = row?.themeMode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +69,11 @@ class AuthStore @Inject constructor(
|
||||
scope.launch { persistUserJson(value) }
|
||||
}
|
||||
|
||||
fun setThemeMode(value: String?) {
|
||||
themeModeState.value = value
|
||||
scope.launch { persistThemeMode(value) }
|
||||
}
|
||||
|
||||
private suspend fun persistCookie(value: String?) {
|
||||
if (dao.get() == null) {
|
||||
dao.upsert(currentEntity().copy(sessionCookie = value))
|
||||
@@ -89,11 +98,20 @@ class AuthStore @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun persistThemeMode(value: String?) {
|
||||
if (dao.get() == null) {
|
||||
dao.upsert(currentEntity().copy(themeMode = value))
|
||||
} else {
|
||||
dao.setThemeMode(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentEntity(): AuthSessionEntity = AuthSessionEntity(
|
||||
id = ROW_ID,
|
||||
sessionCookie = sessionCookieState.value,
|
||||
baseUrl = baseUrlState.value,
|
||||
userJson = userJsonState.value,
|
||||
themeMode = themeModeState.value,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -59,7 +59,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
|
||||
CachedHomeIndexEntity::class,
|
||||
AuthSessionEntity::class,
|
||||
],
|
||||
version = 2,
|
||||
version = 3,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(MinstrelTypeConverters::class)
|
||||
|
||||
+4
@@ -30,4 +30,8 @@ interface AuthSessionDao {
|
||||
/** Partial update: change only the serialized user identity JSON. */
|
||||
@Query("UPDATE auth_session SET userJson = :userJson WHERE id = 0")
|
||||
suspend fun setUserJson(userJson: String?)
|
||||
|
||||
/** Partial update: change only the theme override ("light"/"dark"/null=system). */
|
||||
@Query("UPDATE auth_session SET themeMode = :themeMode WHERE id = 0")
|
||||
suspend fun setThemeMode(themeMode: String?)
|
||||
}
|
||||
|
||||
+9
-1
@@ -5,7 +5,8 @@ import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* Single-row table holding the user's session cookie, configured
|
||||
* server base URL, and the serialized current user identity.
|
||||
* server base URL, the serialized current user identity, and the
|
||||
* theme-override preference.
|
||||
*
|
||||
* `id = 0` is the only valid row. `sessionCookie` is null when the
|
||||
* user is logged out (or has never logged in). `baseUrl` always has
|
||||
@@ -13,6 +14,12 @@ import androidx.room.PrimaryKey
|
||||
* configures one in Settings. `userJson` is the JSON-encoded
|
||||
* UserRef captured at sign-in so the username + isAdmin survive
|
||||
* a cold restart (the cookie alone doesn't carry identity).
|
||||
* `themeMode` is "system" / "light" / "dark" (or null = system).
|
||||
*
|
||||
* The table is the de-facto single-row app-prefs row at this point,
|
||||
* not strictly auth-only. Kept under the auth_session name to avoid
|
||||
* a schema-rename migration; a future cleanup could rename to
|
||||
* `app_preferences`.
|
||||
*/
|
||||
@Entity(tableName = "auth_session")
|
||||
data class AuthSessionEntity(
|
||||
@@ -20,4 +27,5 @@ data class AuthSessionEntity(
|
||||
val sessionCookie: String? = null,
|
||||
val baseUrl: String,
|
||||
val userJson: String? = null,
|
||||
val themeMode: String? = null,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,14 +38,18 @@ import com.fabledsword.minstrel.BuildConfig
|
||||
import com.fabledsword.minstrel.nav.Settings as SettingsRoute
|
||||
import com.fabledsword.minstrel.nav.ServerUrl
|
||||
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
|
||||
import com.fabledsword.minstrel.theme.ThemeMode
|
||||
import com.fabledsword.minstrel.theme.ThemePreferenceViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
navController: NavHostController,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
themeVm: ThemePreferenceViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val themeMode by themeVm.themeMode.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(state.signedOut) {
|
||||
if (state.signedOut) {
|
||||
@@ -75,6 +82,7 @@ fun SettingsScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
AccountCard(username = state.username, serverUrl = state.serverUrl)
|
||||
AppearanceCard(themeMode = themeMode, onPick = themeVm::setThemeMode)
|
||||
AboutCard()
|
||||
SignOutButton(isSigningOut = state.isSigningOut, onSignOut = viewModel::signOut)
|
||||
}
|
||||
@@ -115,6 +123,47 @@ private fun AccountCard(username: String, serverUrl: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AppearanceCard(themeMode: ThemeMode, onPick: (ThemeMode) -> Unit) {
|
||||
val options = ThemeMode.entries
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Appearance",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = "System follows your device setting.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
options.forEachIndexed { index, mode ->
|
||||
SegmentedButton(
|
||||
selected = mode == themeMode,
|
||||
onClick = { onPick(mode) },
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = options.size,
|
||||
),
|
||||
) { Text(displayName(mode)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayName(mode: ThemeMode): String = when (mode) {
|
||||
ThemeMode.SYSTEM -> "System"
|
||||
ThemeMode.LIGHT -> "Light"
|
||||
ThemeMode.DARK -> "Dark"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AboutCard() {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
Reference in New Issue
Block a user