feat(android): circular logo, synthwave backgrounds, glass modal, server names + UX fixes
- New circular badge logo (ic_logo) on Intro + Connect screens; launcher icon rebuilt as dark circle + white grid. - Reddish synthwave backdrop (bg-intro-2) behind Intro, Connect, and the remote/gamepad (edge-to-edge with a light scrim); controllers no longer paint an opaque fill over it. - Server name: added to ServerEntry/prefs, the Connect form, the modal add-form, and saved-server rows; removal now matches by connection identity (rename- and legacy-format-safe). - NESMenu modal restyled to glassmorphism #0A0A0A with centered, larger fields. Connect-form glass cards given a darker base for legibility. - Intro title/subtitle set to #FAFAFA. - Deleting the last server clears the active server and returns to Connect. - D-pad auto-repeat initial delay raised to 500ms so a tap sends one key (fixes doubled nav sound). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
75e470bfa4
commit
aa95e42383
@ -18,7 +18,11 @@ data class ServerEntry(
|
|||||||
val useHttps: Boolean,
|
val useHttps: Boolean,
|
||||||
val port: String = "",
|
val port: String = "",
|
||||||
val password: String = "",
|
val password: String = "",
|
||||||
|
val name: String = "",
|
||||||
) {
|
) {
|
||||||
|
/** Label to show in lists — the user-given name, or the address if unnamed. */
|
||||||
|
fun displayName(): String = name.ifBlank { address }
|
||||||
|
|
||||||
fun toUrl(): String {
|
fun toUrl(): String {
|
||||||
val scheme = if (useHttps) "https" else "http"
|
val scheme = if (useHttps) "https" else "http"
|
||||||
val portSuffix = if (port.isNotBlank()) ":$port" else ""
|
val portSuffix = if (port.isNotBlank()) ":$port" else ""
|
||||||
@ -31,7 +35,9 @@ data class ServerEntry(
|
|||||||
return "$scheme://$address$portSuffix"
|
return "$scheme://$address$portSuffix"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun serialize(): String = "$address|$useHttps|$port|$password"
|
// name is the trailing field so entries saved before naming existed
|
||||||
|
// (4 fields) still deserialize, with name defaulting to "".
|
||||||
|
fun serialize(): String = "$address|$useHttps|$port|$password|$name"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun deserialize(raw: String): ServerEntry? {
|
fun deserialize(raw: String): ServerEntry? {
|
||||||
@ -42,6 +48,7 @@ data class ServerEntry(
|
|||||||
useHttps = parts[1].toBooleanStrictOrNull() ?: false,
|
useHttps = parts[1].toBooleanStrictOrNull() ?: false,
|
||||||
port = parts.getOrElse(2) { "" },
|
port = parts.getOrElse(2) { "" },
|
||||||
password = parts.getOrElse(3) { "" },
|
password = parts.getOrElse(3) { "" },
|
||||||
|
name = parts.getOrElse(4) { "" },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,6 +60,7 @@ class ServerPreferences(private val context: Context) {
|
|||||||
private val activeHttpsKey = booleanPreferencesKey("active_https")
|
private val activeHttpsKey = booleanPreferencesKey("active_https")
|
||||||
private val activePortKey = stringPreferencesKey("active_port")
|
private val activePortKey = stringPreferencesKey("active_port")
|
||||||
private val activePasswordKey = stringPreferencesKey("active_password")
|
private val activePasswordKey = stringPreferencesKey("active_password")
|
||||||
|
private val activeNameKey = stringPreferencesKey("active_name")
|
||||||
private val savedServersKey = stringSetPreferencesKey("saved_servers")
|
private val savedServersKey = stringSetPreferencesKey("saved_servers")
|
||||||
private val introSeenKey = booleanPreferencesKey("intro_seen")
|
private val introSeenKey = booleanPreferencesKey("intro_seen")
|
||||||
|
|
||||||
@ -63,6 +71,7 @@ class ServerPreferences(private val context: Context) {
|
|||||||
useHttps = prefs[activeHttpsKey] ?: false,
|
useHttps = prefs[activeHttpsKey] ?: false,
|
||||||
port = prefs[activePortKey] ?: "",
|
port = prefs[activePortKey] ?: "",
|
||||||
password = prefs[activePasswordKey] ?: "",
|
password = prefs[activePasswordKey] ?: "",
|
||||||
|
name = prefs[activeNameKey] ?: "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +90,7 @@ class ServerPreferences(private val context: Context) {
|
|||||||
prefs[activeHttpsKey] = server.useHttps
|
prefs[activeHttpsKey] = server.useHttps
|
||||||
prefs[activePortKey] = server.port
|
prefs[activePortKey] = server.port
|
||||||
prefs[activePasswordKey] = server.password
|
prefs[activePasswordKey] = server.password
|
||||||
|
prefs[activeNameKey] = server.name
|
||||||
}
|
}
|
||||||
addSavedServer(server)
|
addSavedServer(server)
|
||||||
}
|
}
|
||||||
@ -91,6 +101,7 @@ class ServerPreferences(private val context: Context) {
|
|||||||
prefs.remove(activeHttpsKey)
|
prefs.remove(activeHttpsKey)
|
||||||
prefs.remove(activePortKey)
|
prefs.remove(activePortKey)
|
||||||
prefs.remove(activePasswordKey)
|
prefs.remove(activePasswordKey)
|
||||||
|
prefs.remove(activeNameKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +115,16 @@ class ServerPreferences(private val context: Context) {
|
|||||||
suspend fun removeSavedServer(server: ServerEntry) {
|
suspend fun removeSavedServer(server: ServerEntry) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
val current = prefs[savedServersKey] ?: emptySet()
|
val current = prefs[savedServersKey] ?: emptySet()
|
||||||
prefs[savedServersKey] = current - server.serialize()
|
// Match by connection identity (address/port/scheme) rather than the
|
||||||
|
// exact serialized string, so a rename — or the legacy 4-field format
|
||||||
|
// saved before names existed — still removes the right entry.
|
||||||
|
prefs[savedServersKey] = current.filterNot { raw ->
|
||||||
|
val e = ServerEntry.deserialize(raw)
|
||||||
|
e != null &&
|
||||||
|
e.address == server.address &&
|
||||||
|
e.port == server.port &&
|
||||||
|
e.useHttps == server.useHttps
|
||||||
|
}.toSet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -108,7 +108,9 @@ private fun Btn(icon: ImageVector, key: String, onDir: (String) -> Unit) {
|
|||||||
.pointerInput(key) {
|
.pointerInput(key) {
|
||||||
detectTapGestures(onPress = {
|
detectTapGestures(onPress = {
|
||||||
p = true; onDir(key)
|
p = true; onDir(key)
|
||||||
job = scope.launch { delay(350); while (true) { onDir(key); delay(100) } }
|
// 500ms initial delay so a normal tap sends one key, not two
|
||||||
|
// (a touch tap often exceeds 350ms → doubled nav sound).
|
||||||
|
job = scope.launch { delay(500); while (true) { onDir(key); delay(100) } }
|
||||||
tryAwaitRelease(); p = false; job?.cancel()
|
tryAwaitRelease(); p = false; job?.cancel()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -83,13 +83,15 @@ val ClassicPalette = NESPalette(
|
|||||||
inlayBg = Color(0xFF080808), inlayBorder = Color(0xFF999999),
|
inlayBg = Color(0xFF080808), inlayBorder = Color(0xFF999999),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Glassmorphism-black (OS design): #0A0A0A surfaces, subtle white-alpha borders,
|
||||||
|
// translucent-white buttons. Accents come from each button's ring.
|
||||||
val DarkPalette = NESPalette(
|
val DarkPalette = NESPalette(
|
||||||
body = NES.DarkBody, face = NES.DarkFace, ridge = NES.DarkRidge,
|
body = Color(0xFF0D0D0F), face = Color(0xFF0A0A0A), ridge = Color(0x14FFFFFF),
|
||||||
label = NES.DarkLabel, labelMuted = NES.DarkLabelMuted,
|
label = Color(0xFF9A9A9A), labelMuted = Color(0xFF777777),
|
||||||
dpad = Color(0xFF080808), dpadHi = Color(0xFF141418),
|
dpad = Color(0xFF202024), dpadHi = Color(0xFF33333A),
|
||||||
btn = NES.DarkButtonMain, btnPress = NES.DarkButtonMainPress,
|
btn = Color(0x14FFFFFF), btnPress = Color(0x0AFFFFFF),
|
||||||
capsule = Color(0xFF121216), capsulePress = Color(0xFF0A0A0C),
|
capsule = Color(0x12FFFFFF), capsulePress = Color(0x08FFFFFF),
|
||||||
inlayBg = Color(0xFF060608), inlayBorder = Color(0xFF444448),
|
inlayBg = Color(0xFF0A0A0A), inlayBorder = Color(0x1FFFFFFF),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun paletteFor(style: ControllerStyle) = if (style == ControllerStyle.CLASSIC) ClassicPalette else DarkPalette
|
fun paletteFor(style: ControllerStyle) = if (style == ControllerStyle.CLASSIC) ClassicPalette else DarkPalette
|
||||||
@ -113,7 +115,6 @@ fun NESController(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color(0xFF0C0C0C)) // Slightly lighter than black for shadow visibility
|
|
||||||
.twoFingerHold(onMenu)
|
.twoFingerHold(onMenu)
|
||||||
.padding(horizontal = 40.dp, vertical = 24.dp),
|
.padding(horizontal = 40.dp, vertical = 24.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
@ -193,13 +194,13 @@ fun NESController(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
// C on top (white)
|
// C on top
|
||||||
ColorBtn(Color(0xFF888888), Color(0xFFAAAAAA), 44.dp) { onKey("c") }
|
GlassFaceBtn("C", Color(0xFFBBBBBB), 44.dp) { onKey("c") }
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
// B + A on bottom row
|
// B + A on bottom row
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
ColorBtn(Color(0xFF3B82F6), Color(0xFF60A5FA), 44.dp) { onKey("b") }
|
GlassFaceBtn("B", Color(0xFF60A5FA), 44.dp) { onKey("b") }
|
||||||
ColorBtn(Color(0xFFEA580C), Color(0xFFFB923C), 44.dp) { onKey("a") }
|
GlassFaceBtn("A", Color(0xFFF7931A), 44.dp) { onKey("a") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,7 +265,9 @@ fun OnePointDPad(c: NESPalette, size: Dp, onDir: (String) -> Unit) {
|
|||||||
}
|
}
|
||||||
activeDir = dir; onDir(dir)
|
activeDir = dir; onDir(dir)
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
job = scope.launch { delay(300); while (true) { onDir(dir); delay(90) } }
|
// 500ms initial delay so a normal tap sends one key, not
|
||||||
|
// two (a touch tap often exceeds 300ms → doubled nav sound).
|
||||||
|
job = scope.launch { delay(500); while (true) { onDir(dir); delay(90) } }
|
||||||
tryAwaitRelease()
|
tryAwaitRelease()
|
||||||
job?.cancel(); activeDir = null
|
job?.cancel(); activeDir = null
|
||||||
},
|
},
|
||||||
@ -375,6 +378,28 @@ fun ColorBtn(color: Color, pressColor: Color, sz: Dp = 48.dp, onClick: () -> Uni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Glass face button — dark translucent fill, colored ring + letter (OS style) */
|
||||||
|
@Composable
|
||||||
|
fun GlassFaceBtn(label: String, accent: Color, sz: Dp = 44.dp, onClick: () -> Unit) {
|
||||||
|
var p by remember { mutableStateOf(false) }
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(sz)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
if (p) listOf(Color.White.copy(alpha = 0.05f), Color.White.copy(alpha = 0.02f))
|
||||||
|
else listOf(Color.White.copy(alpha = 0.10f), Color.White.copy(alpha = 0.03f))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.border(1.5.dp, accent.copy(alpha = if (p) 0.95f else 0.55f), CircleShape)
|
||||||
|
.pointerInput(Unit) { detectTapGestures(onPress = { p = true; onClick(); tryAwaitRelease(); p = false }) },
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(label, color = accent.copy(alpha = if (p) 1f else 0.85f), fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** START/SELECT capsule */
|
/** START/SELECT capsule */
|
||||||
@Composable
|
@Composable
|
||||||
fun CapsuleBtn(label: String, c: NESPalette, w: Dp = 64.dp, h: Dp = 28.dp, onClick: () -> Unit) {
|
fun CapsuleBtn(label: String, c: NESPalette, w: Dp = 64.dp, h: Dp = 28.dp, onClick: () -> Unit) {
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package com.archipelago.app.ui.components
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -34,17 +36,35 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.archipelago.app.data.ServerEntry
|
import com.archipelago.app.data.ServerEntry
|
||||||
|
import com.archipelago.app.ui.theme.BitcoinOrange
|
||||||
import com.archipelago.app.ui.theme.ControllerStyle
|
import com.archipelago.app.ui.theme.ControllerStyle
|
||||||
import com.archipelago.app.ui.theme.NES
|
import com.archipelago.app.ui.theme.SurfaceDark
|
||||||
|
import com.archipelago.app.ui.theme.TextMuted
|
||||||
|
import com.archipelago.app.ui.theme.TextPrimary
|
||||||
|
|
||||||
/** NES-styled modal menu — dark blue panel with white borders */
|
// Glassmorphism palette (OS design): near-black surfaces, subtle white borders,
|
||||||
|
// Bitcoin-orange accent.
|
||||||
|
private val PanelBg = SurfaceDark // #0A0A0A
|
||||||
|
private val PanelBorder = Color.White.copy(alpha = 0.12f)
|
||||||
|
private val RowBg = Color.White.copy(alpha = 0.05f)
|
||||||
|
private val RowBorder = Color.White.copy(alpha = 0.08f)
|
||||||
|
private val FieldBg = Color.White.copy(alpha = 0.04f)
|
||||||
|
|
||||||
|
private val PANEL_R = 20.dp
|
||||||
|
private val ROW_R = 14.dp
|
||||||
|
private val ROW_H = 54.dp
|
||||||
|
private val FIELD_H = 58.dp
|
||||||
|
|
||||||
|
/** Glassmorphism modal menu — #0A0A0A surface, subtle white borders. */
|
||||||
@Composable
|
@Composable
|
||||||
fun NESMenu(
|
fun NESMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
@ -66,7 +86,9 @@ fun NESMenu(
|
|||||||
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onDismiss() },
|
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onDismiss() },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
MenuPanel(servers, activeServer, isGamepadMode, controllerStyle, onDismiss, onSelectServer, onAddServer, onRemoveServer, onToggleMode, onToggleStyle, onBackToWebView)
|
AnimatedVisibility(visible = visible, enter = fadeIn() + scaleIn(initialScale = 0.95f), exit = fadeOut() + scaleOut(targetScale = 0.95f)) {
|
||||||
|
MenuPanel(servers, activeServer, isGamepadMode, controllerStyle, onDismiss, onSelectServer, onAddServer, onRemoveServer, onToggleMode, onToggleStyle, onBackToWebView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,29 +108,45 @@ private fun MenuPanel(
|
|||||||
onBackToWebView: (() -> Unit)?,
|
onBackToWebView: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
var showAdd by remember { mutableStateOf(false) }
|
var showAdd by remember { mutableStateOf(false) }
|
||||||
|
var nm by remember { mutableStateOf("") }
|
||||||
var addr by remember { mutableStateOf("") }
|
var addr by remember { mutableStateOf("") }
|
||||||
var pwd by remember { mutableStateOf("") }
|
var pwd by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
fun submit() {
|
||||||
|
if (addr.isNotBlank()) {
|
||||||
|
onAddServer(ServerEntry(addr, false, password = pwd, name = nm))
|
||||||
|
nm = ""; addr = ""; pwd = ""; showAdd = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(max = 360.dp)
|
.widthIn(max = 420.dp)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.padding(horizontal = 20.dp)
|
||||||
.background(NES.MenuPanel)
|
.clip(RoundedCornerShape(PANEL_R))
|
||||||
.border(3.dp, NES.MenuBorder, RoundedCornerShape(4.dp))
|
.background(PanelBg)
|
||||||
|
.border(1.dp, PanelBorder, RoundedCornerShape(PANEL_R))
|
||||||
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) {}
|
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) {}
|
||||||
.padding(16.dp),
|
.padding(22.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
// Title
|
// Title
|
||||||
Text("- MENU -", color = NES.MenuText, fontSize = 14.sp, fontWeight = FontWeight.Bold, letterSpacing = 4.sp,
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(), textAlign = androidx.compose.ui.text.style.TextAlign.Center)
|
"Menu",
|
||||||
Spacer(Modifier.height(4.dp))
|
color = TextPrimary,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
letterSpacing = 2.sp,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
|
|
||||||
// Servers
|
// Servers
|
||||||
servers.forEach { server ->
|
servers.forEach { server ->
|
||||||
val active = server.serialize() == activeServer?.serialize()
|
val active = server.serialize() == activeServer?.serialize()
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label = (if (active) "\u25B6 " else " ") + server.address,
|
label = server.displayName(),
|
||||||
selected = active,
|
selected = active,
|
||||||
onClick = { onSelectServer(server) },
|
onClick = { onSelectServer(server) },
|
||||||
onRemove = { onRemoveServer(server) },
|
onRemove = { onRemoveServer(server) },
|
||||||
@ -116,69 +154,70 @@ private fun MenuPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (servers.isEmpty()) {
|
if (servers.isEmpty()) {
|
||||||
Text(" NO SERVERS", color = NES.MenuMuted, fontSize = 11.sp, modifier = Modifier.padding(vertical = 4.dp))
|
Text("No servers", color = TextMuted, fontSize = 14.sp, modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add server
|
// Add server
|
||||||
if (showAdd) {
|
if (showAdd) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().background(Color.Black.copy(alpha = 0.3f)).padding(8.dp),
|
Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(ROW_R))
|
||||||
|
.background(FieldBg)
|
||||||
|
.border(1.dp, RowBorder, RoundedCornerShape(ROW_R))
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
GlassField(
|
||||||
value = addr, onValueChange = { addr = it.trim() },
|
value = nm, onValueChange = { nm = it },
|
||||||
placeholder = { Text("192.168.1.100", color = NES.MenuMuted, fontSize = 11.sp) },
|
placeholder = "Name (optional)",
|
||||||
modifier = Modifier.fillMaxWidth().height(48.dp), singleLine = true,
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next),
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(color = NES.MenuText, fontSize = 12.sp),
|
|
||||||
colors = nesFieldColors(),
|
|
||||||
shape = RoundedCornerShape(2.dp),
|
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
|
GlassField(
|
||||||
OutlinedTextField(
|
value = addr, onValueChange = { addr = it.trim() },
|
||||||
|
placeholder = "192.168.1.100",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next),
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
GlassField(
|
||||||
value = pwd, onValueChange = { pwd = it },
|
value = pwd, onValueChange = { pwd = it },
|
||||||
placeholder = { Text("PASSWORD", color = NES.MenuMuted, fontSize = 11.sp) },
|
placeholder = "Password",
|
||||||
modifier = Modifier.weight(1f).height(48.dp), singleLine = true,
|
modifier = Modifier.weight(1f),
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Go),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Go),
|
||||||
keyboardActions = KeyboardActions(onGo = {
|
keyboardActions = KeyboardActions(onGo = { submit() }),
|
||||||
if (addr.isNotBlank()) { onAddServer(ServerEntry(addr, false, password = pwd)); addr = ""; pwd = ""; showAdd = false }
|
|
||||||
}),
|
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(color = NES.MenuText, fontSize = 12.sp),
|
|
||||||
colors = nesFieldColors(),
|
|
||||||
shape = RoundedCornerShape(2.dp),
|
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
Modifier.size(48.dp).clip(RoundedCornerShape(2.dp)).background(NES.MenuSelected)
|
Modifier.size(FIELD_H).clip(RoundedCornerShape(12.dp)).background(BitcoinOrange.copy(alpha = 0.15f))
|
||||||
.clickable {
|
.border(1.dp, BitcoinOrange.copy(alpha = 0.4f), RoundedCornerShape(12.dp))
|
||||||
if (addr.isNotBlank()) { onAddServer(ServerEntry(addr, false, password = pwd)); addr = ""; pwd = ""; showAdd = false }
|
.clickable { submit() },
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) { Text("OK", color = NES.MenuText, fontSize = 10.sp, fontWeight = FontWeight.Bold) }
|
) { Text("OK", color = BitcoinOrange, fontSize = 14.sp, fontWeight = FontWeight.Bold) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MenuItem(label = " ADD SERVER", onClick = { showAdd = true })
|
MenuItem(label = "Add Server", labelColor = BitcoinOrange, onClick = { showAdd = true })
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
Box(Modifier.fillMaxWidth().height(1.dp).background(NES.MenuBorder.copy(alpha = 0.3f)))
|
Box(Modifier.fillMaxWidth().height(1.dp).background(PanelBorder))
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
|
|
||||||
// Mode toggle
|
// Mode toggle
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label = if (isGamepadMode) " SWITCH TO KEYBOARD" else " SWITCH TO GAMEPAD",
|
label = if (isGamepadMode) "Switch to Keyboard" else "Switch to Gamepad",
|
||||||
onClick = onToggleMode,
|
onClick = onToggleMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Style toggle
|
// Style toggle
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label = if (controllerStyle == ControllerStyle.CLASSIC) " STYLE: CLASSIC" else " STYLE: DARK",
|
label = if (controllerStyle == ControllerStyle.CLASSIC) "Style: Classic" else "Style: Dark",
|
||||||
onClick = onToggleStyle,
|
onClick = onToggleStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Back to dashboard
|
// Back to dashboard
|
||||||
if (onBackToWebView != null) {
|
if (onBackToWebView != null) {
|
||||||
MenuItem(label = " BACK TO DASHBOARD", onClick = onBackToWebView)
|
MenuItem(label = "Back to Dashboard", onClick = onBackToWebView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,32 +226,69 @@ private fun MenuPanel(
|
|||||||
private fun MenuItem(
|
private fun MenuItem(
|
||||||
label: String,
|
label: String,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
|
labelColor: Color = TextPrimary,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onRemove: (() -> Unit)? = null,
|
onRemove: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(32.dp)
|
.height(ROW_H)
|
||||||
.background(if (selected) NES.MenuSelected.copy(alpha = 0.15f) else Color.Transparent)
|
.clip(RoundedCornerShape(ROW_R))
|
||||||
|
.background(if (selected) BitcoinOrange.copy(alpha = 0.12f) else RowBg)
|
||||||
|
.border(1.dp, if (selected) BitcoinOrange.copy(alpha = 0.4f) else RowBorder, RoundedCornerShape(ROW_R))
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label, color = if (selected) NES.MenuSelected else NES.MenuText, fontSize = 11.sp, fontWeight = FontWeight.Medium)
|
Text(
|
||||||
|
label,
|
||||||
|
color = if (selected) BitcoinOrange else labelColor,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
if (onRemove != null) {
|
if (onRemove != null) {
|
||||||
Text("\u2715", color = NES.MenuMuted, fontSize = 10.sp,
|
Text(
|
||||||
modifier = Modifier.clickable { onRemove() }.padding(horizontal = 8.dp))
|
"✕",
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.clickable { onRemove() }.padding(horizontal = 8.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Glass text field with centered input text. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun nesFieldColors() = OutlinedTextFieldDefaults.colors(
|
private fun GlassField(
|
||||||
focusedBorderColor = NES.MenuBorder,
|
value: String,
|
||||||
unfocusedBorderColor = NES.MenuMuted,
|
onValueChange: (String) -> Unit,
|
||||||
cursorColor = NES.MenuText,
|
placeholder: String,
|
||||||
focusedTextColor = NES.MenuText,
|
modifier: Modifier = Modifier,
|
||||||
unfocusedTextColor = NES.MenuText,
|
visualTransformation: androidx.compose.ui.text.input.VisualTransformation = androidx.compose.ui.text.input.VisualTransformation.None,
|
||||||
)
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
placeholder = {
|
||||||
|
Text(placeholder, color = TextMuted, fontSize = 15.sp, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth().height(FIELD_H),
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
textStyle = TextStyle(color = TextPrimary, fontSize = 16.sp, textAlign = TextAlign.Center),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = Color.White.copy(alpha = 0.3f),
|
||||||
|
unfocusedBorderColor = Color.White.copy(alpha = 0.12f),
|
||||||
|
cursorColor = BitcoinOrange,
|
||||||
|
focusedTextColor = TextPrimary,
|
||||||
|
unfocusedTextColor = TextPrimary,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -50,7 +50,6 @@ fun NESPortraitController(
|
|||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color(0xFF0C0C0C))
|
|
||||||
.twoFingerHold(onMenu)
|
.twoFingerHold(onMenu)
|
||||||
.padding(horizontal = 40.dp, vertical = 24.dp),
|
.padding(horizontal = 40.dp, vertical = 24.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
@ -119,11 +118,11 @@ fun NESPortraitController(
|
|||||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
ColorBtn(Color(0xFF888888), Color(0xFFAAAAAA), 46.dp) { onKey("c") }
|
GlassFaceBtn("C", Color(0xFFBBBBBB), 46.dp) { onKey("c") }
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
ColorBtn(Color(0xFF3B82F6), Color(0xFF60A5FA), 46.dp) { onKey("b") }
|
GlassFaceBtn("B", Color(0xFF60A5FA), 46.dp) { onKey("b") }
|
||||||
ColorBtn(Color(0xFFEA580C), Color(0xFFFB923C), 46.dp) { onKey("a") }
|
GlassFaceBtn("A", Color(0xFFF7931A), 46.dp) { onKey("a") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ 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.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -41,7 +42,7 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@ -67,26 +68,45 @@ fun IntroScreen(onContinue: () -> Unit) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(SurfaceBlack)
|
.background(SurfaceBlack),
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
) {
|
||||||
|
// Reddish synthwave backdrop
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.bg_synthwave),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
// Dark scrim so the title/buttons stay legible over the art
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.55f),
|
||||||
|
Color.Black.copy(alpha = 0.35f),
|
||||||
|
Color.Black.copy(alpha = 0.75f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.padding(horizontal = 32.dp),
|
.padding(horizontal = 32.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
// Wide pixel-art logo
|
// Circular badge logo
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.ic_logo_wide),
|
painter = painterResource(id = R.drawable.ic_logo),
|
||||||
contentDescription = "Archipelago",
|
contentDescription = "Archipelago",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(160.dp)
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.alpha(logoAlpha.value),
|
.alpha(logoAlpha.value),
|
||||||
colorFilter = ColorFilter.tint(Color.White),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
@ -102,7 +122,7 @@ fun IntroScreen(onContinue: () -> Unit) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.welcome_title),
|
text = stringResource(R.string.welcome_title),
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
color = TextPrimary,
|
color = Color(0xFFFAFAFA),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -111,7 +131,7 @@ fun IntroScreen(onContinue: () -> Unit) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.welcome_subtitle),
|
text = stringResource(R.string.welcome_subtitle),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = TextMuted,
|
color = Color(0xFFFAFAFA),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
lineHeight = 26.sp,
|
lineHeight = 26.sp,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.archipelago.app.ui.screens
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -24,13 +25,17 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.archipelago.app.R
|
||||||
import com.archipelago.app.data.ServerPreferences
|
import com.archipelago.app.data.ServerPreferences
|
||||||
import com.archipelago.app.network.ConnectionState
|
import com.archipelago.app.network.ConnectionState
|
||||||
import com.archipelago.app.network.InputWebSocket
|
import com.archipelago.app.network.InputWebSocket
|
||||||
@ -58,7 +63,7 @@ fun RemoteInputScreen(onBack: () -> Unit) {
|
|||||||
|
|
||||||
var isGamepadMode by remember { mutableStateOf(true) }
|
var isGamepadMode by remember { mutableStateOf(true) }
|
||||||
var showModal by remember { mutableStateOf(false) }
|
var showModal by remember { mutableStateOf(false) }
|
||||||
var controllerStyle by remember { mutableStateOf(ControllerStyle.CLASSIC) }
|
var controllerStyle by remember { mutableStateOf(ControllerStyle.DARK) }
|
||||||
var playerId by remember { mutableStateOf(0) } // 0 = broadcast, 1 = P1, 2 = P2
|
var playerId by remember { mutableStateOf(0) } // 0 = broadcast, 1 = P1, 2 = P2
|
||||||
|
|
||||||
val ws = remember { InputWebSocket(scope) }
|
val ws = remember { InputWebSocket(scope) }
|
||||||
@ -113,9 +118,31 @@ fun RemoteInputScreen(onBack: () -> Unit) {
|
|||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color(0xFF0C0C0C))
|
.background(Color(0xFF0C0C0C)),
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
|
||||||
) {
|
) {
|
||||||
|
// Reddish synthwave backdrop behind the controller
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.bg_synthwave),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
// Light scrim — the controller body provides its own contrast, so keep
|
||||||
|
// this subtle and let the backdrop show through around it.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.4f),
|
||||||
|
Color.Black.copy(alpha = 0.25f),
|
||||||
|
Color.Black.copy(alpha = 0.45f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Box(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeDrawing)) {
|
||||||
when {
|
when {
|
||||||
isGamepadMode && isLandscape -> NESController(
|
isGamepadMode && isLandscape -> NESController(
|
||||||
style = controllerStyle,
|
style = controllerStyle,
|
||||||
@ -174,6 +201,7 @@ fun RemoteInputScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
NESMenu(
|
NESMenu(
|
||||||
visible = showModal,
|
visible = showModal,
|
||||||
@ -188,7 +216,20 @@ fun RemoteInputScreen(onBack: () -> Unit) {
|
|||||||
onAddServer = { server ->
|
onAddServer = { server ->
|
||||||
scope.launch { prefs.addSavedServer(server); if (activeServer == null) prefs.setActiveServer(server) }
|
scope.launch { prefs.addSavedServer(server); if (activeServer == null) prefs.setActiveServer(server) }
|
||||||
},
|
},
|
||||||
onRemoveServer = { server -> scope.launch { prefs.removeSavedServer(server) } },
|
onRemoveServer = { server ->
|
||||||
|
scope.launch {
|
||||||
|
prefs.removeSavedServer(server)
|
||||||
|
// Deleting the last server leaves nothing to control — drop the
|
||||||
|
// active server and return to the Connect screen.
|
||||||
|
val remaining = savedServers.count { it.serialize() != server.serialize() }
|
||||||
|
if (remaining == 0) {
|
||||||
|
ws.disconnect()
|
||||||
|
prefs.clearActiveServer()
|
||||||
|
showModal = false
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onToggleMode = { isGamepadMode = !isGamepadMode; showModal = false },
|
onToggleMode = { isGamepadMode = !isGamepadMode; showModal = false },
|
||||||
onToggleStyle = {
|
onToggleStyle = {
|
||||||
controllerStyle = if (controllerStyle == ControllerStyle.CLASSIC) ControllerStyle.DARK else ControllerStyle.CLASSIC
|
controllerStyle = if (controllerStyle == ControllerStyle.CLASSIC) ControllerStyle.DARK else ControllerStyle.CLASSIC
|
||||||
|
|||||||
@ -55,6 +55,7 @@ import androidx.compose.ui.draw.drawWithContent
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@ -97,6 +98,7 @@ fun ServerConnectScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val keyboard = LocalSoftwareKeyboardController.current
|
val keyboard = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
var address by remember { mutableStateOf("") }
|
var address by remember { mutableStateOf("") }
|
||||||
var port by remember { mutableStateOf("") }
|
var port by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
@ -132,12 +134,33 @@ fun ServerConnectScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(SurfaceBlack)
|
.background(SurfaceBlack),
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
|
||||||
) {
|
) {
|
||||||
|
// Reddish synthwave backdrop
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.bg_synthwave),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
// Dark scrim so the form stays legible over the art
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.6f),
|
||||||
|
Color.Black.copy(alpha = 0.45f),
|
||||||
|
Color.Black.copy(alpha = 0.8f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.verticalScroll(state = rememberScrollState())
|
.verticalScroll(state = rememberScrollState())
|
||||||
.drawWithContent { drawContent() }
|
.drawWithContent { drawContent() }
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
@ -145,14 +168,11 @@ fun ServerConnectScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
// Wide logo
|
// Circular badge logo
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.ic_logo_wide),
|
painter = painterResource(id = R.drawable.ic_logo),
|
||||||
contentDescription = "Archipelago",
|
contentDescription = "Archipelago",
|
||||||
modifier = Modifier
|
modifier = Modifier.size(96.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
colorFilter = ColorFilter.tint(Color.White),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@ -178,6 +198,7 @@ fun ServerConnectScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
@ -190,6 +211,34 @@ fun ServerConnectScreen(
|
|||||||
.padding(20.dp),
|
.padding(20.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = {
|
||||||
|
name = it
|
||||||
|
errorMessage = null
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.server_name_label)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.server_name_placeholder)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Next,
|
||||||
|
),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = Color.White.copy(alpha = 0.3f),
|
||||||
|
unfocusedBorderColor = Color.White.copy(alpha = 0.12f),
|
||||||
|
cursorColor = Color.White,
|
||||||
|
focusedLabelColor = Color.White.copy(alpha = 0.7f),
|
||||||
|
unfocusedLabelColor = TextMuted,
|
||||||
|
focusedTextColor = TextPrimary,
|
||||||
|
unfocusedTextColor = TextPrimary,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = address,
|
value = address,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@ -275,7 +324,7 @@ fun ServerConnectScreen(
|
|||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onGo = {
|
onGo = {
|
||||||
keyboard?.hide()
|
keyboard?.hide()
|
||||||
connect(ServerEntry(address, useHttps, port, password))
|
connect(ServerEntry(address, useHttps, port, password, name))
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
@ -345,7 +394,7 @@ fun ServerConnectScreen(
|
|||||||
text = if (isConnecting) stringResource(R.string.connecting) else stringResource(R.string.connect),
|
text = if (isConnecting) stringResource(R.string.connecting) else stringResource(R.string.connect),
|
||||||
onClick = {
|
onClick = {
|
||||||
keyboard?.hide()
|
keyboard?.hide()
|
||||||
connect(ServerEntry(address, useHttps, port, password))
|
connect(ServerEntry(address, useHttps, port, password, name))
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
)
|
)
|
||||||
@ -391,6 +440,7 @@ private fun SavedServerItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
@ -414,9 +464,15 @@ private fun SavedServerItem(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(text = server.address, style = MaterialTheme.typography.bodyMedium, color = TextPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(text = server.displayName(), style = MaterialTheme.typography.bodyMedium, color = TextPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
if (server.port.isNotBlank()) {
|
val secondary = buildString {
|
||||||
Text(text = "Port ${server.port}", style = MaterialTheme.typography.labelMedium, color = TextMuted)
|
if (server.name.isNotBlank()) append(server.address)
|
||||||
|
if (server.port.isNotBlank()) {
|
||||||
|
if (isNotEmpty()) append(":${server.port}") else append("Port ${server.port}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (secondary.isNotBlank()) {
|
||||||
|
Text(text = secondary, style = MaterialTheme.typography.labelMedium, color = TextMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Android/app/src/main/res/drawable/bg_synthwave.jpg
Normal file
BIN
Android/app/src/main/res/drawable/bg_synthwave.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 869 KiB |
@ -5,6 +5,6 @@
|
|||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#030202"
|
android:fillColor="#0A0A0A"
|
||||||
android:pathData="M0,0h108v108H0z" />
|
android:pathData="M0,0h108v108H0z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@ -1,45 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Archipelago pixel-art "A" logo — scaled 90% and centered -->
|
<!-- Archipelago pixel grid (from logo.svg), centered + scaled into the
|
||||||
|
adaptive-icon safe zone. Pairs with the dark circle background so the
|
||||||
|
launcher mask renders a clean circular badge. -->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="1024"
|
android:viewportWidth="752"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="752">
|
||||||
|
|
||||||
<group
|
<group
|
||||||
android:pivotX="512"
|
android:pivotX="376"
|
||||||
android:pivotY="512"
|
android:pivotY="376"
|
||||||
android:scaleX="0.55"
|
android:scaleX="0.72"
|
||||||
android:scaleY="0.55">
|
android:scaleY="0.72">
|
||||||
|
<path
|
||||||
<!-- Row 1: 4 blocks -->
|
android:fillColor="#FFFFFF"
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M357.614,318h71.007v70.936h-71.007z" />
|
android:pathData="M253.805,278.37V222.28H309.853V278.37H253.805ZM315.797,278.37V222.28H372.694V278.37H315.797ZM378.639,278.37V222.28H435.536V278.37H378.639ZM441.481,278.37V222.28H497.529V278.37H441.481ZM441.481,341.259V284.319H497.529V341.259H441.481ZM503.473,341.259V284.319H560.37V341.259H503.473ZM190.963,404.148V347.208H247.86V404.148H190.963ZM253.805,404.148V347.208H309.853V404.148H253.805ZM315.797,404.148V347.208H372.694V404.148H315.797ZM378.639,404.148V347.208H435.536V404.148H378.639ZM441.481,404.148V347.208H497.529V404.148H441.481ZM503.473,404.148V347.208H560.37V404.148H503.473ZM190.963,466.187V410.097H247.86V466.187H190.963ZM253.805,466.187V410.097H309.853V466.187H253.805ZM441.481,466.187V410.097H497.529V466.187H441.481ZM503.473,466.187V410.097H560.37V466.187H503.473ZM253.805,529.076V472.136H309.853V529.076H253.805ZM315.797,529.076V472.136H372.694V529.076H315.797ZM378.639,529.076V472.136H435.536V529.076H378.639ZM441.481,529.076V472.136H497.529V529.076H441.481Z" />
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M436.152,318h72.082v70.936h-72.082z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M515.766,318h72.082v70.936h-72.082z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M595.379,318h71.007v70.936h-71.007z" />
|
|
||||||
|
|
||||||
<!-- Row 2: 2 blocks (right side) -->
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M595.379,396.46h71.007v72.011h-71.007z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M673.917,396.46h72.083v72.011h-72.083z" />
|
|
||||||
|
|
||||||
<!-- Row 3: 6 blocks (full width) -->
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M278,475.994h72.083v72.012h-72.083z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M357.614,475.994h71.007v72.012h-71.007z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M436.152,475.994h72.082v72.012h-72.082z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M515.766,475.994h72.082v72.012h-72.082z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M595.379,475.994h71.007v72.012h-71.007z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M673.917,475.994h72.083v72.012h-72.083z" />
|
|
||||||
|
|
||||||
<!-- Row 4: 4 blocks (sides only — the "A" gap) -->
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M278,555.529h72.083v70.936h-72.083z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M357.614,555.529h71.007v70.936h-71.007z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M595.379,555.529h71.007v70.936h-71.007z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M673.917,555.529h72.083v70.936h-72.083z" />
|
|
||||||
|
|
||||||
<!-- Row 5: 4 blocks (bottom) -->
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M357.614,633.989h71.007v72.011h-71.007z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M436.152,633.989h72.082v72.011h-72.082z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M515.766,633.989h72.082v72.011h-72.082z" />
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M595.379,633.989h71.007v72.011h-71.007z" />
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
33
Android/app/src/main/res/drawable/ic_logo.xml
Normal file
33
Android/app/src/main/res/drawable/ic_logo.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Archipelago circular badge logo (from logo.svg):
|
||||||
|
dark circle with a black→grey gradient ring + white pixel-grid mark. -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="120dp"
|
||||||
|
android:height="120dp"
|
||||||
|
android:viewportWidth="752"
|
||||||
|
android:viewportHeight="752">
|
||||||
|
|
||||||
|
<!-- Ringed circle (circle converted to a path; stroke carries the gradient) -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#0A0A0A"
|
||||||
|
android:strokeWidth="22.8834"
|
||||||
|
android:pathData="M11.441,375.669a364.227,364.227 0 1,0 728.454,0a364.227,364.227 0 1,0 -728.454,0z">
|
||||||
|
<aapt:attr name="android:strokeColor">
|
||||||
|
<gradient
|
||||||
|
android:type="linear"
|
||||||
|
android:startX="751.337"
|
||||||
|
android:startY="751.338"
|
||||||
|
android:endX="0"
|
||||||
|
android:endY="0">
|
||||||
|
<item android:offset="0" android:color="#FF000000" />
|
||||||
|
<item android:offset="1" android:color="#FF666666" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<!-- White Archipelago pixel grid -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M253.805,278.37V222.28H309.853V278.37H253.805ZM315.797,278.37V222.28H372.694V278.37H315.797ZM378.639,278.37V222.28H435.536V278.37H378.639ZM441.481,278.37V222.28H497.529V278.37H441.481ZM441.481,341.259V284.319H497.529V341.259H441.481ZM503.473,341.259V284.319H560.37V341.259H503.473ZM190.963,404.148V347.208H247.86V404.148H190.963ZM253.805,404.148V347.208H309.853V404.148H253.805ZM315.797,404.148V347.208H372.694V404.148H315.797ZM378.639,404.148V347.208H435.536V404.148H378.639ZM441.481,404.148V347.208H497.529V404.148H441.481ZM503.473,404.148V347.208H560.37V404.148H503.473ZM190.963,466.187V410.097H247.86V466.187H190.963ZM253.805,466.187V410.097H309.853V466.187H253.805ZM441.481,466.187V410.097H497.529V466.187H441.481ZM503.473,466.187V410.097H560.37V466.187H503.473ZM253.805,529.076V472.136H309.853V529.076H253.805ZM315.797,529.076V472.136H372.694V529.076H315.797ZM378.639,529.076V472.136H435.536V529.076H378.639ZM441.481,529.076V472.136H497.529V529.076H441.481Z" />
|
||||||
|
</vector>
|
||||||
@ -23,4 +23,6 @@
|
|||||||
<string name="remote_input_hint">Use your phone as a keyboard and mouse for the kiosk</string>
|
<string name="remote_input_hint">Use your phone as a keyboard and mouse for the kiosk</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="open_in_browser">Open in browser</string>
|
<string name="open_in_browser">Open in browser</string>
|
||||||
|
<string name="server_name_label">Server Name (optional)</string>
|
||||||
|
<string name="server_name_placeholder">My Archipelago</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
10
Android/logo.svg
Normal file
10
Android/logo.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="752" height="752" viewBox="0 0 752 752" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="375.668" cy="375.669" r="364.227" fill="#0A0A0A" stroke="url(#paint0_linear_877_1990)" stroke-width="22.8834"/>
|
||||||
|
<path d="M253.805 278.37V222.28H309.853V278.37H253.805ZM315.797 278.37V222.28H372.694V278.37H315.797ZM378.639 278.37V222.28H435.536V278.37H378.639ZM441.481 278.37V222.28H497.529V278.37H441.481ZM441.481 341.259V284.319H497.529V341.259H441.481ZM503.473 341.259V284.319H560.37V341.259H503.473ZM190.963 404.148V347.208H247.86V404.148H190.963ZM253.805 404.148V347.208H309.853V404.148H253.805ZM315.797 404.148V347.208H372.694V404.148H315.797ZM378.639 404.148V347.208H435.536V404.148H378.639ZM441.481 404.148V347.208H497.529V404.148H441.481ZM503.473 404.148V347.208H560.37V404.148H503.473ZM190.963 466.187V410.097H247.86V466.187H190.963ZM253.805 466.187V410.097H309.853V466.187H253.805ZM441.481 466.187V410.097H497.529V466.187H441.481ZM503.473 466.187V410.097H560.37V466.187H503.473ZM253.805 529.076V472.136H309.853V529.076H253.805ZM315.797 529.076V472.136H372.694V529.076H315.797ZM378.639 529.076V472.136H435.536V529.076H378.639ZM441.481 529.076V472.136H497.529V529.076H441.481Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_877_1990" x1="751.337" y1="751.338" x2="0" y2="0.000976562" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop/>
|
||||||
|
<stop offset="1" stop-color="#666666"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Loading…
x
Reference in New Issue
Block a user