diff --git a/scripts/mobile-ui-design-system.mjs b/scripts/mobile-ui-design-system.mjs
index 701c29234d..36afa9a26b 100644
--- a/scripts/mobile-ui-design-system.mjs
+++ b/scripts/mobile-ui-design-system.mjs
@@ -255,6 +255,7 @@ function swiftWeight(weight) {
function swiftTextStyle(name) {
const styles = {
+ brand_wordmark: 'largeTitle',
display_large: 'largeTitle',
display_medium: 'title1',
display_small: 'title2',
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/MainActivity.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/MainActivity.kt
index 1347846aa5..bb9f0298a8 100644
--- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/MainActivity.kt
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/MainActivity.kt
@@ -1,6 +1,10 @@
package com.openbitfun.mobile.app
import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.openbitfun.mobile.app.ui.shell.StartupBrandReveal
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -17,9 +21,11 @@ import com.openbitfun.mobile.app.viewmodel.AppSettingsViewModel
import com.openbitfun.mobile.app.viewmodel.AppThemeMode
class MainActivity : ComponentActivity() {
+ private var showStartupBrand by mutableStateOf(true)
override fun onCreate(savedInstanceState: Bundle?) {
AppLocaleController.applySaved(this)
super.onCreate(savedInstanceState)
+ showStartupBrand = savedInstanceState == null
enableEdgeToEdge()
setContent {
if (intent.getBooleanExtra(DESIGN_PREVIEW_EXTRA, false)) {
@@ -35,8 +41,11 @@ class MainActivity : ComponentActivity() {
AppThemeMode.DARK -> true
}
OpenBitFunTheme(dark = dark) {
- MobileScreen()
- com.openbitfun.mobile.app.ui.shell.NotificationOnboarding()
+ Box {
+ MobileScreen()
+ if (showStartupBrand) StartupBrandReveal { showStartupBrand = false }
+ }
+ if (!showStartupBrand) com.openbitfun.mobile.app.ui.shell.NotificationOnboarding()
}
}
}
@@ -47,6 +56,7 @@ class MainActivity : ComponentActivity() {
}
override fun onStop() {
+ showStartupBrand = false
if (!intent.getBooleanExtra(DESIGN_PREVIEW_EXTRA, false)) accountModel().setBackground(true)
super.onStop()
}
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/PairingScreen.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/PairingScreen.kt
index f83a43a2dd..e3d2e92dcc 100644
--- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/PairingScreen.kt
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/PairingScreen.kt
@@ -1,6 +1,13 @@
package com.openbitfun.mobile.app.ui.remote
-import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.HorizontalDivider
+import com.openbitfun.mobile.app.ui.shell.WelcomeBrandFlow
+import com.openbitfun.mobile.app.ui.theme.generated.MobileDesignGeometry
+import com.openbitfun.mobile.core.feature.session.SessionTimePresentation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -13,8 +20,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Surface
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -28,15 +33,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.lifecycle.compose.LifecycleResumeEffect
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
import com.openbitfun.mobile.app.R
import com.openbitfun.mobile.app.ui.chat.ConversationView
import com.openbitfun.mobile.app.ui.common.CircleControl
@@ -48,44 +49,6 @@ import com.openbitfun.mobile.core.feature.session.RemoteSessionUiState
import com.openbitfun.mobile.core.feature.workspace.RemoteWorkspaceIntent
import com.openbitfun.mobile.core.feature.workspace.RemoteWorkspaceUiState
-/**
- * The remote surface: pair, then either the session list or one open session.
- *
- * Everything below the seam is a [PairingUiState]; this file decides layout and
- * wording and nothing else. The list and the conversation replace each other
- * rather than stacking, matching `pages/RemoteSurfaceHost.ets` — a transcript
- * needs the whole height and its own scroll.
- */
-@Composable
-internal fun PairingScreen(
- modifier: Modifier,
- settingsPlacement: SettingsPlacement,
- sessionDetailsPlacement: SettingsPlacement,
- viewSettingsPlacement: SettingsPlacement,
- onOpenRemoteSettings: () -> Unit,
- onOpenSidebar: (() -> Unit)? = null,
- onBack: () -> Unit = {},
- onOpenAccount: () -> Unit = {},
- onDeviceLink: (String) -> Unit = {},
- compact: Boolean = true,
- requestedSessionId: String? = null,
- creatingSession: Boolean = false,
- onOpenSession: (String) -> Unit = {},
- onCreateSession: () -> Unit = {},
- onRemoteHome: () -> Unit = {},
- startScanning: Boolean = false,
- onScanStarted: () -> Unit = {},
-) {
- ConnectView(
- onSubmit = onDeviceLink,
- onBack = onBack,
- onOpenAccount = onOpenAccount,
- startScanning = startScanning,
- onScanStarted = onScanStarted,
- modifier = modifier,
- )
-}
-
/** The account-device route, which bypasses the QR pairing form entirely. */
@Composable
internal fun AccountRemoteScreen(
@@ -221,135 +184,74 @@ private fun RemoteConnectedScreen(
onIntent = onSessionIntent,
modifier = modifier,
)
- } else if (compact) {
+ } else {
RemoteCompactHome(
remoteState = remoteState,
desktopName = desktopName,
onOpenSidebar = onOpenSidebar,
- onCreate = onCreateSession,
+ onBrowse = onOpenSidebar,
+ onOpen = { id ->
+ onSessionIntent(com.openbitfun.mobile.core.feature.session.RemoteSessionIntent.Open(id))
+ onOpenSession(id)
+ },
onOpenRemoteSettings = onOpenRemoteSettings,
modifier = modifier,
)
- } else {
- Column(modifier = modifier.fillMaxSize()) {
- RemoteShellHeader(onOpenSidebar, onOpenRemoteSettings = onOpenRemoteSettings)
- RemoteSessionListView(
- state = remoteState,
- workspaceState = workspaceState,
- compact = compact,
- sessionDetailsPlacement = sessionDetailsPlacement,
- viewSettingsPlacement = viewSettingsPlacement,
- connectionDetails = connectionDetails,
- onIntent = onSessionIntent,
- onWorkspaceIntent = onWorkspaceIntent,
- onOpen = onOpenSession,
- onCreate = onCreateSession,
- modifier = Modifier.weight(1f),
- )
- }
}
}
@Composable
-private fun RemoteCompactHome(
+internal fun RemoteCompactHome(
remoteState: RemoteSessionUiState,
desktopName: String,
onOpenSidebar: (() -> Unit)?,
- onCreate: () -> Unit,
+ onBrowse: (() -> Unit)?,
+ onOpen: (String) -> Unit,
onOpenRemoteSettings: () -> Unit,
- modifier: Modifier,
+ modifier: Modifier = Modifier,
) {
val ready = remoteState as? RemoteSessionUiState.Ready
- val hasSessions = ready?.sessions?.isNotEmpty() == true
- val loading = remoteState is RemoteSessionUiState.Loading
- val title = when {
- loading -> stringResource(R.string.sessions_loading)
- remoteState is RemoteSessionUiState.Failed -> stringResource(R.string.sessions_failed)
- hasSessions -> stringResource(R.string.remote_pick_session)
- else -> stringResource(R.string.remote_empty_title)
- }
- val body = if (hasSessions) {
- stringResource(R.string.remote_pick_session_text)
- } else {
- stringResource(R.string.remote_empty_text)
- }
-
- Column(modifier = modifier.fillMaxSize()) {
+ val recent = ready?.sessions.orEmpty().filter { it.status != "archived" }.sortedByDescending {
+ SessionTimePresentation.timestampMs(it.updatedAt)?.takeIf { t -> t > 0 }
+ ?: SessionTimePresentation.timestampMs(it.createdAt) ?: 0
+ }.take(3)
+ Column(modifier.fillMaxSize()) {
RemoteShellHeader(onOpenSidebar, desktopName, onOpenRemoteSettings)
Column(
- modifier = Modifier
- .weight(1f)
- .fillMaxWidth()
- .padding(start = 24.dp, end = 24.dp, bottom = 56.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
+ Modifier.weight(1f).align(Alignment.CenterHorizontally)
+ .widthIn(max = MobileDesignGeometry.RecentHomeMaxWidth).fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = MobileDesignGeometry.RecentHomeGutter, vertical = 24.dp),
) {
- if (loading) {
- CircularProgressIndicator(
- modifier = Modifier.padding(bottom = 18.dp).size(28.dp),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
+ WelcomeBrandFlow(Modifier.align(Alignment.CenterHorizontally).size(MobileDesignGeometry.RecentHomeMarkSize), sweep = true)
+ Text(stringResource(R.string.home_recent_title), fontSize = 25.sp,
+ fontWeight = FontWeight.Medium, textAlign = androidx.compose.ui.text.style.TextAlign.Center,
+ modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 32.dp))
+ if (remoteState is RemoteSessionUiState.Loading) CircularProgressIndicator(Modifier.size(24.dp))
+ if (remoteState is RemoteSessionUiState.Failed) {
+ Text(stringResource(R.string.sessions_failed), color = MaterialTheme.colorScheme.onSurfaceVariant)
}
- Text(
- title,
- fontSize = 20.sp,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface,
- textAlign = TextAlign.Center,
- )
- Text(
- body,
- fontSize = 14.sp,
- lineHeight = 21.sp,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = TextAlign.Center,
- modifier = Modifier.width(280.dp).padding(top = 10.dp),
- )
- if (!loading) {
- Button(
- onClick = onCreate,
- shape = RoundedCornerShape(23.dp),
- modifier = Modifier.padding(top = 12.dp).width(148.dp).height(46.dp),
- ) {
- Text(stringResource(R.string.remote_start_session), fontSize = 15.sp)
- }
+ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(stringResource(R.string.home_recent_recent), fontSize = 12.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f))
+ if (onBrowse != null) TextButton(onClick = onBrowse) { Text(stringResource(R.string.home_recent_all), fontSize = 12.sp) }
}
- }
- }
-}
-
-/** The remote landing page precedes the connection chooser, as on HarmonyOS. */
-@Composable
-internal fun DisconnectedRemoteHome(
- onOpenSidebar: (() -> Unit)?,
- onConnect: () -> Unit,
-) {
- Column(Modifier.fillMaxSize().testTag("remote-disconnected-home")) {
- RemoteShellHeader(onOpenSidebar = onOpenSidebar, onOpenRemoteSettings = null)
- Column(
- modifier = Modifier.weight(1f).fillMaxWidth()
- .padding(start = 20.dp, end = 20.dp, bottom = 48.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Surface(
- shape = RoundedCornerShape(24.dp),
- border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
- modifier = Modifier.size(74.dp),
- ) {
- Box(contentAlignment = Alignment.Center) {
- Icon(painterResource(R.drawable.ic_symbol_desktop), contentDescription = null,
- modifier = Modifier.size(42.dp))
+ recent.forEach { session ->
+ Column(Modifier.fillMaxWidth().clickable(enabled = !ready!!.busy) { onOpen(session.id) }
+ .padding(vertical = MobileDesignGeometry.RecentHomeRowPadding)) {
+ Text(session.title, fontSize = 15.sp, maxLines = 2)
+ val workspace = session.workspaceName?.takeIf { it.isNotBlank() }
+ ?: session.workspacePath.orEmpty().trimEnd('/').substringAfterLast('/')
+ Text(listOf(desktopName, workspace).filter { it.isNotBlank() }.joinToString(" · "),
+ fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(top = 6.dp), maxLines = 1)
}
+ HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
- Text(stringResource(R.string.sidebar_connect_desktop),
- style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
- Text(stringResource(R.string.remote_connect_description),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
- Button(onClick = onConnect, shape = RoundedCornerShape(22.dp),
- modifier = Modifier.width(136.dp).height(44.dp)) {
- Text(stringResource(R.string.pairing_connect), style = MaterialTheme.typography.titleSmall)
+ if (recent.isEmpty() && remoteState !is RemoteSessionUiState.Loading) {
+ Text(stringResource(if (desktopName.isBlank()) R.string.home_recent_connect else R.string.home_recent_empty),
+ fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(vertical = 18.dp))
}
}
}
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt
index a563501d93..ac0859c7f1 100644
--- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt
@@ -50,8 +50,7 @@ import com.openbitfun.mobile.app.ui.common.AdaptiveModalSurface
import com.openbitfun.mobile.app.ui.remote.AccountRemoteScreen
import com.openbitfun.mobile.app.ui.remote.ConnectAccountDeviceScreen
import com.openbitfun.mobile.app.ui.remote.FilePreviewSurface
-import com.openbitfun.mobile.app.ui.remote.DisconnectedRemoteHome
-import com.openbitfun.mobile.app.ui.remote.PairingScreen
+import com.openbitfun.mobile.app.ui.remote.ConnectView
import com.openbitfun.mobile.app.ui.settings.GeneralSettingsScreen
import com.openbitfun.mobile.app.ui.settings.SettingsScreen
import com.openbitfun.mobile.app.ui.shell.sidebar.AppSidebar
@@ -408,23 +407,22 @@ internal fun MobileScreen() {
)
RemoteControlSource.NONE -> if (readyAccount != null) {
- ConnectAccountDeviceScreen(
- state = readyAccount,
- onBack = { shell.openRemoteConnect() },
- onRefresh = { accountViewModel.dispatch(AccountIntent.RefreshDevices) },
- onSelect = accountViewModel::selectDevice,
- onOpenScanner = {
- shell.openRemoteScanner()
- },
- modifier = Modifier,
- )
- } else {
- DisconnectedRemoteHome(
- onOpenSidebar = if (showMenu) { { compactDrawerOpen = true } } else null,
- onConnect = shell::openRemoteConnect,
+ com.openbitfun.mobile.app.ui.remote.RemoteCompactHome(
+ remoteState = RemoteSessionUiState.Idle,
+ desktopName = "",
+ onOpenSidebar = { compactDrawerOpen = true },
+ onBrowse = { shell.openRemoteConnect() },
+ onOpen = {},
+ onOpenRemoteSettings = { shell.openRemoteConnect() },
)
-
- }
+ } else WelcomeHome(
+ signedIn = readyAccount != null,
+ onLogin = {
+ if (readyAccount != null) shell.openRemoteConnect() else shell.openAccount()
+ },
+ onScan = shell::openRemoteScanner,
+ modifier = Modifier.fillMaxSize(),
+ )
}
}
}
@@ -586,17 +584,12 @@ internal fun MobileScreen() {
onOpenScanner = shell::openRemoteScanner,
modifier = sheetModifier,
)
- } else PairingScreen(
- onDeviceLink = connectDeviceLink,
+ } else ConnectView(
+ onSubmit = connectDeviceLink,
modifier = sheetModifier,
- settingsPlacement = settingsPlacement,
- sessionDetailsPlacement = sessionDetailsPlacement,
- viewSettingsPlacement = remoteViewSettingsPlacement,
- onOpenRemoteSettings = { shell.openSettings(SettingsMode.REMOTE) },
onBack = shell::closeRemoteConnect,
onOpenAccount = { shell.closeRemoteConnect(); shell.openAccount() },
startScanning = shell.remoteScanRequested,
-
)
}
AdaptiveModalSurface(
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/StartupBrandReveal.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/StartupBrandReveal.kt
new file mode 100644
index 0000000000..1f67fc16de
--- /dev/null
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/StartupBrandReveal.kt
@@ -0,0 +1,118 @@
+package com.openbitfun.mobile.app.ui.shell
+
+import android.graphics.Paint
+import android.graphics.Typeface
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.openbitfun.mobile.app.ui.theme.generated.MobileDesignColors
+import com.openbitfun.mobile.app.ui.theme.generated.MobileDesignTypography
+import com.openbitfun.mobile.app.ui.theme.generated.MobileDesignMotion
+import kotlin.math.*
+
+/** Presentation-only cold-start timeline; Compose honors the system animator scale. */
+@Composable
+internal fun StartupBrandReveal(onFinished: () -> Unit) {
+ val progress = remember { Animatable(0f) }
+ val finish by rememberUpdatedState(onFinished)
+ val lifecycle = LocalLifecycleOwner.current.lifecycle
+ DisposableEffect(lifecycle) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_STOP) finish()
+ }
+ lifecycle.addObserver(observer)
+ onDispose { lifecycle.removeObserver(observer) }
+ }
+ LaunchedEffect(Unit) {
+ progress.animateTo(1f, tween(MobileDesignMotion.StartupBrand, easing = LinearEasing))
+ finish()
+ }
+ val p = progress.value
+ val t = min(p / .65f * .9f, .9f)
+ val settle = smooth((p - .66f) / .19f)
+ val ink = MaterialTheme.colorScheme.onBackground
+ val dotColor = MobileDesignColors.Light.BrandDot
+ val paint = remember { Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
+ textAlign = Paint.Align.CENTER
+ textSize = MobileDesignTypography.BrandWordmark.fontSize.value
+ } }
+ val widths = remember { listOf(30f,25f,24f,25f,28f,10f,16f,24f,25f,25f) }
+ val positions = remember { widths.runningFold(24f) { x, w -> x + w } }
+ BoxWithConstraints(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
+ .graphicsLayer { alpha = 1 - smooth((p - .97f) / .03f) }
+ .clearAndSetSemantics { }
+ .pointerInput(Unit) { awaitPointerEventScope { while (true) awaitPointerEvent().changes.forEach { it.consume() } } },
+ contentAlignment = Alignment.Center) {
+ val stageScale = min(1f, ((maxWidth.value - 32f) / 280f).coerceAtLeast(.1f))
+ Box(Modifier.size(280.dp, 240.dp).graphicsLayer { scaleX = stageScale; scaleY = stageScale }) {
+ val markProgress = ((p - .66f) / .19f).coerceIn(0f, 1f)
+ val q = markProgress - 1
+ val markScale = .65f + .35f * (1 + 2.2f*q*q*q + 1.2f*q*q)
+ WelcomeBrandFlow(
+ Modifier.offset(94.dp, 14.dp).size(92.dp).graphicsLayer {
+ alpha = ease(markProgress); scaleX = markScale; scaleY = markScale
+ })
+ Canvas(Modifier.fillMaxSize()) {
+ drawIntoCanvas { canvas ->
+ val native = canvas.nativeCanvas
+ native.save(); native.scale(size.width / 280f, size.height / 240f)
+ "OpenBıtFun".forEachIndexed { i, ch ->
+ val reveal = ease((t - (.19f + i * .048f)) / .05f)
+ val phase = ((t - (.19f + i * .048f)) / .085f).coerceIn(0f,1f)
+ val bounce = sin(phase * PI).toFloat() * (1-phase).pow(.65f)
+ val x = positions[i] + widths[i]/2
+ val y = 110 + 42*settle + 7*(1-reveal)-8*bounce
+ paint.color = ink.toArgb(); paint.alpha = (255*reveal).toInt()
+ native.save(); native.rotate((if(i%2==0) -1 else 1)*bounce*6, x, y)
+ native.drawText(ch.toString(), x, y-(paint.fontMetrics.ascent+paint.fontMetrics.descent)/2, paint)
+ native.restore()
+ }
+ var x = 9f; var y = 110f; var sx=1f; var sy=1f
+ for (i in 0..9) {
+ val end=.19f+i*.048f; val begin=if(i==0) end-.065f else end-.048f
+ val from=if(i==0) 9f else positions[i]+10; val to=positions[i+1]+10
+ if(t>=end){x=to;continue}
+ if(t>=begin){
+ val step=((t-begin)/(end-begin)).coerceIn(0f,1f)
+ val hop=((step-.16f)/.84f).coerceIn(0f,1f)
+ val squash=sin((step/.16f).coerceIn(0f,1f)*PI).toFloat()
+ x=from+(to-from)*smooth(hop); y-=4*hop*(1-hop)*(if(i==0)20 else 15)
+ sx=1+.2f*squash;sy=1-.18f*squash
+ }; break
+ }
+ if(t>=.70f){
+ val flight=((t-.70f)/.16f).coerceIn(0f,1f);val travel=smooth(flight)
+ x+=(positions[5]+5-x)*travel;y=110-15*travel-sin(PI*flight).toFloat()*44
+ sx=1+(7.35f/9-1)*travel;sy=sx
+ if(t>.86f&&t<.9f)y-=sin((t-.86f)/.04f*PI).toFloat()*2
+ }
+ y+=42*settle;paint.color=dotColor.toArgb()
+ val halo=sin(((t-.86f)/.04f).coerceIn(0f,1f)*PI).toFloat()
+ paint.alpha=(255*.16f*halo).toInt().coerceIn(0,255);native.drawCircle(x,y,11f,paint)
+ paint.alpha=(255*ease(t/.12f)).toInt();native.save();native.scale(sx,sy,x,y)
+ native.drawCircle(x,y,4.5f,paint);native.restore();native.restore()
+ }
+ }
+ }
+ }
+}
+private fun smooth(x: Float): Float { val p=x.coerceIn(0f,1f);return p*p*(3-2*p) }
+private fun ease(x: Float): Float = 1-(1-x.coerceIn(0f,1f)).pow(3)
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/WelcomeBrandFlow.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/WelcomeBrandFlow.kt
new file mode 100644
index 0000000000..7d40297d9a
--- /dev/null
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/WelcomeBrandFlow.kt
@@ -0,0 +1,74 @@
+package com.openbitfun.mobile.app.ui.shell
+
+import android.graphics.Paint
+import android.graphics.DashPathEffect
+import androidx.core.graphics.PathParser
+import androidx.compose.foundation.Canvas
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlin.coroutines.coroutineContext
+
+private val contourData = listOf(
+ "M154.860 59.584 Q167.500 59.584 173.820 70.531 L200.680 117.053 Q207.000 128.000 200.680 138.947 L173.820 185.469 Q167.500 196.416 154.860 196.416 L101.140 196.416 Q88.500 196.416 82.180 185.469 L55.320 138.947 Q49.000 128.000 55.320 117.053 L82.180 70.531 Q88.500 59.584 101.140 59.584 Z",
+ "M152.788 57.185 Q165.682 56.702 172.547 67.628 L201.722 114.060 Q208.586 124.985 202.557 136.392 L176.934 184.875 Q170.905 196.282 158.011 196.765 L103.212 198.815 Q90.318 199.298 83.453 188.372 L54.278 141.940 Q47.414 131.015 53.443 119.608 L79.066 71.125 Q85.095 59.718 97.989 59.235 Z",
+ "M150.574 54.847 Q163.702 53.863 171.119 64.741 L202.639 110.973 Q210.056 121.851 204.343 133.713 L180.066 184.126 Q174.353 195.988 161.224 196.972 L105.426 201.153 Q92.298 202.137 84.881 191.259 L53.361 145.027 Q45.944 134.149 51.657 122.287 L75.934 71.874 Q81.647 60.012 94.776 59.028 Z",
+ "M148.218 52.578 Q161.562 51.074 169.537 61.879 L203.427 107.798 Q211.401 118.603 206.031 130.911 L183.208 183.221 Q177.838 195.529 164.494 197.032 L107.782 203.422 Q94.438 204.926 86.463 194.121 L52.573 148.202 Q44.599 137.397 49.969 125.089 L72.792 72.779 Q78.162 60.471 91.506 58.968 Z",
+ "M145.724 50.384 Q159.263 48.344 167.799 59.048 L204.079 104.542 Q212.616 115.246 207.614 127.991 L186.355 182.157 Q181.353 194.902 167.814 196.943 L110.276 205.616 Q96.737 207.656 88.201 196.952 L51.921 151.458 Q43.384 140.754 48.386 128.009 L69.645 73.843 Q74.647 61.098 88.186 59.057 Z",
+ "M143.094 48.274 Q156.805 45.680 165.907 56.257 L204.592 101.209 Q213.694 111.786 209.085 124.957 L189.498 180.935 Q184.889 194.106 171.178 196.700 L112.906 207.726 Q99.195 210.320 90.093 199.743 L51.408 154.791 Q42.306 144.214 46.915 131.043 L66.502 75.065 Q71.111 61.894 84.822 59.300 Z",
+ "M140.330 46.254 Q154.191 43.091 163.861 53.512 L204.959 97.806 Q214.629 108.227 210.439 121.813 L192.629 179.551 Q188.438 193.137 174.578 196.301 L115.670 209.746 Q101.809 212.909 92.139 202.488 L51.041 158.194 Q41.371 147.773 45.561 134.187 L63.371 76.449 Q67.562 62.863 81.422 59.699 Z",
+ "M137.437 44.331 Q151.423 40.584 161.662 50.823 L205.177 94.338 Q215.416 104.577 211.669 118.563 L195.741 178.007 Q191.993 191.993 178.007 195.741 L118.563 211.669 Q104.577 215.416 94.338 205.177 L50.823 161.662 Q40.584 151.423 44.331 137.437 L60.259 77.993 Q64.007 64.007 77.993 60.259 Z",
+ "M134.416 42.513 Q148.504 38.167 159.311 48.195 L205.242 90.813 Q216.049 100.840 212.769 115.214 L198.826 176.300 Q195.545 190.673 181.458 195.019 L121.584 213.487 Q107.496 217.833 96.689 207.805 L50.758 165.187 Q39.951 155.160 43.231 140.786 L57.174 79.700 Q60.455 65.327 74.542 60.981 Z",
+ "M131.272 40.805 Q145.436 35.849 156.810 45.637 L205.149 87.237 Q216.523 97.025 213.733 111.769 L201.877 174.431 Q199.087 189.175 184.923 194.131 L124.728 215.195 Q110.564 220.151 99.190 210.363 L50.851 168.763 Q39.477 158.975 42.267 144.231 L54.123 81.569 Q56.913 66.825 71.077 61.869 Z",
+ "M128.010 39.216 Q142.223 33.637 154.160 43.157 L204.895 83.616 Q216.832 93.136 214.556 108.234 L204.885 172.401 Q202.609 187.499 188.396 193.077 L127.990 216.784 Q113.777 222.363 101.840 212.843 L51.105 172.384 Q39.168 162.864 41.444 147.766 L51.115 83.599 Q53.391 68.501 67.604 62.923 Z",
+ "M124.633 37.750 Q138.869 31.539 151.365 40.762 L204.475 79.959 Q216.972 89.182 215.233 104.616 L207.842 170.209 Q206.103 185.643 191.868 191.854 L131.367 218.250 Q117.131 224.461 104.635 215.238 L51.525 176.041 Q39.028 166.818 40.767 151.384 L48.158 85.791 Q49.897 70.357 64.132 64.146 Z",
+ "M121.147 36.415 Q135.377 29.562 148.427 38.459 L203.889 76.272 Q216.938 85.169 215.758 100.920 L210.742 167.858 Q209.562 183.608 195.331 190.461 L134.853 219.585 Q120.623 226.438 107.573 217.541 L52.111 179.728 Q39.062 170.831 40.242 155.080 L45.258 88.142 Q46.438 72.392 60.669 65.539 Z",
+ "M117.556 35.216 Q131.752 27.713 145.348 36.256 L203.131 72.563 Q216.727 81.106 216.127 97.152 L213.575 165.347 Q212.975 181.393 198.778 188.896 L138.444 220.784 Q124.248 228.287 110.652 219.744 L52.869 183.437 Q39.273 174.894 39.873 158.848 L42.425 90.653 Q43.025 74.607 57.222 67.104 Z",
+ "M113.866 34.160 Q128.000 26.000 142.134 34.160 L202.201 68.840 Q216.335 77.000 216.335 93.320 L216.335 162.680 Q216.335 179.000 202.201 187.160 L142.134 221.840 Q128.000 230.000 113.866 221.840 L53.799 187.160 Q39.665 179.000 39.665 162.680 L39.665 93.320 Q39.665 77.000 53.799 68.840 Z"
+)
+private val contourLengths = listOf(460.633f,470.212f,479.792f,489.371f,498.950f,508.529f,518.108f,527.687f,537.267f,546.846f,556.425f,566.004f,575.583f,585.163f,594.742f)
+@Composable
+internal fun WelcomeBrandFlow(modifier: Modifier, sweep: Boolean = false) {
+ val paths=remember { contourData.map { PathParser.createPathFromPathData(it)!! } }
+ val paint=remember { Paint(Paint.ANTI_ALIAS_FLAG).apply { style=Paint.Style.STROKE;strokeWidth=1f } }
+ var phase by remember { mutableFloatStateOf(0f) }
+ var moving by remember { mutableStateOf(false) }
+ val lifecycle=LocalLifecycleOwner.current.lifecycle
+ LaunchedEffect(lifecycle) {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ moving=coroutineContext[androidx.compose.ui.MotionDurationScale]?.scaleFactor!=0f
+ if(moving) while(isActive) { phase=(System.currentTimeMillis() % (if(sweep) 5000 else 18000)).toFloat() / (if(sweep) 5000f else 18000f);delay(33) }
+ }
+ }
+ val ink=MaterialTheme.colorScheme.onBackground
+ Canvas(modifier) {
+ drawIntoCanvas { canvas ->
+ val c=canvas.nativeCanvas;c.save();c.scale(size.width/256,size.height/256);paint.color=ink.toArgb()
+ if(sweep) {
+ val shift=(minOf(1f,phase/.75f)*2-1)*256
+ val colors=floatArrayOf(.16f,.25f,1f,.25f,.16f).map { alpha ->
+ ink.copy(alpha = if(moving) alpha else .5f).toArgb()
+ }.toIntArray()
+ paint.pathEffect=null;paint.alpha=255;paint.strokeWidth=1.45f
+ paint.shader=android.graphics.LinearGradient(shift,shift,256+shift,256+shift,colors,
+ floatArrayOf(0f,.46f,.55f,.65f,1f),android.graphics.Shader.TileMode.CLAMP)
+ paths.forEach { c.drawPath(it,paint) };paint.shader=null
+ } else paths.forEachIndexed { index,path ->
+ paint.pathEffect=null;paint.alpha=64;c.drawPath(path,paint)
+ if(moving) listOf(.34f,.26f,.18f).forEachIndexed { layer,length ->
+ val total=contourLengths[index]
+ paint.pathEffect=DashPathEffect(floatArrayOf(total*length/2,total*(1-length),total*length/2,0f),-total*(phase+index*.03f))
+ paint.alpha=(255*listOf(.12f,.14f,.36f)[layer]).toInt();c.drawPath(path,paint)
+ }
+ };c.restore()
+ }
+ }
+}
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/WelcomeHome.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/WelcomeHome.kt
new file mode 100644
index 0000000000..b0d7dd89cf
--- /dev/null
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/WelcomeHome.kt
@@ -0,0 +1,108 @@
+package com.openbitfun.mobile.app.ui.shell
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import com.openbitfun.mobile.app.R
+import com.openbitfun.mobile.app.ui.theme.generated.MobileDesignColors
+import com.openbitfun.mobile.app.ui.theme.generated.MobileDesignGeometry as G
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.isActive
+import kotlin.math.*
+
+/** A welcome layout, C text motion. Only presentation state is kept here. */
+@Composable
+internal fun WelcomeHome(onLogin: () -> Unit, onScan: () -> Unit, signedIn: Boolean = false, modifier: Modifier = Modifier) {
+ val phrases = listOf("OpenBitFun", stringResource(R.string.welcome_work), stringResource(R.string.welcome_play), stringResource(R.string.welcome_yours))
+ val elapsed = remember { Animatable(0f) }
+ val lifecycle = LocalLifecycleOwner.current.lifecycle
+ LaunchedEffect(lifecycle, phrases) {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ val reduced = currentCoroutineContext()[androidx.compose.ui.MotionDurationScale]?.scaleFactor == 0f
+ if (reduced) { elapsed.snapTo(2700f); return@repeatOnLifecycle }
+ while (currentCoroutineContext().isActive) {
+ elapsed.snapTo(0f)
+ elapsed.animateTo(phrases.sumOf { welcomeDuration(it) }.toFloat(), tween(phrases.sumOf { welcomeDuration(it) }, easing = LinearEasing))
+ }
+ }
+ }
+ var time = elapsed.value
+ var phrase = phrases.first()
+ for (item in phrases) { phrase = item; if (time < welcomeDuration(item)) break; time -= welcomeDuration(item) }
+ val ink = MaterialTheme.colorScheme.onBackground
+ BoxWithConstraints(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.TopCenter) {
+ val pageHeight = maxHeight.coerceAtLeast(540.dp)
+ Column(Modifier.widthIn(max = G.WelcomeMaxWidth).fillMaxWidth().verticalScroll(rememberScrollState()).heightIn(min = pageHeight)) {
+ Row(Modifier.fillMaxWidth().height(G.WelcomeHeaderHeight).padding(horizontal = G.WelcomeGutter), verticalAlignment = Alignment.CenterVertically) {
+ Row { "OpenBitFun".forEach { c -> WelcomeGlyph(c, G.WelcomeHeaderWordSize.value, Modifier) } }
+ }
+ Box(Modifier.fillMaxWidth().height((pageHeight - G.WelcomeHeaderHeight - 228.dp).coerceAtLeast(270.dp)), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.offset(y = (-28).dp)) {
+ WelcomeBrandFlow(Modifier.size(G.WelcomeMarkSize))
+ Spacer(Modifier.height(29.dp))
+ Row(Modifier.height(48.dp).clearAndSetSemantics { contentDescription = "OpenBitFun" }, verticalAlignment = Alignment.CenterVertically) {
+ phrase.forEachIndexed { index, c ->
+ val p = ((time - 180 - index * 55) / 800).coerceIn(0f, 1f)
+ val ease = 1 - (1-p).pow(3)
+ val bump = sin(p * PI).toFloat() * (1-p)
+ val out = ((time-welcomeDuration(phrase)+650)/650).coerceIn(0f,1f)
+ WelcomeGlyph(c, if(phrase.length>12) 27f else G.WelcomeWordSize.value, Modifier.graphicsLayer {
+ alpha = (p*3).coerceIn(0f,1f)*(1-out)
+ translationX = ((1-ease)*38-bump*5-out*out*30).dp.toPx()
+ scaleX = 1+bump*.045f; scaleY = scaleX
+ })
+ }
+ }
+ }
+ }
+ Column(Modifier.fillMaxWidth().background(MobileDesignColors.Light.WelcomeDock, RoundedCornerShape(topStart = G.WelcomeDockRadius, topEnd = G.WelcomeDockRadius))
+ .padding(start = G.WelcomeGutter, end = G.WelcomeGutter, top = G.WelcomeGutter, bottom = G.WelcomeDockBottom), verticalArrangement = Arrangement.spacedBy(G.WelcomeButtonGap)) {
+ WelcomeAction(stringResource(if (signedIn) R.string.sidebar_connect_desktop else R.string.welcome_login), onLogin)
+ WelcomeAction(stringResource(R.string.welcome_scan), onScan, scan = true)
+ // Reserve the optional MiniApp row; this host does not ship a MiniApp runtime yet.
+ Spacer(Modifier.height(44.dp))
+ }
+ }
+ }
+}
+
+@Composable
+private fun WelcomeAction(label: String, onClick: () -> Unit, scan: Boolean = false) {
+ Button(onClick, Modifier.fillMaxWidth().height(G.WelcomeButtonHeight), shape = RoundedCornerShape(28.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = MobileDesignColors.Light.WelcomeButton, contentColor = MobileDesignColors.Light.WelcomeButtonLabel)) {
+ if(scan) { Icon(painterResource(R.drawable.ic_symbol_qrcode_viewfinder), null, Modifier.size(20.dp)); Spacer(Modifier.width(10.dp)) }
+ Text(label, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
+ }
+}
+private fun welcomeDuration(word: String) = 250+word.length*185+(if(word=="OpenBitFun")600 else 0)+1800+650
+
+@Composable
+private fun WelcomeGlyph(letter: Char, fontSize: Float, modifier: Modifier) {
+ Text(if(letter=='i') "ı" else letter.toString(), fontSize = fontSize.sp,
+ fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground,
+ modifier = modifier.drawWithContent {
+ drawContent()
+ if(letter=='i') drawCircle(MobileDesignColors.Light.BrandDot, radius = fontSize.sp.toPx()*.07f,
+ center = Offset(size.width/2, fontSize.sp.toPx()*.25f))
+ })
+}
diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt
index d169a23b39..a35d5f5002 100644
--- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt
+++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt
@@ -52,6 +52,11 @@ internal object MobileDesignColors {
val CodeConstant = Color(0xFFA04444)
val CodeProperty = Color(0xFF466D78)
val CodeTargetBg = Color(0xFFFFF1BE)
+ val BrandDot = Color(0xFF16B9CE)
+ val WelcomeDock = Color(0xFF171917)
+ val WelcomeButton = Color(0xFFFFFFFF)
+ val WelcomeButtonLabel = Color(0xFF171917)
+ val WelcomeSecondary = Color(0xFFB9BCB9)
}
object Dark {
@@ -98,6 +103,11 @@ internal object MobileDesignColors {
val CodeConstant = Color(0xFFE79A9A)
val CodeProperty = Color(0xFF9CC8D0)
val CodeTargetBg = Color(0xFF5A4E24)
+ val BrandDot = Color(0xFF16B9CE)
+ val WelcomeDock = Color(0xFF171917)
+ val WelcomeButton = Color(0xFFFFFFFF)
+ val WelcomeButtonLabel = Color(0xFF171917)
+ val WelcomeSecondary = Color(0xFFB9BCB9)
}
}
@@ -118,6 +128,7 @@ internal object MobileDesignTypography {
val LabelLarge = TextStyle(fontSize = 15.sp, lineHeight = 20.sp, fontWeight = FontWeight.Medium)
val LabelMedium = TextStyle(fontSize = 14.sp, lineHeight = 18.sp, fontWeight = FontWeight.Medium)
val LabelSmall = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.Normal)
+ val BrandWordmark = TextStyle(fontSize = 42.sp, lineHeight = 56.sp, fontWeight = FontWeight.Medium)
}
internal object MobileDesignGeometry {
@@ -180,6 +191,22 @@ internal object MobileDesignGeometry {
val ModelListBottomPadding = 16.dp
val ModelEmptyAccountHeight = 80.dp
val ModelEditorHeight = 560.dp
+ val WelcomeMaxWidth = 520.dp
+ val WelcomeGutter = 25.dp
+ val WelcomeHeaderHeight = 58.dp
+ val WelcomeButtonHeight = 49.dp
+ val WelcomeButtonGap = 11.dp
+ val WelcomeDockRadius = 30.dp
+ val WelcomeDockBottom = 39.dp
+ val WelcomeMarkSize = 92.dp
+ val WelcomeWordSize = 34.dp
+ val WelcomeStageHeight = 220.dp
+ val WelcomeHeaderWordSize = 17.dp
+ val RecentHomeGutter = 24.dp
+ val RecentHomeMarkSize = 132.dp
+ val RecentHomeTitleSize = 25.dp
+ val RecentHomeMaxWidth = 560.dp
+ val RecentHomeRowPadding = 18.dp
}
internal object MobileDesignBreakpoints {
@@ -191,4 +218,5 @@ internal object MobileDesignBreakpoints {
internal object MobileDesignMotion {
const val Quick: Int = 180
const val Structure: Int = 220
+ const val StartupBrand: Int = 6800
}
diff --git a/src/apps/mobile/android/app/src/main/res/drawable/ic_symbol_qrcode_viewfinder.xml b/src/apps/mobile/android/app/src/main/res/drawable/ic_symbol_qrcode_viewfinder.xml
new file mode 100644
index 0000000000..be6a89e45b
--- /dev/null
+++ b/src/apps/mobile/android/app/src/main/res/drawable/ic_symbol_qrcode_viewfinder.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/apps/mobile/android/app/src/main/res/values-zh/strings.xml b/src/apps/mobile/android/app/src/main/res/values-zh/strings.xml
index fa4766330f..ac27e8d545 100644
--- a/src/apps/mobile/android/app/src/main/res/values-zh/strings.xml
+++ b/src/apps/mobile/android/app/src/main/res/values-zh/strings.xml
@@ -58,11 +58,11 @@
请输入有效的用户名和密码。
用户名或密码被拒绝。
登录尝试过多,请稍后再试。
- 账号中继暂时不可用。
- 无法连接账号中继。
+ 登录服务暂时不可用,请稍后重试。
+ 暂时无法连接,请检查网络后重试。
中继返回了无效的账号响应。
无法恢复安全账号会话。
- 账号中继没有及时响应。
+ 连接超时,请检查网络后重试。
通用聊天
更多
复制 Markdown
@@ -444,9 +444,19 @@
稍后
开启通知
允许 OpenBitFun 在任务完成时发送通知。你可以稍后在系统设置中更改。
- 扫描桌面端显示的二维码,开始远程处理任务。
在 OpenBitFun 桌面端点击「连接移动端」\n扫描二维码完成连接
需要相机权限才能扫码,请在系统设置中允许 OpenBitFun 访问相机,或改为手动配对。
无法打开相机,请检查权限后重试,或改为手动配对。
扫描二维码或粘贴远程连接链接后会显示桌面端连接状态。
+ 登录账号
+ 扫码连接电脑
+ 干点活儿
+ 玩点花样
+ 随你发挥
+ 今天,想做点什么?
+ 最近会话
+ 浏览工作区
+ 全部会话
+ 还没有可继续的对话
+ 连接电脑后,继续你的对话。
diff --git a/src/apps/mobile/android/app/src/main/res/values/strings.xml b/src/apps/mobile/android/app/src/main/res/values/strings.xml
index 28dfec51a8..0cb669d76f 100644
--- a/src/apps/mobile/android/app/src/main/res/values/strings.xml
+++ b/src/apps/mobile/android/app/src/main/res/values/strings.xml
@@ -60,10 +60,10 @@
Enter a valid username and password.
The username or password was rejected.
Too many sign-in attempts. Try again later.
- The account relay is unavailable.
- Could not reach the account relay.
+ Sign-in is temporarily unavailable. Try again later.
+ Unable to connect. Check your connection and try again.
The relay returned an invalid account response.
- The account relay did not answer in time.
+ Connection timed out. Check your connection and try again.
The secure account session could not be restored.
General Chat
More
@@ -459,9 +459,19 @@
Later
Enable notifications
Allow OpenBitFun to notify you when tasks finish. You can change this later in system settings.
- Scan the QR code shown on your desktop to start working remotely.
On OpenBitFun Desktop, tap Connect mobile\nthen scan the QR code
Camera access is required to scan. Allow OpenBitFun to use the camera in system settings, or pair manually.
The camera could not be opened. Check permissions and retry, or pair manually.
Scanning a QR code or pasting a remote connection link will show the desktop connection status.
+ Sign in
+ Scan to connect
+ Get things done
+ Try something fun
+ Make it yours
+ What would you like to do today?
+ Recent conversations
+ Browse workspaces
+ All conversations
+ No conversations to continue yet
+ Connect a computer to continue your conversations.
diff --git a/src/apps/mobile/design-system/README.md b/src/apps/mobile/design-system/README.md
index a86c546c63..9d4d9baad9 100644
--- a/src/apps/mobile/design-system/README.md
+++ b/src/apps/mobile/design-system/README.md
@@ -84,3 +84,79 @@ Valid scenario ids come from `scenarios/mobile-preview-scenarios.json`. Save
captures using the convention documented in `preview/snapshots/README.md`, then
open the desktop comparison surface to inspect them beside the HarmonyOS
baseline.
+
+## Cold-start brand motion
+
+HarmonyOS, Android and iOS render `startup_brand_reveal` natively. The 6.8-second
+sequence uses the `brand_wordmark` artwork typography and the dedicated
+`brand_dot` cyan, distinct from action and status colors. Platform soft sans
+fonts are used; the HTML prototype's macOS fonts are not redistributed. This
+decorative wordmark scales with its 280-unit stage, rather than Dynamic Type;
+normal app text continues to respect accessibility sizing.
+
+The cyan dot hops with the letter reveals, returns to the dotless i, then the
+shared contour mark expands above the word. Cold-launch presentation is
+independent of account and network readiness. Backgrounding removes it without
+replay, and system reduced-motion skips it. Notification onboarding waits until
+the overlay finishes. Design-preview launches bypass the startup overlay.
+
+Validate on a compact and wide native window, including a live resize where
+available, and check a background/foreground cycle during the reveal.
+
+## Signed-out welcome home
+
+The disconnected, signed-out home uses the A welcome composition with the C
+staggered-slide phrase animation: a fixed brand mark, desktop-derived localized
+short phrases, and equally weighted sign-in and scan actions in a dark dock.
+This is the signed-out home itself, not an onboarding layer before another
+landing page. Compact and wide hosts use the same entry on all three platforms:
+without a control target, show the welcome composition; account identity only
+changes its primary action from sign-in to connect. Connect opens the existing
+device chooser and scan opens the existing scanner. With a retained target,
+disconnect/reconnect preserves session context and retry controls instead of
+returning to welcome. An account reset can leave HarmonyOS disconnected rather
+than idle; either state leads to welcome once the control target is cleared.
+`welcome_*` geometry tokens own the compact dimensions and a 520-unit wide
+content cap. The four dedicated welcome colors keep the dark dock and white
+buttons stable in both appearance modes; normal page/action tokens invert or
+change surfaces and cannot express this fixed brand treatment.
+
+Each phrase enters with 55 ms per-glyph staggering and an 800 ms cubic ease-out,
+then leaves over 650 ms. Reduced motion renders a static wordmark. Background
+or invisible hosts suspend presentation work. Native safe-area and accessibility
+behavior remain platform-owned; these are logical layout units, not a promise
+of identical font rasterization on different systems.
+
+HarmonyOS exposes the bundled MiniApp destination. Android and iOS preserve the
+reserved footer area until they own that capability, rather than presenting an
+inert MiniApp action. QR actions still use the existing account-device protocol;
+this UI change does not add guest credentials or bypass server authorization.
+HarmonyOS can render the isolated surface with the existing design-preview
+launch parameter `openbitfunDesignPreview=welcome-home`, without logging out an
+active account or loading its connection state.
+
+The welcome and startup marks share the desktop AboutBrandMark contour geometry:
+15 rounded hexagonal paths with three low-opacity highlights traveling around
+each contour over 18 seconds. `assets/welcome-brand-contours.json` records the
+256-unit paths and lengths; native renderers own their drawing and lifecycle.
+The startup keeps its existing entrance choreography; the welcome mark stays
+in place while the highlights and short phrases loop. Reduced motion keeps the
+contours static. No microphone or voice-service dependency is introduced.
+
+### Recent-conversation home
+
+Signed-in home shows up to three non-archived sessions from the current remote
+device, ordered by valid update timestamp (creation time is the fallback). It
+labels the device and workspace without treating a loaded catalog as an active
+workspace selection. All conversations opens the existing sidebar; creation
+stays in the workspace-owned sidebar action. Signed-in users without a remote
+target see the same home with a connection entry, while signed-out users retain
+the welcome page. Loading and offline states never erase retained sessions.
+The mark uses a five-second diagonal highlight sweep; reduced motion uses a
+static contour. Native hosts retain their own lifecycle and adaptive layout.
+
+The recent-home brand and headline are centered above the leading-aligned session list. The mark retains its original silhouette and uses the same diagonal sweep across native surfaces. HarmonyOS uses a 156vp mark and 21fp headline for its optical balance; other hosts use the shared geometry.
+
+HarmonyOS welcome occupies the full window when signed out without an active remote target. It suppresses the workspace sidebar without changing retained selection. At 600vp and above, the brand, phrase and constrained action group are centered without the compact dock; smaller windows retain the stacked dock. The welcome mark is 156vp compact and 184vp wide, using the diagonal sweep. Size changes update this layout in place.
+
+For HarmonyOS welcome windows at least 840vp wide with width/height at least 1.2, a centered composition capped at 1000vp places the brand and actions side by side. This follows available window geometry rather than a device model or fold count.
diff --git a/src/apps/mobile/design-system/assets/welcome-brand-contours.json b/src/apps/mobile/design-system/assets/welcome-brand-contours.json
new file mode 100644
index 0000000000..6e5727b483
--- /dev/null
+++ b/src/apps/mobile/design-system/assets/welcome-brand-contours.json
@@ -0,0 +1,67 @@
+{
+ "viewBox": 256,
+ "cycleMs": 18000,
+ "source": "Desktop AboutBrandMark rounded six-corner ribbon, 15 fixed contours.",
+ "contours": [
+ {
+ "path": "M154.860 59.584 Q167.500 59.584 173.820 70.531 L200.680 117.053 Q207.000 128.000 200.680 138.947 L173.820 185.469 Q167.500 196.416 154.860 196.416 L101.140 196.416 Q88.500 196.416 82.180 185.469 L55.320 138.947 Q49.000 128.000 55.320 117.053 L82.180 70.531 Q88.500 59.584 101.140 59.584 Z",
+ "length": 460.63326438618736
+ },
+ {
+ "path": "M152.788 57.185 Q165.682 56.702 172.547 67.628 L201.722 114.060 Q208.586 124.985 202.557 136.392 L176.934 184.875 Q170.905 196.282 158.011 196.765 L103.212 198.815 Q90.318 199.298 83.453 188.372 L54.278 141.940 Q47.414 131.015 53.443 119.608 L79.066 71.125 Q85.095 59.718 97.989 59.235 Z",
+ "length": 470.2124371537121
+ },
+ {
+ "path": "M150.574 54.847 Q163.702 53.863 171.119 64.741 L202.639 110.973 Q210.056 121.851 204.343 133.713 L180.066 184.126 Q174.353 195.988 161.224 196.972 L105.426 201.153 Q92.298 202.137 84.881 191.259 L53.361 145.027 Q45.944 134.149 51.657 122.287 L75.934 71.874 Q81.647 60.012 94.776 59.028 Z",
+ "length": 479.79160992123684
+ },
+ {
+ "path": "M148.218 52.578 Q161.562 51.074 169.537 61.879 L203.427 107.798 Q211.401 118.603 206.031 130.911 L183.208 183.221 Q177.838 195.529 164.494 197.032 L107.782 203.422 Q94.438 204.926 86.463 194.121 L52.573 148.202 Q44.599 137.397 49.969 125.089 L72.792 72.779 Q78.162 60.471 91.506 58.968 Z",
+ "length": 489.3707826887615
+ },
+ {
+ "path": "M145.724 50.384 Q159.263 48.344 167.799 59.048 L204.079 104.542 Q212.616 115.246 207.614 127.991 L186.355 182.157 Q181.353 194.902 167.814 196.943 L110.276 205.616 Q96.737 207.656 88.201 196.952 L51.921 151.458 Q43.384 140.754 48.386 128.009 L69.645 73.843 Q74.647 61.098 88.186 59.057 Z",
+ "length": 498.94995545628615
+ },
+ {
+ "path": "M143.094 48.274 Q156.805 45.680 165.907 56.257 L204.592 101.209 Q213.694 111.786 209.085 124.957 L189.498 180.935 Q184.889 194.106 171.178 196.700 L112.906 207.726 Q99.195 210.320 90.093 199.743 L51.408 154.791 Q42.306 144.214 46.915 131.043 L66.502 75.065 Q71.111 61.894 84.822 59.300 Z",
+ "length": 508.5291282238109
+ },
+ {
+ "path": "M140.330 46.254 Q154.191 43.091 163.861 53.512 L204.959 97.806 Q214.629 108.227 210.439 121.813 L192.629 179.551 Q188.438 193.137 174.578 196.301 L115.670 209.746 Q101.809 212.909 92.139 202.488 L51.041 158.194 Q41.371 147.773 45.561 134.187 L63.371 76.449 Q67.562 62.863 81.422 59.699 Z",
+ "length": 518.1083009913356
+ },
+ {
+ "path": "M137.437 44.331 Q151.423 40.584 161.662 50.823 L205.177 94.338 Q215.416 104.577 211.669 118.563 L195.741 178.007 Q191.993 191.993 178.007 195.741 L118.563 211.669 Q104.577 215.416 94.338 205.177 L50.823 161.662 Q40.584 151.423 44.331 137.437 L60.259 77.993 Q64.007 64.007 77.993 60.259 Z",
+ "length": 527.6874737588603
+ },
+ {
+ "path": "M134.416 42.513 Q148.504 38.167 159.311 48.195 L205.242 90.813 Q216.049 100.840 212.769 115.214 L198.826 176.300 Q195.545 190.673 181.458 195.019 L121.584 213.487 Q107.496 217.833 96.689 207.805 L50.758 165.187 Q39.951 155.160 43.231 140.786 L57.174 79.700 Q60.455 65.327 74.542 60.981 Z",
+ "length": 537.2666465263849
+ },
+ {
+ "path": "M131.272 40.805 Q145.436 35.849 156.810 45.637 L205.149 87.237 Q216.523 97.025 213.733 111.769 L201.877 174.431 Q199.087 189.175 184.923 194.131 L124.728 215.195 Q110.564 220.151 99.190 210.363 L50.851 168.763 Q39.477 158.975 42.267 144.231 L54.123 81.569 Q56.913 66.825 71.077 61.869 Z",
+ "length": 546.8458192939097
+ },
+ {
+ "path": "M128.010 39.216 Q142.223 33.637 154.160 43.157 L204.895 83.616 Q216.832 93.136 214.556 108.234 L204.885 172.401 Q202.609 187.499 188.396 193.077 L127.990 216.784 Q113.777 222.363 101.840 212.843 L51.105 172.384 Q39.168 162.864 41.444 147.766 L51.115 83.599 Q53.391 68.501 67.604 62.923 Z",
+ "length": 556.4249920614343
+ },
+ {
+ "path": "M124.633 37.750 Q138.869 31.539 151.365 40.762 L204.475 79.959 Q216.972 89.182 215.233 104.616 L207.842 170.209 Q206.103 185.643 191.868 191.854 L131.367 218.250 Q117.131 224.461 104.635 215.238 L51.525 176.041 Q39.028 166.818 40.767 151.384 L48.158 85.791 Q49.897 70.357 64.132 64.146 Z",
+ "length": 566.004164828959
+ },
+ {
+ "path": "M121.147 36.415 Q135.377 29.562 148.427 38.459 L203.889 76.272 Q216.938 85.169 215.758 100.920 L210.742 167.858 Q209.562 183.608 195.331 190.461 L134.853 219.585 Q120.623 226.438 107.573 217.541 L52.111 179.728 Q39.062 170.831 40.242 155.080 L45.258 88.142 Q46.438 72.392 60.669 65.539 Z",
+ "length": 575.5833375964837
+ },
+ {
+ "path": "M117.556 35.216 Q131.752 27.713 145.348 36.256 L203.131 72.563 Q216.727 81.106 216.127 97.152 L213.575 165.347 Q212.975 181.393 198.778 188.896 L138.444 220.784 Q124.248 228.287 110.652 219.744 L52.869 183.437 Q39.273 174.894 39.873 158.848 L42.425 90.653 Q43.025 74.607 57.222 67.104 Z",
+ "length": 585.1625103640084
+ },
+ {
+ "path": "M113.866 34.160 Q128.000 26.000 142.134 34.160 L202.201 68.840 Q216.335 77.000 216.335 93.320 L216.335 162.680 Q216.335 179.000 202.201 187.160 L142.134 221.840 Q128.000 230.000 113.866 221.840 L53.799 187.160 Q39.665 179.000 39.665 162.680 L39.665 93.320 Q39.665 77.000 53.799 68.840 Z",
+ "length": 594.7416831315331
+ }
+ ]
+}
diff --git a/src/apps/mobile/design-system/components/mobile-components.json b/src/apps/mobile/design-system/components/mobile-components.json
index 66594e047d..fb854431bf 100644
--- a/src/apps/mobile/design-system/components/mobile-components.json
+++ b/src/apps/mobile/design-system/components/mobile-components.json
@@ -133,6 +133,34 @@
"states": ["loading", "text", "markdown", "image", "unsupported", "failed", "truncated"],
"tokens": ["sheet_header_height", "file_link", "card", "line", "soft"],
"platformNotes": "Placement is shared policy; rendering, file export, and platform-safe dismissal remain native adapters."
+ },
+ "miniapp_navigation": {
+ "purpose": "Opens locally bundled tools from primary sidebar navigation, without requiring account or device access.",
+ "anatomy": ["outlined_apps_glyph", "leading_aligned_label", "full_row_touch_target"],
+ "states": ["idle", "pressed", "focused", "signed_out", "connected"],
+ "tokens": ["ink", "body_large", "control_touch_size"],
+ "platformNotes": "Use a quiet 48vp icon-and-label row in compact and wide sidebars, with 16vp medium-weight navigation text, a 24vp icon slot and a 14vp gap. HarmonyOS uses the native outlined sys.symbol.square_grid_2x2 at 24vp, centered in its slot and tinted with the semantic ink color. Render the catalog inside the shell content pane so selecting it slides the right pane left as the drawer closes, using the existing 250ms ease-out shell motion. Sidebar session selection uses the same shell motion without a second route-push animation. Do not promote this navigation destination to a primary action on remote home."
+ },
+ "miniapp_catalog": {
+ "purpose": "Presents bundled apps with large visual previews that open each app directly.",
+ "anatomy": ["back_control", "centered_page_title", "offline_caption", "square_preview", "app_title"],
+ "states": ["catalog", "loading", "error", "app_open", "light", "dark"],
+ "tokens": ["page_bg", "ink", "muted", "soft", "body_large", "title_small", "body_medium", "label_small"],
+ "platformNotes": "Use a two-column library of square previews on compact screens and three columns when the gallery reaches 600vp. Clip previews to 24vp corners with 14vp column and 20vp row gaps; keep app names below images. The centered gallery caps at 1000vp, while open apps use full width. Reuse established showcase assets. Resize the grid without reloading an open app."
+ },
+ "welcome_home": {
+ "purpose": "Signed-out disconnected home: fixed brand mark, looping desktop phrases, equal login and QR actions.",
+ "anatomy": ["wordmark_header", "fixed_brand_mark", "sliding_phrase", "dark_action_dock", "login", "scan", "optional_bundled_miniapps"],
+ "states": ["signed_out", "reduce_motion", "compact", "wide"],
+ "tokens": ["welcome_max_width", "welcome_gutter", "welcome_header_height", "welcome_button_height", "welcome_button_gap", "welcome_dock_radius", "welcome_dock_bottom", "welcome_mark_size", "welcome_word_size", "welcome_stage_height", "welcome_header_word_size", "welcome_dock", "welcome_button", "welcome_button_label", "welcome_secondary", "brand_dot"],
+ "platformNotes": "A layout with C staggered slide motion. Mark never animates. Per-glyph entry: 180ms + index*55ms, 800ms cubic ease-out, 38px slide with 5px settling overshoot. Exit is 650ms, 30px left. Respect reduced motion and stop work offscreen. Retain real account/scan routing; only show MiniApps on hosts that implement them. Safe-area insets remain native."
+ },
+ "startup_brand_reveal": {
+ "purpose": "Provides a short desktop-inspired identity transition on cold launch without waiting for network or account loading.",
+ "anatomy": ["theme_background", "bouncing_brand_dot", "staggered_wordmark", "desktop_contour_brand_mark"],
+ "states": ["cold_start", "reduced_motion", "backgrounded"],
+ "tokens": ["page_bg", "ink", "brand_dot", "startup_brand"],
+ "platformNotes": "Native cold-start overlay on HarmonyOS, Android and iOS; 6800ms timeline independent of network readiness. A cyan dot hops ahead of ten 42-unit letters with subtle letter bounce. At normalized text time 0.70–0.86 it arcs back to the dotless i, settling at 7.35 units diameter with one fading halo. Text time is min(progress / 0.65 * 0.9, 0.9). Logo expands from 0.65 to 1 with a small overshoot during progress 0.66–0.85 as the word moves down 42 units. Use a centered 280×240 stage, scaled down for narrow windows, 92-unit contour mark on Android/iOS and a 156vp mark on HarmonyOS, with platform-native soft sans typography. HarmonyOS positions the mark at (62, -34) to preserve separation from the settled wordmark. Fade the overlay over the last 3%. Reserve full glyph slots; no layout changes during reveal. Remove on completion or background and do not replay on activity recreation/foreground. Skip for reduced motion. Notification onboarding follows completion. The dedicated brand_dot token preserves identity independently of action/status colors."
}
}
}
diff --git a/src/apps/mobile/design-system/preview/generated/mobile-design-data.js b/src/apps/mobile/design-system/preview/generated/mobile-design-data.js
index b4a0e3cd11..f4334ad465 100644
--- a/src/apps/mobile/design-system/preview/generated/mobile-design-data.js
+++ b/src/apps/mobile/design-system/preview/generated/mobile-design-data.js
@@ -177,6 +177,26 @@ export const mobileTokens = {
"code_target_bg": {
"light": "#FFF1BE",
"dark": "#5A4E24"
+ },
+ "brand_dot": {
+ "light": "#16B9CE",
+ "dark": "#16B9CE"
+ },
+ "welcome_dock": {
+ "light": "#171917",
+ "dark": "#171917"
+ },
+ "welcome_button": {
+ "light": "#FFFFFF",
+ "dark": "#FFFFFF"
+ },
+ "welcome_button_label": {
+ "light": "#171917",
+ "dark": "#171917"
+ },
+ "welcome_secondary": {
+ "light": "#B9BCB9",
+ "dark": "#B9BCB9"
}
},
"typography": {
@@ -259,6 +279,11 @@ export const mobileTokens = {
"size": 12,
"lineHeight": 16,
"weight": 400
+ },
+ "brand_wordmark": {
+ "size": 42,
+ "lineHeight": 56,
+ "weight": 500
}
},
"geometry": {
@@ -320,7 +345,23 @@ export const mobileTokens = {
"model_list_top_padding": 10,
"model_list_bottom_padding": 16,
"model_empty_account_height": 80,
- "model_editor_height": 560
+ "model_editor_height": 560,
+ "welcome_max_width": 520,
+ "welcome_gutter": 25,
+ "welcome_header_height": 58,
+ "welcome_button_height": 49,
+ "welcome_button_gap": 11,
+ "welcome_dock_radius": 30,
+ "welcome_dock_bottom": 39,
+ "welcome_mark_size": 92,
+ "welcome_word_size": 34,
+ "welcome_stage_height": 220,
+ "welcome_header_word_size": 17,
+ "recent_home_gutter": 24,
+ "recent_home_mark_size": 132,
+ "recent_home_title_size": 25,
+ "recent_home_max_width": 560,
+ "recent_home_row_padding": 18
},
"breakpoints": {
"wide": 600,
@@ -329,7 +370,8 @@ export const mobileTokens = {
},
"motion": {
"quick": 180,
- "structure": 220
+ "structure": 220,
+ "startup_brand": 6800
}
};
export const mobileComponents = {
@@ -853,6 +895,114 @@ export const mobileComponents = {
"soft"
],
"platformNotes": "Placement is shared policy; rendering, file export, and platform-safe dismissal remain native adapters."
+ },
+ "miniapp_navigation": {
+ "purpose": "Opens locally bundled tools from primary sidebar navigation, without requiring account or device access.",
+ "anatomy": [
+ "outlined_apps_glyph",
+ "leading_aligned_label",
+ "full_row_touch_target"
+ ],
+ "states": [
+ "idle",
+ "pressed",
+ "focused",
+ "signed_out",
+ "connected"
+ ],
+ "tokens": [
+ "ink",
+ "body_large",
+ "control_touch_size"
+ ],
+ "platformNotes": "Use a quiet 48vp icon-and-label row in compact and wide sidebars, with 16vp medium-weight navigation text, a 24vp icon slot and a 14vp gap. HarmonyOS uses the native outlined sys.symbol.square_grid_2x2 at 24vp, centered in its slot and tinted with the semantic ink color. Render the catalog inside the shell content pane so selecting it slides the right pane left as the drawer closes, using the existing 250ms ease-out shell motion. Sidebar session selection uses the same shell motion without a second route-push animation. Do not promote this navigation destination to a primary action on remote home."
+ },
+ "miniapp_catalog": {
+ "purpose": "Presents bundled apps with large visual previews that open each app directly.",
+ "anatomy": [
+ "back_control",
+ "centered_page_title",
+ "offline_caption",
+ "square_preview",
+ "app_title"
+ ],
+ "states": [
+ "catalog",
+ "loading",
+ "error",
+ "app_open",
+ "light",
+ "dark"
+ ],
+ "tokens": [
+ "page_bg",
+ "ink",
+ "muted",
+ "soft",
+ "body_large",
+ "title_small",
+ "body_medium",
+ "label_small"
+ ],
+ "platformNotes": "Use a two-column library of square previews on compact screens and three columns when the gallery reaches 600vp. Clip previews to 24vp corners with 14vp column and 20vp row gaps; keep app names below images. The centered gallery caps at 1000vp, while open apps use full width. Reuse established showcase assets. Resize the grid without reloading an open app."
+ },
+ "welcome_home": {
+ "purpose": "Signed-out disconnected home: fixed brand mark, looping desktop phrases, equal login and QR actions.",
+ "anatomy": [
+ "wordmark_header",
+ "fixed_brand_mark",
+ "sliding_phrase",
+ "dark_action_dock",
+ "login",
+ "scan",
+ "optional_bundled_miniapps"
+ ],
+ "states": [
+ "signed_out",
+ "reduce_motion",
+ "compact",
+ "wide"
+ ],
+ "tokens": [
+ "welcome_max_width",
+ "welcome_gutter",
+ "welcome_header_height",
+ "welcome_button_height",
+ "welcome_button_gap",
+ "welcome_dock_radius",
+ "welcome_dock_bottom",
+ "welcome_mark_size",
+ "welcome_word_size",
+ "welcome_stage_height",
+ "welcome_header_word_size",
+ "welcome_dock",
+ "welcome_button",
+ "welcome_button_label",
+ "welcome_secondary",
+ "brand_dot"
+ ],
+ "platformNotes": "A layout with C staggered slide motion. Mark never animates. Per-glyph entry: 180ms + index*55ms, 800ms cubic ease-out, 38px slide with 5px settling overshoot. Exit is 650ms, 30px left. Respect reduced motion and stop work offscreen. Retain real account/scan routing; only show MiniApps on hosts that implement them. Safe-area insets remain native."
+ },
+ "startup_brand_reveal": {
+ "purpose": "Provides a short desktop-inspired identity transition on cold launch without waiting for network or account loading.",
+ "anatomy": [
+ "theme_background",
+ "bouncing_brand_dot",
+ "staggered_wordmark",
+ "desktop_contour_brand_mark"
+ ],
+ "states": [
+ "cold_start",
+ "reduced_motion",
+ "backgrounded"
+ ],
+ "tokens": [
+ "page_bg",
+ "ink",
+ "brand_dot",
+ "startup_brand"
+ ],
+ "platformNotes": "Native cold-start overlay on HarmonyOS, Android and iOS; 6800ms timeline independent of network readiness. A cyan dot hops ahead of ten 42-unit letters with subtle letter bounce. At normalized text time 0.70–0.86 it arcs back to the dotless i, settling at 7.35 units diameter with one fading halo. Text time is min(progress / 0.65 * 0.9, 0.9). Logo expands from 0.65 to 1 with a small overshoot during progress 0.66–0.85 as the word moves down 42 units. Use a centered 280×240 stage, scaled down for narrow windows, 92-unit contour mark on Android/iOS and a 156vp mark on HarmonyOS, with platform-native soft sans typography. HarmonyOS positions the mark at (62, -34) to preserve separation from the settled wordmark. Fade the overlay over the last 3%. Reserve full glyph slots; no layout changes during reveal. Remove on completion or background and do not replay on activity recreation/foreground. Skip for reduced motion. Notification onboarding follows completion. The dedicated brand_dot token preserves identity independently of action/status colors."
}
}
};
diff --git a/src/apps/mobile/design-system/tokens/mobile-tokens.json b/src/apps/mobile/design-system/tokens/mobile-tokens.json
index 5b5e46b10d..a298153346 100644
--- a/src/apps/mobile/design-system/tokens/mobile-tokens.json
+++ b/src/apps/mobile/design-system/tokens/mobile-tokens.json
@@ -5,67 +5,285 @@
"signature": "Paper-and-ink conversation chrome with a single blue information accent"
},
"colors": {
- "start_window_background": { "light": "#FFFFFF", "dark": "#000000" },
- "transparent": { "light": "#00000000", "dark": "#00000000" },
- "page_bg": { "light": "#FFFFFF", "dark": "#151514" },
- "page_bg_fade": { "light": "#00FFFFFF", "dark": "#00151514" },
- "ink": { "light": "#171717", "dark": "#F4F3EF" },
- "muted": { "light": "#706F6A", "dark": "#AAA8A0" },
- "subtle": { "light": "#A5A39B", "dark": "#77756E" },
- "line": { "light": "#E9E7E2", "dark": "#363531" },
- "card": { "light": "#FFFFFF", "dark": "#252522" },
- "accent": { "light": "#111111", "dark": "#5B5954" },
- "file_link": { "light": "#2563EB", "dark": "#60A5FA" },
- "primary_action": { "light": "#111111", "dark": "#454540" },
- "content_on_action": { "light": "#FFFFFF", "dark": "#FFFFFF" },
- "connect_hero_bg": { "light": "#E6EDFF", "dark": "#2B2B29" },
- "connect_hero_accent": { "light": "#9DB4FF", "dark": "#4A4944" },
- "connect_hero_secondary": { "light": "#C9C5FF", "dark": "#3C3B38" },
- "connect_hero_surface": { "light": "#F8FAFF", "dark": "#252522" },
- "connect_scan_accent": { "light": "#FFD021", "dark": "#FFD021" },
- "scrim": { "light": "#44000000", "dark": "#44000000" },
- "shell_scrim": { "light": "#24000000", "dark": "#24000000" },
- "media_background": { "light": "#FF000000", "dark": "#FF000000" },
- "media_scrim": { "light": "#B8000000", "dark": "#B8000000" },
- "media_control_background": { "light": "#8C000000", "dark": "#8C000000" },
- "toast_background": { "light": "#D1171717", "dark": "#D1171717" },
- "shadow_faint": { "light": "#08000000", "dark": "#08000000" },
- "shadow_subtle": { "light": "#12000000", "dark": "#12000000" },
- "shadow_medium": { "light": "#18000000", "dark": "#18000000" },
- "shadow_strong": { "light": "#24000000", "dark": "#24000000" },
- "floating_border": { "light": "#18000000", "dark": "#18000000" },
- "soft": { "light": "#F4F3F0", "dark": "#2D2C28" },
- "floating_panel_bg": { "light": "#F7F7F5", "dark": "#1E1E1C" },
- "status_success": { "light": "#27C46A", "dark": "#3BD47B" },
- "status_danger": { "light": "#E04F4F", "dark": "#FF6B6B" },
- "code_line_number": { "light": "#AAA69D", "dark": "#77756E" },
- "code_keyword": { "light": "#8F3F71", "dark": "#D99AC4" },
- "code_string": { "light": "#477A4A", "dark": "#9BCB9D" },
- "code_number": { "light": "#9A5B13", "dark": "#E3B36D" },
- "code_comment": { "light": "#7A8078", "dark": "#96958D" },
- "code_function": { "light": "#2C6693", "dark": "#8CBCE0" },
- "code_type": { "light": "#865A20", "dark": "#D5B27F" },
- "code_constant": { "light": "#A04444", "dark": "#E79A9A" },
- "code_property": { "light": "#466D78", "dark": "#9CC8D0" },
- "code_target_bg": { "light": "#FFF1BE", "dark": "#5A4E24" }
+ "start_window_background": {
+ "light": "#FFFFFF",
+ "dark": "#000000"
+ },
+ "transparent": {
+ "light": "#00000000",
+ "dark": "#00000000"
+ },
+ "page_bg": {
+ "light": "#FFFFFF",
+ "dark": "#151514"
+ },
+ "page_bg_fade": {
+ "light": "#00FFFFFF",
+ "dark": "#00151514"
+ },
+ "ink": {
+ "light": "#171717",
+ "dark": "#F4F3EF"
+ },
+ "muted": {
+ "light": "#706F6A",
+ "dark": "#AAA8A0"
+ },
+ "subtle": {
+ "light": "#A5A39B",
+ "dark": "#77756E"
+ },
+ "line": {
+ "light": "#E9E7E2",
+ "dark": "#363531"
+ },
+ "card": {
+ "light": "#FFFFFF",
+ "dark": "#252522"
+ },
+ "accent": {
+ "light": "#111111",
+ "dark": "#5B5954"
+ },
+ "file_link": {
+ "light": "#2563EB",
+ "dark": "#60A5FA"
+ },
+ "primary_action": {
+ "light": "#111111",
+ "dark": "#454540"
+ },
+ "content_on_action": {
+ "light": "#FFFFFF",
+ "dark": "#FFFFFF"
+ },
+ "connect_hero_bg": {
+ "light": "#E6EDFF",
+ "dark": "#2B2B29"
+ },
+ "connect_hero_accent": {
+ "light": "#9DB4FF",
+ "dark": "#4A4944"
+ },
+ "connect_hero_secondary": {
+ "light": "#C9C5FF",
+ "dark": "#3C3B38"
+ },
+ "connect_hero_surface": {
+ "light": "#F8FAFF",
+ "dark": "#252522"
+ },
+ "connect_scan_accent": {
+ "light": "#FFD021",
+ "dark": "#FFD021"
+ },
+ "scrim": {
+ "light": "#44000000",
+ "dark": "#44000000"
+ },
+ "shell_scrim": {
+ "light": "#24000000",
+ "dark": "#24000000"
+ },
+ "media_background": {
+ "light": "#FF000000",
+ "dark": "#FF000000"
+ },
+ "media_scrim": {
+ "light": "#B8000000",
+ "dark": "#B8000000"
+ },
+ "media_control_background": {
+ "light": "#8C000000",
+ "dark": "#8C000000"
+ },
+ "toast_background": {
+ "light": "#D1171717",
+ "dark": "#D1171717"
+ },
+ "shadow_faint": {
+ "light": "#08000000",
+ "dark": "#08000000"
+ },
+ "shadow_subtle": {
+ "light": "#12000000",
+ "dark": "#12000000"
+ },
+ "shadow_medium": {
+ "light": "#18000000",
+ "dark": "#18000000"
+ },
+ "shadow_strong": {
+ "light": "#24000000",
+ "dark": "#24000000"
+ },
+ "floating_border": {
+ "light": "#18000000",
+ "dark": "#18000000"
+ },
+ "soft": {
+ "light": "#F4F3F0",
+ "dark": "#2D2C28"
+ },
+ "floating_panel_bg": {
+ "light": "#F7F7F5",
+ "dark": "#1E1E1C"
+ },
+ "status_success": {
+ "light": "#27C46A",
+ "dark": "#3BD47B"
+ },
+ "status_danger": {
+ "light": "#E04F4F",
+ "dark": "#FF6B6B"
+ },
+ "code_line_number": {
+ "light": "#AAA69D",
+ "dark": "#77756E"
+ },
+ "code_keyword": {
+ "light": "#8F3F71",
+ "dark": "#D99AC4"
+ },
+ "code_string": {
+ "light": "#477A4A",
+ "dark": "#9BCB9D"
+ },
+ "code_number": {
+ "light": "#9A5B13",
+ "dark": "#E3B36D"
+ },
+ "code_comment": {
+ "light": "#7A8078",
+ "dark": "#96958D"
+ },
+ "code_function": {
+ "light": "#2C6693",
+ "dark": "#8CBCE0"
+ },
+ "code_type": {
+ "light": "#865A20",
+ "dark": "#D5B27F"
+ },
+ "code_constant": {
+ "light": "#A04444",
+ "dark": "#E79A9A"
+ },
+ "code_property": {
+ "light": "#466D78",
+ "dark": "#9CC8D0"
+ },
+ "code_target_bg": {
+ "light": "#FFF1BE",
+ "dark": "#5A4E24"
+ },
+ "brand_dot": {
+ "light": "#16B9CE",
+ "dark": "#16B9CE"
+ },
+ "welcome_dock": {
+ "light": "#171917",
+ "dark": "#171917"
+ },
+ "welcome_button": {
+ "light": "#FFFFFF",
+ "dark": "#FFFFFF"
+ },
+ "welcome_button_label": {
+ "light": "#171917",
+ "dark": "#171917"
+ },
+ "welcome_secondary": {
+ "light": "#B9BCB9",
+ "dark": "#B9BCB9"
+ }
},
"typography": {
- "display_large": { "size": 24, "lineHeight": 30, "weight": 700 },
- "display_medium": { "size": 22, "lineHeight": 28, "weight": 700 },
- "display_small": { "size": 20, "lineHeight": 26, "weight": 700 },
- "headline_large": { "size": 22, "lineHeight": 28, "weight": 700 },
- "headline_medium": { "size": 20, "lineHeight": 26, "weight": 700 },
- "headline_small": { "size": 18, "lineHeight": 24, "weight": 700 },
- "title_large": { "size": 20, "lineHeight": 26, "weight": 700 },
- "conversation_header_title": { "size": 17, "lineHeight": 22, "weight": 600 },
- "title_medium": { "size": 17, "lineHeight": 22, "weight": 500 },
- "title_small": { "size": 15, "lineHeight": 20, "weight": 500 },
- "body_large": { "size": 16, "lineHeight": 24, "weight": 400 },
- "body_medium": { "size": 14, "lineHeight": 21, "weight": 400 },
- "body_small": { "size": 13, "lineHeight": 19, "weight": 400 },
- "label_large": { "size": 15, "lineHeight": 20, "weight": 500 },
- "label_medium": { "size": 14, "lineHeight": 18, "weight": 500 },
- "label_small": { "size": 12, "lineHeight": 16, "weight": 400 }
+ "display_large": {
+ "size": 24,
+ "lineHeight": 30,
+ "weight": 700
+ },
+ "display_medium": {
+ "size": 22,
+ "lineHeight": 28,
+ "weight": 700
+ },
+ "display_small": {
+ "size": 20,
+ "lineHeight": 26,
+ "weight": 700
+ },
+ "headline_large": {
+ "size": 22,
+ "lineHeight": 28,
+ "weight": 700
+ },
+ "headline_medium": {
+ "size": 20,
+ "lineHeight": 26,
+ "weight": 700
+ },
+ "headline_small": {
+ "size": 18,
+ "lineHeight": 24,
+ "weight": 700
+ },
+ "title_large": {
+ "size": 20,
+ "lineHeight": 26,
+ "weight": 700
+ },
+ "conversation_header_title": {
+ "size": 17,
+ "lineHeight": 22,
+ "weight": 600
+ },
+ "title_medium": {
+ "size": 17,
+ "lineHeight": 22,
+ "weight": 500
+ },
+ "title_small": {
+ "size": 15,
+ "lineHeight": 20,
+ "weight": 500
+ },
+ "body_large": {
+ "size": 16,
+ "lineHeight": 24,
+ "weight": 400
+ },
+ "body_medium": {
+ "size": 14,
+ "lineHeight": 21,
+ "weight": 400
+ },
+ "body_small": {
+ "size": 13,
+ "lineHeight": 19,
+ "weight": 400
+ },
+ "label_large": {
+ "size": 15,
+ "lineHeight": 20,
+ "weight": 500
+ },
+ "label_medium": {
+ "size": 14,
+ "lineHeight": 18,
+ "weight": 500
+ },
+ "label_small": {
+ "size": 12,
+ "lineHeight": 16,
+ "weight": 400
+ },
+ "brand_wordmark": {
+ "size": 42,
+ "lineHeight": 56,
+ "weight": 500
+ }
},
"geometry": {
"conversation_header_height": 76,
@@ -126,7 +344,23 @@
"model_list_top_padding": 10,
"model_list_bottom_padding": 16,
"model_empty_account_height": 80,
- "model_editor_height": 560
+ "model_editor_height": 560,
+ "welcome_max_width": 520,
+ "welcome_gutter": 25,
+ "welcome_header_height": 58,
+ "welcome_button_height": 49,
+ "welcome_button_gap": 11,
+ "welcome_dock_radius": 30,
+ "welcome_dock_bottom": 39,
+ "welcome_mark_size": 92,
+ "welcome_word_size": 34,
+ "welcome_stage_height": 220,
+ "welcome_header_word_size": 17,
+ "recent_home_gutter": 24,
+ "recent_home_mark_size": 132,
+ "recent_home_title_size": 25,
+ "recent_home_max_width": 560,
+ "recent_home_row_padding": 18
},
"breakpoints": {
"wide": 600,
@@ -135,6 +369,7 @@
},
"motion": {
"quick": 180,
- "structure": 220
+ "structure": 220,
+ "startup_brand": 6800
}
}
diff --git a/src/apps/mobile/harmonyos/.gitignore b/src/apps/mobile/harmonyos/.gitignore
index aedfdbe8ea..42c31716c8 100644
--- a/src/apps/mobile/harmonyos/.gitignore
+++ b/src/apps/mobile/harmonyos/.gitignore
@@ -29,3 +29,6 @@
*.pem
*.hap
*.app
+
+# Generated from shared desktop MiniApp sources by entry/hvigorfile.ts
+/entry/src/main/resources/rawfile/miniapps/
diff --git a/src/apps/mobile/harmonyos/AGENTS.md b/src/apps/mobile/harmonyos/AGENTS.md
index f447897f62..966318bf79 100644
--- a/src/apps/mobile/harmonyos/AGENTS.md
+++ b/src/apps/mobile/harmonyos/AGENTS.md
@@ -69,6 +69,7 @@ through `LocaleController`. Do not import Web UI or mobile-web locale catalogs.
The current local HarmonyOS verification loop is:
```bash
+node --test miniapps/*.test.cjs # bundled sources, browser bridge, and local storage compatibility
source scripts/ohos-env.sh
"$HVIGORW" --mode module -p product=default -p module=entry@default assembleHap --no-daemon
"$HVIGORW" --mode module -p module=entry@default -p ohos.test.type=LocalTest test --no-daemon
diff --git a/src/apps/mobile/harmonyos/README.md b/src/apps/mobile/harmonyos/README.md
index b1621fbaee..48e1295b46 100644
--- a/src/apps/mobile/harmonyos/README.md
+++ b/src/apps/mobile/harmonyos/README.md
@@ -43,3 +43,56 @@ a local signing identity in DevEco Studio when installing the app on a device.
The current project targets HarmonyOS `6.1.1(24)` and supports
`6.0.1(21)` or newer on phone and tablet devices.
+
+## Built-in MiniApps
+
+Choose **MiniApps** from the sidebar to open Gomoku, Regex Playground,
+or Daily Divination. The gallery uses two columns of rounded showcase previews
+on phones and three on wide screens; tapping a preview opens the app directly.
+Showcase images reuse the existing desktop artwork under
+`src/web-ui/src/assets/miniapps/showcases/`. Navigation typography follows the
+shared mobile body role, with the same proportions on compact and wide hosts.
+
+These bundled pages run locally in ArkWeb without signing
+in, pairing, a desktop connection, or internet access. Storage and clipboard
+writes use the phone. Data is isolated by app and retained across app upgrades;
+it is not synchronized with desktop MiniApps. Closing a page discards its
+unsaved page state. Changing desktop targets has no effect on these local tools.
+
+Every IDE/CLI HAP build runs `miniapps/generate.cjs` through
+`entry/hvigorfile.ts`. It packages the existing shared MiniApp sources and
+canonical appearance tokens into generated raw resources; do not edit those
+outputs. `miniapps/mobile.css` owns phone-only layout adaptation. The pages
+follow the app language and system light/dark appearance. They resize in place
+for compact, wide, and folded/unfolded layouts.
+
+The sandboxed iframe has no network access or local-file access. The native
+bridge permits only per-app storage keys and clipboard writes; it has no generic
+invoke, Worker, shell, workspace, AI, or Agent API. Local records are replaced
+atomically, and malformed records are retained and return an error. These local
+tools do not read any remote workspace or peer content, and do not execute jobs
+on CLI/Detached Dispatch hosts. Existing Remote Connect protocols are unchanged.
+Desktop catalogs, page transfer, and remote MiniApp execution are deferred.
+
+Run the local test and HAP build commands in `AGENTS.md`, plus:
+
+```bash
+node --test miniapps/*.test.cjs
+```
+
+Before claiming device verification, exercise local/offline launches, saved data
+after relaunch, clipboard, keyboard input, compact/wide layouts, light/dark
+appearance, and a live resize/fold transition on supported hardware.
+
+### Home and launch presentation
+
+Cold launches reuse the desktop contour mark and reveal the OpenBitFun wordmark
+letter by letter in approximately 1.3 seconds. This presentation does not wait
+for network/account initialization, is dismissed when backgrounded, and does not
+replay on foreground. The system reduced-motion preference skips it where the
+platform exposes that preference (API 23+). Native launch artwork and the app
+surface both follow the system light/dark theme.
+
+The welcome surface keeps connection actions at the bottom and offers bundled
+MiniApps without a desktop connection. Connected, connecting, and empty wide
+surfaces retain their existing remote capability and session routing behavior.
diff --git a/src/apps/mobile/harmonyos/entry/hvigorfile.ts b/src/apps/mobile/harmonyos/entry/hvigorfile.ts
index b0e3a1ab98..91d8ca97dd 100644
--- a/src/apps/mobile/harmonyos/entry/hvigorfile.ts
+++ b/src/apps/mobile/harmonyos/entry/hvigorfile.ts
@@ -1,5 +1,8 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
+// Include the current shared sources on every IDE and CLI build.
+require('../miniapps/generate.cjs').generate();
+
export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets
index 707161e88f..a0ee390a78 100644
--- a/src/apps/mobile/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets
+++ b/src/apps/mobile/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets
@@ -13,7 +13,7 @@ export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const scenarioId = want.parameters?.['openbitfunDesignPreview'];
- if (scenarioId === 'connected-conversation' ||
+ if (scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
scenarioId === 'streaming-dark' ||
scenarioId === 'reconnecting-wide' || scenarioId === 'narrow-multiline' ||
scenarioId === 'fold-context' || scenarioId === 'long-reading') {
diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets
index 526ee74b0d..9f53b6ca8c 100644
--- a/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets
+++ b/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets
@@ -54,6 +54,11 @@ export class MobileDesignColors {
static readonly codeConstant: MobileColorPair = new MobileColorPair("#A04444", "#E79A9A");
static readonly codeProperty: MobileColorPair = new MobileColorPair("#466D78", "#9CC8D0");
static readonly codeTargetBg: MobileColorPair = new MobileColorPair("#FFF1BE", "#5A4E24");
+ static readonly brandDot: MobileColorPair = new MobileColorPair("#16B9CE", "#16B9CE");
+ static readonly welcomeDock: MobileColorPair = new MobileColorPair("#171917", "#171917");
+ static readonly welcomeButton: MobileColorPair = new MobileColorPair("#FFFFFF", "#FFFFFF");
+ static readonly welcomeButtonLabel: MobileColorPair = new MobileColorPair("#171917", "#171917");
+ static readonly welcomeSecondary: MobileColorPair = new MobileColorPair("#B9BCB9", "#B9BCB9");
}
export class MobileTypographyToken {
@@ -85,6 +90,7 @@ export class MobileDesignTypography {
static readonly labelLarge: MobileTypographyToken = new MobileTypographyToken(15, 20, 500);
static readonly labelMedium: MobileTypographyToken = new MobileTypographyToken(14, 18, 500);
static readonly labelSmall: MobileTypographyToken = new MobileTypographyToken(12, 16, 400);
+ static readonly brandWordmark: MobileTypographyToken = new MobileTypographyToken(42, 56, 500);
}
export class MobileDesignGeometry {
@@ -147,6 +153,22 @@ export class MobileDesignGeometry {
static readonly modelListBottomPadding: number = 16;
static readonly modelEmptyAccountHeight: number = 80;
static readonly modelEditorHeight: number = 560;
+ static readonly welcomeMaxWidth: number = 520;
+ static readonly welcomeGutter: number = 25;
+ static readonly welcomeHeaderHeight: number = 58;
+ static readonly welcomeButtonHeight: number = 49;
+ static readonly welcomeButtonGap: number = 11;
+ static readonly welcomeDockRadius: number = 30;
+ static readonly welcomeDockBottom: number = 39;
+ static readonly welcomeMarkSize: number = 92;
+ static readonly welcomeWordSize: number = 34;
+ static readonly welcomeStageHeight: number = 220;
+ static readonly welcomeHeaderWordSize: number = 17;
+ static readonly recentHomeGutter: number = 24;
+ static readonly recentHomeMarkSize: number = 132;
+ static readonly recentHomeTitleSize: number = 25;
+ static readonly recentHomeMaxWidth: number = 560;
+ static readonly recentHomeRowPadding: number = 18;
}
export class MobileDesignBreakpoints {
@@ -158,4 +180,5 @@ export class MobileDesignBreakpoints {
export class MobileDesignMotion {
static readonly quick: number = 180;
static readonly structure: number = 220;
+ static readonly startupBrand: number = 6800;
}
diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/EnUsMessages.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/EnUsMessages.ets
index 708b2dfb55..95fd8ddb4a 100644
--- a/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/EnUsMessages.ets
+++ b/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/EnUsMessages.ets
@@ -1,4 +1,34 @@
export const EN_US_MESSAGES: [string, string][] = [
+ ['home.recent_title', 'What would you like to do today?'],
+ ['home.recent_recent', 'Recent conversations'],
+ ['home.recent_browse', 'Browse workspaces'],
+ ['home.recent_all', 'All conversations'],
+ ['home.recent_empty', 'No conversations to continue yet'],
+ ['home.recent_connect', 'Connect a computer to continue your conversations.'],
+ ['account.errorNetwork', 'Unable to connect. Check your connection and try again.'],
+ ['account.errorTimeout', 'Connection timed out. Check your connection and try again.'],
+ ['account.errorSecureConnection', 'Unable to establish a secure connection. Check your network and try again.'],
+ ['account.errorRateLimited', 'Too many attempts. Try again later.'],
+ ['home.welcome_login', 'Sign in'],
+ ['home.welcome_scan', 'Scan to connect'],
+ ['home.welcome_work', 'Get things done'],
+ ['home.welcome_play', 'Try something fun'],
+ ['home.welcome_yours', 'Make it yours'],
+ ['home.connectComputer', 'Connect computer'],
+ ['home.connecting', 'Connecting…'],
+ ['home.exploreMiniApps', 'Explore MiniApps'],
+ ['home.miniAppsHint', 'Ready to use, even without a computer'],
+ ['miniapp.allApps', 'All apps'],
+ ['miniapp.availableOffline', 'Available offline'],
+
+ ['miniapp.timeout', 'The local operation timed out. It may still finish; it was not retried.'],
+ ['miniapp.title', 'MiniApps'],
+ ['miniapp.refresh', 'Refresh'],
+ ['miniapp.empty', 'No built-in MiniApps are available.'],
+ ['miniapp.unsupported', 'This capability is unavailable in built-in mobile MiniApps.'],
+ ['miniapp.invalidPage', 'The MiniApp page is incomplete. Reopen it.'],
+ ['miniapp.closed', 'The MiniApp was closed or changed.'],
+ ['miniapp.previewHint', 'Built in · Works offline. Data stays on this phone.'],
['notification.onboardingTitle', 'Task completion notifications'],
['notification.onboardingBody', 'Allow OpenBitFun to notify you when tasks finish. You can change this later in system settings.'],
['notification.onboardingLater', 'Later'],
@@ -259,7 +289,7 @@ export const EN_US_MESSAGES: [string, string][] = [
['remote.settings.loginServer', 'Sign-in server'],
['remote.settings.accountSignIn', 'Sign in with GitHub'],
['remote.settings.accountSigningIn', 'Signing in…'],
- ['remote.settings.accountLoginFailed', 'Sign-in failed. Check the account details or server settings.'],
+ ['remote.settings.accountLoginFailed', 'Unable to sign in right now. Try again later.'],
['remote.settings.relayUrlPlaceholder', 'Relay URL, for example https://relay.example.com'],
['remote.settings.accountLogout', 'Sign out'],
['remote.settings.accountLoggingOut', 'Signing out…'],
diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/ZhCnMessages.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/ZhCnMessages.ets
index 35f1802977..31d912904a 100644
--- a/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/ZhCnMessages.ets
+++ b/src/apps/mobile/harmonyos/entry/src/main/ets/i18n/ZhCnMessages.ets
@@ -1,4 +1,34 @@
export const ZH_CN_MESSAGES: [string, string][] = [
+ ['home.recent_title', '今天,想做点什么?'],
+ ['home.recent_recent', '最近会话'],
+ ['home.recent_browse', '浏览工作区'],
+ ['home.recent_all', '全部会话'],
+ ['home.recent_empty', '还没有可继续的对话'],
+ ['home.recent_connect', '连接电脑后,继续你的对话。'],
+ ['account.errorNetwork', '暂时无法连接,请检查网络后重试。'],
+ ['account.errorTimeout', '连接超时,请检查网络后重试。'],
+ ['account.errorSecureConnection', '无法建立安全连接,请检查网络后重试。'],
+ ['account.errorRateLimited', '操作太频繁,请稍后重试。'],
+ ['home.welcome_login', '登录账号'],
+ ['home.welcome_scan', '扫码连接电脑'],
+ ['home.welcome_work', '干点活儿'],
+ ['home.welcome_play', '玩点花样'],
+ ['home.welcome_yours', '随你发挥'],
+ ['home.connectComputer', '连接电脑'],
+ ['home.connecting', '正在连接…'],
+ ['home.exploreMiniApps', '探索小应用'],
+ ['home.miniAppsHint', '无需连接电脑,也能即刻使用'],
+ ['miniapp.allApps', '全部应用'],
+ ['miniapp.availableOffline', '离线可用'],
+
+ ['miniapp.timeout', '本地操作超时,操作可能仍会完成,未自动重试。'],
+ ['miniapp.title', '小应用'],
+ ['miniapp.refresh', '刷新'],
+ ['miniapp.empty', '暂无内置小应用。'],
+ ['miniapp.unsupported', '手机内置小应用不支持此能力。'],
+ ['miniapp.invalidPage', '小应用页面数据不完整,请重新打开。'],
+ ['miniapp.closed', '小应用已关闭或切换。'],
+ ['miniapp.previewHint', '内置应用 · 离线可用,数据保存在本机。'],
['notification.onboardingTitle', '开启任务完成提醒'],
['notification.onboardingBody', '允许 OpenBitFun 在任务完成时发送通知。你可以稍后在系统设置中更改。'],
['notification.onboardingLater', '稍后'],
@@ -259,7 +289,7 @@ export const ZH_CN_MESSAGES: [string, string][] = [
['remote.settings.loginServer', '登录服务器'],
['remote.settings.accountSignIn', '使用 GitHub 登录'],
['remote.settings.accountSigningIn', '正在登录…'],
- ['remote.settings.accountLoginFailed', '登录失败,请检查账号密码或服务器设置。'],
+ ['remote.settings.accountLoginFailed', '暂时无法登录,请稍后重试。'],
['remote.settings.relayUrlPlaceholder', 'Relay 地址,例如 https://relay.example.com'],
['remote.settings.accountLogout', '退出登录'],
['remote.settings.accountLoggingOut', '正在退出…'],
diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/model/MiniAppModels.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/model/MiniAppModels.ets
new file mode 100644
index 0000000000..558e8dd0fa
--- /dev/null
+++ b/src/apps/mobile/harmonyos/entry/src/main/ets/model/MiniAppModels.ets
@@ -0,0 +1,46 @@
+export interface MiniAppSummary {
+ id: string;
+ name: string;
+ description: string;
+}
+
+export interface MiniAppBridgeParams {
+ key?: string;
+ value?: Object | null;
+ text?: string;
+}
+
+export interface MiniAppBridgeRequest {
+ id: string;
+ method: string;
+ params?: MiniAppBridgeParams;
+}
+
+export interface MiniAppBridgeError { message: string; }
+export interface MiniAppBridgeReply {
+ jsonrpc: string;
+ id: string;
+ result?: Object | null;
+ error?: MiniAppBridgeError;
+}
+
+/** Local data source, deliberately independent of any connection/session. */
+export interface MiniAppHost {
+ list(locale: string): Promise;
+ document(appId: string): Promise;
+ call(appId: string, method: string, params: MiniAppBridgeParams): Promise