feat(ui): m5 shard-website design pass
All checks were successful
PR Checks / android-build (pull_request) Successful in 10m6s
All checks were successful
PR Checks / android-build (pull_request) Successful in 10m6s
Restyle every Android screen with the shard-website theme from the "Runic Gateway Screens" design (docs/android/PLAN.md §M5): deep blue-black surfaces, a slate-blue accent, parchment serif body copy, and an engraved Cinzel serif display face. The app is now dark-only, matching the design. Theme layer (propagates to all token-based screens): - Color.kt: replace the placeholder purple palette with named shard tokens. - Theme.kt: one dark color scheme mapped onto the palette + 8/12/16dp shapes; drop the light branch; keep optional per-shard brand-accent seeding. - Type.kt: full type scale — Cinzel display/headline/title, serif body, letter-spaced sans labels/buttons. - Font.kt + res/font/cinzel_variable.ttf (SIL OFL, app/licenses/Cinzel-OFL.txt): the Cinzel display family, pinned to 500/600/700 via FontVariation. Shared components (ui/components/ThemeComponents.kt): StatusPill (semantic tones), OnlineDot, SectionLabel, FeatureCard (gradient), StatBar — adopted across Home, Account, Shard hub, Champs, Characters, Houses, and the character sheet (vitals/skills meters). Shell: dark top-bar + drawer styling; dark launch theme and light system-bar icons so the first frame matches (no white flash). Build (assembleDebug) and unit tests green. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
93
app/licenses/Cinzel-OFL.txt
Normal file
93
app/licenses/Cinzel-OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.runicgateway.app
|
package com.runicgateway.app
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -35,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
// Dark-only app (M5): force light system-bar icons over the transparent bars so
|
||||||
|
// they stay legible on the deep blue-black surfaces regardless of system theme.
|
||||||
|
val barStyle = SystemBarStyle.dark(Color.TRANSPARENT)
|
||||||
|
enableEdgeToEdge(statusBarStyle = barStyle, navigationBarStyle = barStyle)
|
||||||
setContent {
|
setContent {
|
||||||
val appViewModel: AppViewModel = hiltViewModel()
|
val appViewModel: AppViewModel = hiltViewModel()
|
||||||
val state by appViewModel.state.collectAsStateWithLifecycle()
|
val state by appViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.material3.NavigationDrawerItemDefaults
|
|||||||
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.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -29,7 +30,9 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -111,11 +114,17 @@ fun RunicApp(
|
|||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = isTopLevel,
|
gesturesEnabled = isTopLevel,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
ModalDrawerSheet {
|
ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant) {
|
||||||
|
val drawerItemColors = NavigationDrawerItemDefaults.colors(
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
unselectedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = brand?.name?.takeIf { it.isNotBlank() } ?: stringResource(R.string.app_name),
|
text = brand?.name?.takeIf { it.isNotBlank() } ?: stringResource(R.string.app_name),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
@@ -128,6 +137,7 @@ fun RunicApp(
|
|||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
navController.navigateTopLevel(entry.route)
|
navController.navigateTopLevel(entry.route)
|
||||||
},
|
},
|
||||||
|
colors = drawerItemColors,
|
||||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,6 +162,7 @@ fun RunicApp(
|
|||||||
navController.navigate(Routes.LOGIN)
|
navController.navigate(Routes.LOGIN)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
colors = drawerItemColors,
|
||||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
|
||||||
)
|
)
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
@@ -161,6 +172,7 @@ fun RunicApp(
|
|||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
onChangeServer()
|
onChangeServer()
|
||||||
},
|
},
|
||||||
|
colors = drawerItemColors,
|
||||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -170,10 +182,19 @@ fun RunicApp(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
actionIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
brand?.name?.takeIf { it.isNotBlank() }
|
text = (brand?.name?.takeIf { it.isNotBlank() }
|
||||||
?: stringResource(R.string.app_name),
|
?: stringResource(R.string.app_name)).uppercase(),
|
||||||
|
style = MaterialTheme.typography.titleSmall.copy(letterSpacing = 1.2.sp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import com.runicgateway.app.ui.auth.AccountViewModel.Feedback
|
|||||||
import com.runicgateway.app.ui.auth.AccountViewModel.Section
|
import com.runicgateway.app.ui.auth.AccountViewModel.Section
|
||||||
import com.runicgateway.app.ui.components.ErrorView
|
import com.runicgateway.app.ui.components.ErrorView
|
||||||
import com.runicgateway.app.ui.components.LoadingView
|
import com.runicgateway.app.ui.components.LoadingView
|
||||||
|
import com.runicgateway.app.ui.components.PillTone
|
||||||
|
import com.runicgateway.app.ui.components.StatusPill
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The signed-in account surface (PLAN.md §5, §6.3): identity + role, self-service
|
* The signed-in account surface (PLAN.md §5, §6.3): identity + role, self-service
|
||||||
@@ -101,11 +103,10 @@ private fun IdentityCard(username: String, roleLabel: String) {
|
|||||||
Card(Modifier.fillMaxWidth()) {
|
Card(Modifier.fillMaxWidth()) {
|
||||||
Column(Modifier.padding(20.dp)) {
|
Column(Modifier.padding(20.dp)) {
|
||||||
Text(text = username, style = MaterialTheme.typography.titleLarge)
|
Text(text = username, style = MaterialTheme.typography.titleLarge)
|
||||||
Text(
|
StatusPill(
|
||||||
text = roleLabel,
|
text = roleLabel,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
tone = PillTone.Info,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
modifier = Modifier.padding(top = 4.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,10 +222,9 @@ private fun TwoFactorSection(
|
|||||||
) {
|
) {
|
||||||
var code by rememberSaveable { mutableStateOf("") }
|
var code by rememberSaveable { mutableStateOf("") }
|
||||||
SectionCard(R.string.account_totp_title) {
|
SectionCard(R.string.account_totp_title) {
|
||||||
Text(
|
StatusPill(
|
||||||
stringResource(if (account.totp_enabled) R.string.account_totp_on else R.string.account_totp_off),
|
text = stringResource(if (account.totp_enabled) R.string.account_totp_on else R.string.account_totp_off),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
tone = if (account.totp_enabled) PillTone.Success else PillTone.Neutral,
|
||||||
color = if (account.totp_enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
package com.runicgateway.app.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.runicgateway.app.ui.theme.ShardCardBottom
|
||||||
|
import com.runicgateway.app.ui.theme.ShardCardTop
|
||||||
|
import com.runicgateway.app.ui.theme.ShardDanger
|
||||||
|
import com.runicgateway.app.ui.theme.ShardDangerBg
|
||||||
|
import com.runicgateway.app.ui.theme.ShardElevated
|
||||||
|
import com.runicgateway.app.ui.theme.ShardFaint
|
||||||
|
import com.runicgateway.app.ui.theme.ShardOutline
|
||||||
|
import com.runicgateway.app.ui.theme.ShardPillBg
|
||||||
|
import com.runicgateway.app.ui.theme.ShardPillFg
|
||||||
|
import com.runicgateway.app.ui.theme.ShardSuccess
|
||||||
|
import com.runicgateway.app.ui.theme.ShardSuccessBg
|
||||||
|
import com.runicgateway.app.ui.theme.ShardSuccessDot
|
||||||
|
import com.runicgateway.app.ui.theme.ShardWarning
|
||||||
|
import com.runicgateway.app.ui.theme.ShardWarningBg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared visual building blocks for the M5 shard-website design pass
|
||||||
|
* (docs/android/PLAN.md §M5): the recurring pill, section-label, feature-card,
|
||||||
|
* and stat-bar motifs the mockup repeats across screens. Pure presentation —
|
||||||
|
* no state, no data dependencies — so any screen can adopt them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Semantic tone for a [StatusPill] / [OnlineDot]. */
|
||||||
|
enum class PillTone { Success, Warning, Danger, Neutral, Info }
|
||||||
|
|
||||||
|
private data class PillColors(val fg: Color, val bg: Color)
|
||||||
|
|
||||||
|
private fun toneColors(tone: PillTone): PillColors = when (tone) {
|
||||||
|
PillTone.Success -> PillColors(ShardSuccess, ShardSuccessBg)
|
||||||
|
PillTone.Warning -> PillColors(ShardWarning, ShardWarningBg)
|
||||||
|
PillTone.Danger -> PillColors(ShardDanger, ShardDangerBg)
|
||||||
|
PillTone.Neutral, PillTone.Info -> PillColors(ShardPillFg, ShardPillBg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small uppercase status chip — "Live", "Up", "Enabled", "IDOC", a role — with a
|
||||||
|
* rounded filled background tinted by [tone]. Mirrors the mockup's pill badges.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun StatusPill(text: String, tone: PillTone, modifier: Modifier = Modifier) {
|
||||||
|
val c = toneColors(tone)
|
||||||
|
Surface(color = c.bg, shape = CircleShape, modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = text.uppercase(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = c.fg,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The small colored status dot (green when live), matched to a [PillTone]. */
|
||||||
|
@Composable
|
||||||
|
fun OnlineDot(tone: PillTone, modifier: Modifier = Modifier) {
|
||||||
|
val color = when (tone) {
|
||||||
|
PillTone.Success -> ShardSuccessDot
|
||||||
|
PillTone.Warning -> ShardWarning
|
||||||
|
PillTone.Danger -> ShardDanger
|
||||||
|
PillTone.Neutral, PillTone.Info -> ShardFaint
|
||||||
|
}
|
||||||
|
Box(modifier.size(8.dp).clip(CircleShape).background(color))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The muted, uppercase, letter-spaced section header the design uses above grouped
|
||||||
|
* content ("Attributes", "Vitals", "Password", "Linked accounts").
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
text = text.uppercase(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = ShardFaint,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The elevated "feature" card: a vertical blue gradient with a hairline outline and
|
||||||
|
* soft shadow, used for the home status card, the shard-online banner, and the
|
||||||
|
* vendor card. [content] is laid out in a padded [Column].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun FeatureCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentPadding: Int = 18,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Brush.verticalGradient(listOf(ShardCardTop, ShardCardBottom)))
|
||||||
|
.border(1.dp, ShardOutline, RoundedCornerShape(12.dp)),
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(contentPadding.dp), content = content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A slim rounded meter (vitals / skills). [fraction] is clamped to 0..1; the fill is
|
||||||
|
* the slate accent over a bordered dark track.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun StatBar(fraction: Float, modifier: Modifier = Modifier) {
|
||||||
|
val pct = fraction.coerceIn(0f, 1f)
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(6.dp)
|
||||||
|
.clip(RoundedCornerShape(3.dp))
|
||||||
|
.background(ShardElevated)
|
||||||
|
.border(1.dp, ShardOutline, RoundedCornerShape(3.dp)),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(pct)
|
||||||
|
.height(6.dp)
|
||||||
|
.clip(RoundedCornerShape(3.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.secondary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,19 @@
|
|||||||
package com.runicgateway.app.ui.home
|
package com.runicgateway.app.ui.home
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
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.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
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.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -27,7 +27,10 @@ import com.runicgateway.app.data.api.dto.BrandDto
|
|||||||
import com.runicgateway.app.data.api.dto.StatusDto
|
import com.runicgateway.app.data.api.dto.StatusDto
|
||||||
import com.runicgateway.app.ui.UiState
|
import com.runicgateway.app.ui.UiState
|
||||||
import com.runicgateway.app.ui.components.ErrorView
|
import com.runicgateway.app.ui.components.ErrorView
|
||||||
|
import com.runicgateway.app.ui.components.FeatureCard
|
||||||
import com.runicgateway.app.ui.components.LoadingView
|
import com.runicgateway.app.ui.components.LoadingView
|
||||||
|
import com.runicgateway.app.ui.components.OnlineDot
|
||||||
|
import com.runicgateway.app.ui.components.PillTone
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Home / status (PLAN.md §6.1): the shard's name + tagline from branding, and a
|
* Home / status (PLAN.md §6.1): the shard's name + tagline from branding, and a
|
||||||
@@ -83,21 +86,19 @@ private fun HomeContent(brand: BrandDto?, status: StatusDto, modifier: Modifier
|
|||||||
@Composable
|
@Composable
|
||||||
private fun StatusCard(status: StatusDto) {
|
private fun StatusCard(status: StatusDto) {
|
||||||
val online = !status.isMaintenance
|
val online = !status.isMaintenance
|
||||||
val containerColor =
|
|
||||||
if (online) MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
else MaterialTheme.colorScheme.errorContainer
|
|
||||||
|
|
||||||
Card(
|
FeatureCard {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
OnlineDot(if (online) PillTone.Success else PillTone.Danger)
|
||||||
) {
|
Spacer(Modifier.width(10.dp))
|
||||||
Column(Modifier.padding(16.dp)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
if (online) R.string.home_status_live else R.string.home_status_maintenance,
|
if (online) R.string.home_status_live else R.string.home_status_maintenance,
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
if (status.isMaintenance && status.statusMessage.isNotBlank()) {
|
if (status.isMaintenance && status.statusMessage.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = status.statusMessage,
|
text = status.statusMessage,
|
||||||
@@ -119,4 +120,3 @@ private fun StatusCard(status: StatusDto) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import com.runicgateway.app.data.api.dto.TitlesDto
|
|||||||
import com.runicgateway.app.ui.UiState
|
import com.runicgateway.app.ui.UiState
|
||||||
import com.runicgateway.app.ui.components.ErrorView
|
import com.runicgateway.app.ui.components.ErrorView
|
||||||
import com.runicgateway.app.ui.components.LoadingView
|
import com.runicgateway.app.ui.components.LoadingView
|
||||||
|
import com.runicgateway.app.ui.components.SectionLabel
|
||||||
|
import com.runicgateway.app.ui.components.StatBar
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,13 +142,16 @@ private fun AttributesBlock(stats: CharStatsDto) {
|
|||||||
Stat(stringResource(R.string.player_char_dex), stats.dex)
|
Stat(stringResource(R.string.player_char_dex), stats.dex)
|
||||||
Stat(stringResource(R.string.player_char_int), stats.int)
|
Stat(stringResource(R.string.player_char_int), stats.int)
|
||||||
}
|
}
|
||||||
Column(Modifier.padding(top = 12.dp)) {
|
Column(Modifier.padding(top = 16.dp)) {
|
||||||
|
SectionLabel(stringResource(R.string.player_char_vitals))
|
||||||
|
Column(Modifier.padding(top = 8.dp)) {
|
||||||
Vital(stringResource(R.string.player_char_hits), stats.hits, stats.hitsMax)
|
Vital(stringResource(R.string.player_char_hits), stats.hits, stats.hitsMax)
|
||||||
Vital(stringResource(R.string.player_char_mana), stats.mana, stats.manaMax)
|
Vital(stringResource(R.string.player_char_mana), stats.mana, stats.manaMax)
|
||||||
Vital(stringResource(R.string.player_char_stam), stats.stam, stats.stamMax)
|
Vital(stringResource(R.string.player_char_stam), stats.stam, stats.stamMax)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Stat(label: String, value: Int?) {
|
private fun Stat(label: String, value: Int?) {
|
||||||
@@ -158,9 +163,13 @@ private fun Stat(label: String, value: Int?) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Vital(label: String, cur: Int?, max: Int?) {
|
private fun Vital(label: String, cur: Int?, max: Int?) {
|
||||||
Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
Column(Modifier.padding(vertical = 5.dp)) {
|
||||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
Row(Modifier.fillMaxWidth().padding(bottom = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text("${cur ?: "—"} / ${max ?: "—"}", style = MaterialTheme.typography.bodyMedium)
|
Text(label, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text("${cur ?: "—"} / ${max ?: "—"}", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
val fraction = if (max != null && max > 0 && cur != null) cur.toFloat() / max else 0f
|
||||||
|
StatBar(fraction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,9 +203,13 @@ private fun SkillsBlock(skills: List<SkillDto>) {
|
|||||||
if (shown.isEmpty()) return
|
if (shown.isEmpty()) return
|
||||||
SheetCard(R.string.player_char_skills) {
|
SheetCard(R.string.player_char_skills) {
|
||||||
shown.forEach { skill ->
|
shown.forEach { skill ->
|
||||||
Row(Modifier.fillMaxWidth().padding(vertical = 3.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
val value = skill.value ?: 0.0
|
||||||
Text(skill.n ?: "—", style = MaterialTheme.typography.bodyMedium)
|
Column(Modifier.padding(vertical = 5.dp)) {
|
||||||
Text(formatSkill(skill.value ?: 0.0), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
|
Row(Modifier.fillMaxWidth().padding(bottom = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Text(skill.n ?: "—", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(formatSkill(value), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
StatBar((value / SKILL_CAP).toFloat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,6 +256,9 @@ private fun SheetCard(titleRes: Int, content: @Composable () -> Unit) {
|
|||||||
|
|
||||||
// ── Pure helpers (unit-tested) ───────────────────────────────────────────────
|
// ── Pure helpers (unit-tested) ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** ServUO's default skill cap; used to scale the skill meter fill (0..1). */
|
||||||
|
private const val SKILL_CAP = 120.0
|
||||||
|
|
||||||
/** Format a skill value: drop the ".0" on whole numbers, else one decimal. */
|
/** Format a skill value: drop the ".0" on whole numbers, else one decimal. */
|
||||||
internal fun formatSkill(value: Double): String =
|
internal fun formatSkill(value: Double): String =
|
||||||
if (value % 1.0 == 0.0) value.toInt().toString() else "%.1f".format(value)
|
if (value % 1.0 == 0.0) value.toInt().toString() else "%.1f".format(value)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import com.runicgateway.app.ui.ErrorKind
|
|||||||
import com.runicgateway.app.ui.UiState
|
import com.runicgateway.app.ui.UiState
|
||||||
import com.runicgateway.app.ui.components.ErrorView
|
import com.runicgateway.app.ui.components.ErrorView
|
||||||
import com.runicgateway.app.ui.components.LoadingView
|
import com.runicgateway.app.ui.components.LoadingView
|
||||||
|
import com.runicgateway.app.ui.components.PillTone
|
||||||
|
import com.runicgateway.app.ui.components.StatusPill
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The player's linked game accounts + character rosters (PLAN.md §6.3). Tapping a
|
* The player's linked game accounts + character rosters (PLAN.md §6.3). Tapping a
|
||||||
@@ -212,14 +214,18 @@ private fun CharRow(char: RosterCharDto, onOpenChar: (String) -> Unit) {
|
|||||||
Modifier.fillMaxWidth().padding(14.dp),
|
Modifier.fillMaxWidth().padding(14.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.weight(1f)) {
|
|
||||||
Text(char.name ?: stringResource(R.string.player_char_unknown), style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(if (char.online) R.string.player_char_online else R.string.player_char_offline),
|
char.name ?: stringResource(R.string.player_char_unknown),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = if (char.online) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
StatusPill(
|
||||||
|
text = stringResource(
|
||||||
|
if (char.online) R.string.player_char_online else R.string.player_char_offline,
|
||||||
|
),
|
||||||
|
tone = if (char.online) PillTone.Success else PillTone.Neutral,
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Text("›", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("›", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.runicgateway.app.R
|
import com.runicgateway.app.R
|
||||||
import com.runicgateway.app.data.api.dto.ChampDto
|
import com.runicgateway.app.data.api.dto.ChampDto
|
||||||
|
import com.runicgateway.app.ui.components.PillTone
|
||||||
|
import com.runicgateway.app.ui.components.StatusPill
|
||||||
|
|
||||||
/** The champion-spawn board (PLAN.md §6.2), live via SSE deltas. */
|
/** The champion-spawn board (PLAN.md §6.2), live via SSE deltas. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -53,11 +55,7 @@ private fun ChampCard(champ: ChampDto) {
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
champ.status?.let {
|
champ.status?.let {
|
||||||
Text(
|
StatusPill(text = it, tone = champStatusTone(it))
|
||||||
text = it.replaceFirstChar { c -> c.uppercase() },
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val detail = champDetail(champ)
|
val detail = champDetail(champ)
|
||||||
@@ -82,6 +80,13 @@ private fun ChampCard(champ: ChampDto) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Map a champ status to a pill tone: live spawns read green, cooldowns amber. */
|
||||||
|
private fun champStatusTone(status: String): PillTone = when (status.lowercase()) {
|
||||||
|
"active", "up", "spawned" -> PillTone.Success
|
||||||
|
"cooldown", "restarting", "advancing" -> PillTone.Warning
|
||||||
|
else -> PillTone.Neutral
|
||||||
|
}
|
||||||
|
|
||||||
/** A short category-specific status line, mirroring the website's champ detail. */
|
/** A short category-specific status line, mirroring the website's champ detail. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun champDetail(champ: ChampDto): String = when (champ.category) {
|
private fun champDetail(champ: ChampDto): String = when (champ.category) {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.runicgateway.app.R
|
import com.runicgateway.app.R
|
||||||
import com.runicgateway.app.data.api.dto.HouseDto
|
import com.runicgateway.app.data.api.dto.HouseDto
|
||||||
|
import com.runicgateway.app.ui.components.PillTone
|
||||||
|
import com.runicgateway.app.ui.components.StatusPill
|
||||||
|
|
||||||
/** The public "falling houses" (IDOC) board (PLAN.md §6.2), live via `house.decay`. */
|
/** The public "falling houses" (IDOC) board (PLAN.md §6.2), live via `house.decay`. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -54,10 +56,9 @@ private fun HouseCard(house: HouseDto) {
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
Text(
|
StatusPill(
|
||||||
text = stringResource(R.string.houses_idoc_badge),
|
text = stringResource(R.string.houses_idoc_badge),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
tone = PillTone.Danger,
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val where = listOfNotNull(
|
val where = listOfNotNull(
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import com.runicgateway.app.ui.UiState
|
|||||||
import com.runicgateway.app.ui.components.EmptyView
|
import com.runicgateway.app.ui.components.EmptyView
|
||||||
import com.runicgateway.app.ui.components.ErrorView
|
import com.runicgateway.app.ui.components.ErrorView
|
||||||
import com.runicgateway.app.ui.components.LoadingView
|
import com.runicgateway.app.ui.components.LoadingView
|
||||||
|
import com.runicgateway.app.ui.theme.ShardSuccess
|
||||||
|
import com.runicgateway.app.ui.theme.ShardSuccessDot
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A small "Live / Offline" indicator for the shard screens (PLAN.md §6.2): a dot +
|
* A small "Live / Offline" indicator for the shard screens (PLAN.md §6.2): a dot +
|
||||||
@@ -34,22 +36,19 @@ import com.runicgateway.app.ui.components.LoadingView
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LiveChip(connected: Boolean, modifier: Modifier = Modifier) {
|
fun LiveChip(connected: Boolean, modifier: Modifier = Modifier) {
|
||||||
val color = if (connected) {
|
val dotColor = if (connected) ShardSuccessDot else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
MaterialTheme.colorScheme.primary
|
val textColor = if (connected) ShardSuccess else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
}
|
|
||||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||||
androidx.compose.foundation.layout.Box(
|
androidx.compose.foundation.layout.Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(8.dp)
|
.size(8.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(color),
|
.background(dotColor),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(if (connected) R.string.shard_live else R.string.shard_offline),
|
text = stringResource(if (connected) R.string.shard_live else R.string.shard_offline),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = color,
|
color = textColor,
|
||||||
modifier = Modifier.padding(start = 6.dp),
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ import com.runicgateway.app.data.api.dto.OnlineStaffDto
|
|||||||
import com.runicgateway.app.data.api.dto.ShardStatusDto
|
import com.runicgateway.app.data.api.dto.ShardStatusDto
|
||||||
import com.runicgateway.app.ui.UiState
|
import com.runicgateway.app.ui.UiState
|
||||||
import com.runicgateway.app.ui.components.ErrorView
|
import com.runicgateway.app.ui.components.ErrorView
|
||||||
|
import com.runicgateway.app.ui.components.FeatureCard
|
||||||
import com.runicgateway.app.ui.components.LoadingView
|
import com.runicgateway.app.ui.components.LoadingView
|
||||||
|
import com.runicgateway.app.ui.components.PillTone
|
||||||
|
import com.runicgateway.app.ui.components.SectionLabel
|
||||||
|
import com.runicgateway.app.ui.components.StatusPill
|
||||||
|
|
||||||
/** Board destinations reachable from the hub. */
|
/** Board destinations reachable from the hub. */
|
||||||
enum class ShardBoard { CHAMPS, GUILDS, GOVERNORS, HOUSES }
|
enum class ShardBoard { CHAMPS, GUILDS, GOVERNORS, HOUSES }
|
||||||
@@ -114,19 +118,25 @@ private fun HubContent(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatusCard(status: ShardStatusDto, presenceCount: Int?) {
|
private fun StatusCard(status: ShardStatusDto, presenceCount: Int?) {
|
||||||
Card(Modifier.fillMaxWidth()) {
|
FeatureCard {
|
||||||
Column(Modifier.padding(16.dp)) {
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
if (status.isOnline) R.string.shard_status_online else R.string.shard_status_offline,
|
if (status.isOnline) R.string.shard_status_online else R.string.shard_status_offline,
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = if (status.isOnline) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
val online = presenceCount ?: status.onlineCount
|
val online = presenceCount ?: status.onlineCount
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.shard_online_count, online),
|
text = stringResource(R.string.shard_online_count, online),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(top = 6.dp),
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
)
|
)
|
||||||
status.economy?.gold?.let { gold ->
|
status.economy?.gold?.let { gold ->
|
||||||
@@ -138,6 +148,13 @@ private fun StatusCard(status: ShardStatusDto, presenceCount: Int?) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
StatusPill(
|
||||||
|
text = stringResource(
|
||||||
|
if (status.isOnline) R.string.shard_live else R.string.shard_offline,
|
||||||
|
),
|
||||||
|
tone = if (status.isOnline) PillTone.Success else PillTone.Neutral,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,11 +185,7 @@ private fun BoardsCard(onOpenBoard: (ShardBoard) -> Unit) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SectionHeader(text: String) {
|
private fun SectionHeader(text: String) {
|
||||||
Text(
|
SectionLabel(text = text, modifier = Modifier.padding(top = 4.dp))
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
modifier = Modifier.padding(top = 4.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -5,12 +5,44 @@ package com.runicgateway.app.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
// Placeholder palette for the M0 skeleton. The M5 design pass replaces this and
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// derives Material 3 colors from each shard's per-install branding (PLAN.md §3, §5).
|
// Shard-website palette (M5 design pass, docs/android/PLAN.md §M5). Deep blue-black
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
// surfaces, a slate-blue accent, parchment serif text, and a light CTA fill —
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
// mirrored from the "Runic Gateway Screens" design. Dark-only by design.
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650A4)
|
// Surfaces
|
||||||
val PurpleGrey40 = Color(0xFF625B71)
|
val ShardPage = Color(0xFF0B0F14) // app/page background
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val ShardSurface = Color(0xFF0E1318) // screen background
|
||||||
|
val ShardElevated = Color(0xFF11161D) // top bar, inputs, drawer, list tracks
|
||||||
|
val ShardCardTop = Color(0xFF192231) // feature-card gradient (top)
|
||||||
|
val ShardCardBottom = Color(0xFF141A21) // feature-card gradient (bottom)
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
val ShardOutline = Color(0xFF2A3544) // borders / input outlines
|
||||||
|
val ShardDivider = Color(0xFF1D2733) // hairline row dividers
|
||||||
|
|
||||||
|
// Text
|
||||||
|
val ShardHeading = Color(0xFFEEF3F8) // brightest headings
|
||||||
|
val ShardHeadingDim = Color(0xFFE6EDF6) // heading on surface
|
||||||
|
val ShardBody = Color(0xFFC4CDD8) // body copy
|
||||||
|
val ShardMuted = Color(0xFFAEB8C4) // secondary text
|
||||||
|
val ShardFaint = Color(0xFF6F7D8E) // meta / faint labels
|
||||||
|
|
||||||
|
// Accent + CTA
|
||||||
|
val ShardAccent = Color(0xFF7F99BD) // links, secondary highlights
|
||||||
|
val ShardCta = Color(0xFFCDD9E8) // filled button surface
|
||||||
|
val ShardOnCta = Color(0xFF0B0F14) // text on the filled button
|
||||||
|
|
||||||
|
// Neutral / info pill
|
||||||
|
val ShardPillBg = Color(0xFF13243C)
|
||||||
|
val ShardPillFg = Color(0xFFCDD9E8)
|
||||||
|
|
||||||
|
// Semantic (status pills + dots)
|
||||||
|
val ShardSuccess = Color(0xFF7FD0A4)
|
||||||
|
val ShardSuccessBg = Color(0x295FB98A) // rgba(95,185,138,0.16)
|
||||||
|
val ShardSuccessDot = Color(0xFF5FB98A)
|
||||||
|
val ShardWarning = Color(0xFFE6C26A)
|
||||||
|
val ShardWarningBg = Color(0x2EE6C26A) // rgba(230,194,106,0.18)
|
||||||
|
val ShardDanger = Color(0xFFD98B84)
|
||||||
|
val ShardDangerBg = Color(0x29D98B84) // rgba(217,139,132,0.16)
|
||||||
|
|||||||
40
app/src/main/java/com/runicgateway/app/ui/theme/Font.kt
Normal file
40
app/src/main/java/com/runicgateway/app/ui/theme/Font.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
package com.runicgateway.app.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.ExperimentalTextApi
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontVariation
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import com.runicgateway.app.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type families for the M5 shard-website design pass (docs/android/PLAN.md §M5).
|
||||||
|
*
|
||||||
|
* - [Cinzel] — the engraved serif display face used for headings, screen titles,
|
||||||
|
* and the top-bar title. Shipped as a single weight-axis **variable** font
|
||||||
|
* (`res/font/cinzel_variable.ttf`, SIL OFL — see `app/licenses/Cinzel-OFL.txt`);
|
||||||
|
* the 500/600/700 instances the design uses are pinned via [FontVariation]
|
||||||
|
* (supported on API 26+, and our minSdk is 29).
|
||||||
|
* - [AppSerif] — the parchment body face. Android's platform serif is Noto Serif,
|
||||||
|
* which reads as the design's Georgia body copy without bundling another binary.
|
||||||
|
* - [AppSans] — the label/meta/button face (the design's "Helvetica Neue" runs).
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTextApi::class)
|
||||||
|
private fun cinzel(weight: FontWeight) =
|
||||||
|
Font(
|
||||||
|
R.font.cinzel_variable,
|
||||||
|
weight = weight,
|
||||||
|
variationSettings = FontVariation.Settings(FontVariation.weight(weight.weight)),
|
||||||
|
)
|
||||||
|
|
||||||
|
val Cinzel = FontFamily(
|
||||||
|
cinzel(FontWeight.Medium), // 500
|
||||||
|
cinzel(FontWeight.SemiBold), // 600
|
||||||
|
cinzel(FontWeight.Bold), // 700
|
||||||
|
)
|
||||||
|
|
||||||
|
val AppSerif = FontFamily.Serif
|
||||||
|
val AppSans = FontFamily.SansSerif
|
||||||
@@ -3,54 +3,78 @@
|
|||||||
*/
|
*/
|
||||||
package com.runicgateway.app.ui.theme
|
package com.runicgateway.app.ui.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
/**
|
||||||
primary = Purple80,
|
* The shard-website color scheme (M5 design pass). The app is **dark-only** — the
|
||||||
secondary = PurpleGrey80,
|
* design is a single deep blue-black theme, so there is no light variant and the
|
||||||
tertiary = Pink80,
|
* system light/dark setting is intentionally ignored. Material roles are mapped
|
||||||
|
* onto the palette in [ui/theme/Color.kt] so the ~20 token-based screens take on
|
||||||
|
* the theme without per-screen color work.
|
||||||
|
*/
|
||||||
|
private val ShardColorScheme = darkColorScheme(
|
||||||
|
primary = ShardCta, // filled CTA buttons
|
||||||
|
onPrimary = ShardOnCta,
|
||||||
|
secondary = ShardAccent, // links / secondary highlights
|
||||||
|
onSecondary = ShardOnCta,
|
||||||
|
tertiary = ShardAccent,
|
||||||
|
onTertiary = ShardOnCta,
|
||||||
|
background = ShardPage,
|
||||||
|
onBackground = ShardBody,
|
||||||
|
surface = ShardSurface,
|
||||||
|
onSurface = ShardBody,
|
||||||
|
surfaceVariant = ShardElevated,
|
||||||
|
onSurfaceVariant = ShardMuted,
|
||||||
|
surfaceContainer = ShardElevated,
|
||||||
|
surfaceContainerHigh = ShardElevated,
|
||||||
|
surfaceContainerLow = ShardSurface,
|
||||||
|
outline = ShardOutline,
|
||||||
|
outlineVariant = ShardDivider,
|
||||||
|
secondaryContainer = ShardPillBg, // neutral chips / selected drawer item
|
||||||
|
onSecondaryContainer = ShardPillFg,
|
||||||
|
error = ShardDanger,
|
||||||
|
onError = ShardOnCta,
|
||||||
|
errorContainer = ShardDangerBg,
|
||||||
|
onErrorContainer = ShardDanger,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
/** 8dp inputs/chips, 12dp cards, 16dp large surfaces — matching the mockup radii. */
|
||||||
primary = Purple40,
|
private val ShardShapes = Shapes(
|
||||||
secondary = PurpleGrey40,
|
extraSmall = RoundedCornerShape(8.dp),
|
||||||
tertiary = Pink40,
|
small = RoundedCornerShape(8.dp),
|
||||||
|
medium = RoundedCornerShape(12.dp),
|
||||||
|
large = RoundedCornerShape(16.dp),
|
||||||
|
extraLarge = RoundedCornerShape(24.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App theme for the functional pass. The color scheme is seeded from the
|
* App theme. The color scheme is the fixed shard-website dark palette; when a shard
|
||||||
* per-shard brand accent (PLAN.md §3: the app themes itself from the site's
|
* publishes a brand accent (PLAN.md §3), it seeds the [MaterialTheme]'s primary and
|
||||||
* branding) when one is available, so the app takes on each shard's color; it
|
* secondary roles so buttons and highlights carry that shard's color while the rest
|
||||||
* falls back to a neutral placeholder scheme before connect or when a site
|
* of the deep blue-black system stays intact. With no accent, the slate default is
|
||||||
* publishes no accent.
|
* used.
|
||||||
*
|
|
||||||
* This is deliberately a minimal seeding — a single-color override on the
|
|
||||||
* default Material 3 schemes. The M5 design pass replaces it with a full,
|
|
||||||
* designed color system (§2.1, §5); nothing here is meant to be the final look.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RunicGatewayTheme(
|
fun RunicGatewayTheme(
|
||||||
accent: Color? = null,
|
accent: Color? = null,
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val base = if (darkTheme) DarkColorScheme else LightColorScheme
|
|
||||||
val colorScheme = if (accent != null) {
|
val colorScheme = if (accent != null) {
|
||||||
// Brand consistency across a shard wins over device dynamic color: seed
|
ShardColorScheme.copy(primary = accent, secondary = accent, tertiary = accent)
|
||||||
// the primary role from the accent so buttons/highlights carry the brand.
|
|
||||||
base.copy(primary = accent, secondary = accent)
|
|
||||||
} else {
|
} else {
|
||||||
base
|
ShardColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
|
shapes = ShardShapes,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,77 @@ package com.runicgateway.app.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
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
|
||||||
|
|
||||||
// Default Material 3 type scale for the skeleton; refined in the M5 design pass.
|
/**
|
||||||
|
* The M5 type scale. Three families carry the design (see [ui/theme/Font.kt]):
|
||||||
|
* [Cinzel] for the engraved display/headline/title roles, [AppSerif] (Noto Serif)
|
||||||
|
* for parchment body copy, and [AppSans] for the letter-spaced label/meta/button
|
||||||
|
* roles. Sizes and tracking mirror the "Runic Gateway Screens" mockup.
|
||||||
|
*/
|
||||||
val Typography = Typography(
|
val Typography = Typography(
|
||||||
|
// Display / headline / title — Cinzel engraved serif
|
||||||
|
displayLarge = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 40.sp, lineHeight = 46.sp, letterSpacing = 0.4.sp,
|
||||||
|
),
|
||||||
|
displayMedium = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.3.sp,
|
||||||
|
),
|
||||||
|
displaySmall = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.2.sp,
|
||||||
|
),
|
||||||
|
headlineLarge = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 26.sp, lineHeight = 34.sp, letterSpacing = 0.2.sp,
|
||||||
|
),
|
||||||
|
headlineMedium = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.2.sp,
|
||||||
|
),
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.2.sp,
|
||||||
|
),
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 20.sp, lineHeight = 26.sp, letterSpacing = 0.2.sp,
|
||||||
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 17.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp,
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 15.sp, lineHeight = 22.sp, letterSpacing = 0.1.sp,
|
||||||
|
),
|
||||||
|
// Body — parchment serif
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = AppSerif, fontWeight = FontWeight.Normal,
|
||||||
fontWeight = FontWeight.Normal,
|
fontSize = 16.sp, lineHeight = 26.sp, letterSpacing = 0.15.sp,
|
||||||
fontSize = 16.sp,
|
),
|
||||||
lineHeight = 24.sp,
|
bodyMedium = TextStyle(
|
||||||
letterSpacing = 0.5.sp,
|
fontFamily = AppSerif, fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 15.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp,
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontFamily = AppSerif, fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 13.sp, lineHeight = 20.sp, letterSpacing = 0.2.sp,
|
||||||
|
),
|
||||||
|
// Labels / meta / buttons — sans, letter-spaced
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontFamily = AppSans, fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 14.sp, lineHeight = 18.sp, letterSpacing = 0.45.sp,
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontFamily = AppSans, fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp,
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = AppSans, fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp, lineHeight = 15.sp, letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
app/src/main/res/font/cinzel_variable.ttf
Normal file
BIN
app/src/main/res/font/cinzel_variable.ttf
Normal file
Binary file not shown.
@@ -2,4 +2,7 @@
|
|||||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#1B1033</color>
|
<color name="ic_launcher_background">#1B1033</color>
|
||||||
|
<!-- M5 design pass: the shard-website page color, used as the launch window
|
||||||
|
background so the first frame matches the dark Compose theme (no white flash). -->
|
||||||
|
<color name="launch_background">#0B0F14</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -143,6 +143,7 @@
|
|||||||
<string name="player_char_governor">Governor of %1$s</string>
|
<string name="player_char_governor">Governor of %1$s</string>
|
||||||
<string name="player_char_guildmaster">Guildmaster, %1$s</string>
|
<string name="player_char_guildmaster">Guildmaster, %1$s</string>
|
||||||
<string name="player_char_attributes">Attributes</string>
|
<string name="player_char_attributes">Attributes</string>
|
||||||
|
<string name="player_char_vitals">Vitals</string>
|
||||||
<string name="player_char_str">STR</string>
|
<string name="player_char_str">STR</string>
|
||||||
<string name="player_char_dex">DEX</string>
|
<string name="player_char_dex">DEX</string>
|
||||||
<string name="player_char_int">INT</string>
|
<string name="player_char_int">INT</string>
|
||||||
|
|||||||
@@ -6,5 +6,11 @@
|
|||||||
background / status-bar behavior for launch.
|
background / status-bar behavior for launch.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<style name="Theme.RunicGateway" parent="android:Theme.Material.Light.NoActionBar" />
|
<!-- Dark launch theme (the app is dark-only, M5). Sets the window background to the
|
||||||
|
page color so the pre-Compose frame matches; Compose owns the live UI theme. -->
|
||||||
|
<style name="Theme.RunicGateway" parent="android:Theme.Material.NoActionBar">
|
||||||
|
<item name="android:windowBackground">@color/launch_background</item>
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user