mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-02 06:24:16 +02:00
feat: event firmware easter egg with ambient branding (#5354)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Generated
+4
@@ -340,6 +340,10 @@ establishing_session
|
||||
ethernet_config
|
||||
ethernet_enabled
|
||||
ethernet_ip
|
||||
event_welcome_burning_man
|
||||
event_welcome_defcon
|
||||
event_welcome_hamvention
|
||||
event_welcome_open_sauce
|
||||
exchange_position
|
||||
expand_chart
|
||||
expires
|
||||
|
||||
@@ -69,6 +69,7 @@ import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
|
||||
import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider
|
||||
import org.meshtastic.core.ui.util.LocalBarcodeScannerSupported
|
||||
import org.meshtastic.core.ui.util.LocalEventBranding
|
||||
import org.meshtastic.core.ui.util.LocalInlineMapProvider
|
||||
import org.meshtastic.core.ui.util.LocalMapMainScreenProvider
|
||||
import org.meshtastic.core.ui.util.LocalMapViewProvider
|
||||
@@ -179,9 +180,12 @@ class MainActivity : AppCompatActivity() {
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun AppCompositionLocals(content: @Composable () -> Unit) {
|
||||
val eventEdition by model.eventEdition.collectAsStateWithLifecycle()
|
||||
CompositionLocalProvider(
|
||||
LocalEventBranding provides eventEdition,
|
||||
LocalBarcodeScannerProvider provides { onResult -> rememberBarcodeScanner(onResult) },
|
||||
LocalNfcScannerProvider provides { onResult, onDisabled -> NfcScannerEffect(onResult, onDisabled) },
|
||||
LocalBarcodeScannerSupported provides true,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.event_welcome_burning_man
|
||||
import org.meshtastic.core.resources.event_welcome_defcon
|
||||
import org.meshtastic.core.resources.event_welcome_hamvention
|
||||
import org.meshtastic.core.resources.event_welcome_open_sauce
|
||||
import org.meshtastic.core.resources.img_event_hamvention
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
|
||||
/**
|
||||
* Display metadata for event-specific firmware editions. Maps a [FirmwareEdition] proto enum to the resources needed to
|
||||
* render event branding in the UI (toolbar icon, welcome message, etc.).
|
||||
*/
|
||||
data class EventEdition(val name: String, val iconRes: DrawableResource?, val welcomeMessageRes: StringResource)
|
||||
|
||||
/**
|
||||
* Maps a [FirmwareEdition] to its [EventEdition] display metadata, or `null` if the edition is not an event (e.g.,
|
||||
* VANILLA, SMART_CITIZEN) or is unrecognized.
|
||||
*/
|
||||
fun FirmwareEdition.toEventEdition(): EventEdition? = when (this) {
|
||||
FirmwareEdition.HAMVENTION ->
|
||||
EventEdition(
|
||||
name = "Hamvention",
|
||||
iconRes = Res.drawable.img_event_hamvention,
|
||||
welcomeMessageRes = Res.string.event_welcome_hamvention,
|
||||
)
|
||||
|
||||
FirmwareEdition.OPEN_SAUCE ->
|
||||
EventEdition(name = "Open Sauce", iconRes = null, welcomeMessageRes = Res.string.event_welcome_open_sauce)
|
||||
|
||||
FirmwareEdition.DEFCON ->
|
||||
EventEdition(name = "DEFCON", iconRes = null, welcomeMessageRes = Res.string.event_welcome_defcon)
|
||||
|
||||
FirmwareEdition.BURNING_MAN ->
|
||||
EventEdition(name = "Burning Man", iconRes = null, welcomeMessageRes = Res.string.event_welcome_burning_man)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/** Returns `true` if this edition represents an event firmware (value >= 16, excluding DIY). */
|
||||
fun FirmwareEdition.isEventEdition(): Boolean = toEventEdition() != null
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
@@ -364,6 +364,10 @@
|
||||
<string name="ethernet_config">Ethernet Options</string>
|
||||
<string name="ethernet_enabled">Ethernet enabled</string>
|
||||
<string name="ethernet_ip">Ethernet IP:</string>
|
||||
<string name="event_welcome_burning_man">Welcome to Burning Man! 🔥</string>
|
||||
<string name="event_welcome_defcon">Welcome to DEFCON! 💀</string>
|
||||
<string name="event_welcome_hamvention">Welcome to Hamvention! 🍖📻</string>
|
||||
<string name="event_welcome_open_sauce">Welcome to Open Sauce! 🔧</string>
|
||||
<string name="exchange_position">Exchange position</string>
|
||||
<string name="expand_chart">Expand chart</string>
|
||||
<string name="expires">Expires</string>
|
||||
|
||||
@@ -19,7 +19,11 @@ package org.meshtastic.core.ui.component
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -28,17 +32,27 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ic_meshtastic
|
||||
import org.meshtastic.core.resources.navigate_back
|
||||
import org.meshtastic.core.ui.icon.ArrowBack
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.util.LocalEventBranding
|
||||
import org.meshtastic.core.ui.util.SnackbarManager
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -52,6 +66,7 @@ fun MainAppBar(
|
||||
onNavigateUp: () -> Unit,
|
||||
actions: @Composable () -> Unit,
|
||||
onClickChip: (Node) -> Unit,
|
||||
brandingContent: @Composable () -> Unit = { EventAwareBranding() },
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
@@ -84,7 +99,7 @@ fun MainAppBar(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{ Icon(imageVector = vectorResource(Res.drawable.ic_meshtastic), contentDescription = null) }
|
||||
{ brandingContent() }
|
||||
},
|
||||
actions = {
|
||||
TopBarActions(ourNode = ourNode, showNodeChip = showNodeChip, actions = actions, onClickChip = onClickChip)
|
||||
@@ -92,6 +107,31 @@ fun MainAppBar(
|
||||
)
|
||||
}
|
||||
|
||||
/** Reads [LocalEventBranding] to show event artwork (with tap → snackbar), or the default Meshtastic logo. */
|
||||
@Composable
|
||||
private fun EventAwareBranding() {
|
||||
val eventEdition = LocalEventBranding.current
|
||||
val iconRes = eventEdition?.iconRes
|
||||
if (iconRes != null) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarManager = koinInject<SnackbarManager>()
|
||||
Image(
|
||||
painter = painterResource(iconRes),
|
||||
contentDescription = eventEdition.name,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier =
|
||||
Modifier.size(32.dp).clip(CircleShape).clickable(role = Role.Button) {
|
||||
scope.launch {
|
||||
val message = getString(eventEdition.welcomeMessageRes)
|
||||
snackbarManager.showSnackbar(message)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Icon(imageVector = vectorResource(Res.drawable.ic_meshtastic), contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopBarActions(
|
||||
ourNode: Node?,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import org.meshtastic.core.model.EventEdition
|
||||
|
||||
/**
|
||||
* Provides the active [EventEdition] (if any) to the composition tree. When a connected device reports an event
|
||||
* firmware edition, this local is populated at the app root so that
|
||||
* [MainAppBar][org.meshtastic.core.ui.component.MainAppBar] can display event branding automatically — no per-screen
|
||||
* wiring needed.
|
||||
*/
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
val LocalEventBranding = compositionLocalOf<EventEdition?> { null }
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -38,12 +39,15 @@ import org.jetbrains.compose.resources.getString
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.EventEdition
|
||||
import org.meshtastic.core.model.MeshActivity
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.TracerouteMapAvailability
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.service.TracerouteResponse
|
||||
import org.meshtastic.core.model.toEventEdition
|
||||
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
||||
import org.meshtastic.core.navigation.DeepLinkRouter
|
||||
import org.meshtastic.core.repository.FirmwareReleaseRepository
|
||||
@@ -120,6 +124,12 @@ class UIViewModel(
|
||||
|
||||
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmware_edition }
|
||||
|
||||
val eventEdition: StateFlow<EventEdition?> =
|
||||
combine(firmwareEdition, connectionState) { edition, state ->
|
||||
if (state is ConnectionState.Connected) edition?.toEventEdition() else null
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = null)
|
||||
|
||||
val clientNotification: StateFlow<ClientNotification?> = serviceRepository.clientNotification
|
||||
|
||||
fun clearClientNotification(notification: ClientNotification) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.meshtastic.desktop
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -77,6 +78,7 @@ import org.meshtastic.core.resources.desktop_tray_show
|
||||
import org.meshtastic.core.resources.desktop_tray_tooltip
|
||||
import org.meshtastic.core.service.MeshServiceOrchestrator
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.LocalEventBranding
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
import org.meshtastic.desktop.data.DesktopPreferencesDataSource
|
||||
import org.meshtastic.desktop.di.desktopModule
|
||||
@@ -300,9 +302,13 @@ private fun ApplicationScope.MeshtasticWindow(
|
||||
state = windowState,
|
||||
onPreviewKeyEvent = { event -> handleKeyboardShortcut(event, multiBackstack, ::exitApplication) },
|
||||
) {
|
||||
val eventEdition by uiViewModel.eventEdition.collectAsState()
|
||||
|
||||
CoilImageLoaderSetup()
|
||||
AppTheme(darkTheme = isDarkTheme, contrastLevel = contrastLevel) {
|
||||
DesktopMainScreen(uiViewModel, multiBackstack)
|
||||
CompositionLocalProvider(LocalEventBranding provides eventEdition) {
|
||||
AppTheme(darkTheme = isDarkTheme, contrastLevel = contrastLevel) {
|
||||
DesktopMainScreen(uiViewModel, multiBackstack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -35,6 +35,7 @@ import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.SessionStatus
|
||||
import org.meshtastic.core.model.toEventEdition
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.administration
|
||||
import org.meshtastic.core.resources.connect_radio_for_remote_admin
|
||||
@@ -189,16 +190,18 @@ private fun FirmwareSection(
|
||||
SectionCard(title = Res.string.firmware) {
|
||||
Column {
|
||||
firmwareEdition?.let { edition ->
|
||||
val eventEdition = edition.toEventEdition()
|
||||
val icon =
|
||||
when (edition) {
|
||||
FirmwareEdition.VANILLA -> MeshtasticIcons.Icecream
|
||||
else -> MeshtasticIcons.ForkLeft
|
||||
}
|
||||
val displayName = eventEdition?.name ?: edition.name
|
||||
|
||||
ListItem(
|
||||
text = stringResource(Res.string.firmware_edition),
|
||||
leadingIcon = icon,
|
||||
supportingText = edition.name,
|
||||
supportingText = displayName,
|
||||
copyable = true,
|
||||
trailingIcon = null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user